Author: 6630a6

  • Python Bottle Lightweight Web Framework

    Looking for a fast, single‑file web framework that lets you prototype APIs and micro‑services in minutes? Python Bottle is the perfect blend of simplicity and power, offering a lightweight solution that doesn’t sacrifice flexibility. In this guide we’ll explore what makes Bottle stand out, walk through a complete “Hello World” example, and uncover advanced features like routing, templating, plugins, and deployment options—all while keeping the codebase under 100 KB.

    What Is Bottle?

    Bottle is a micro web framework for Python, designed to be easy to install, understand, and extend. It follows the philosophy of “one file, zero dependencies” (apart from the Python standard library), which makes it ideal for:

    • Quick prototypes and proof‑of‑concept projects.
    • Embedded devices or serverless functions where size matters.
    • Learning the fundamentals of web development without the overhead of larger frameworks.

    Despite its minimal footprint, Bottle supports routing, request/response handling, templates, static files, and a robust plugin system—features you’d expect from full‑stack frameworks like Flask or Django.

    Getting Started: Installation and First App

    Installation in One Command

    Because Bottle is pure Python, installing it is straightforward:

    pip install bottle

    If you prefer a single‑file deployment, you can even download bottle.py directly from the official repository and place it alongside your script.

    “Hello World” in 10 Lines

    Below is a minimal Bottle application that listens on localhost:8080 and returns “Hello, World!” when you visit the root URL.

    from bottle import route, run
    
    @route('/')
    def hello():
        return "Hello, World!"
    
    run(host='localhost', port=8080, debug=True)

    Save this as app.py and run python app.py. Open http://localhost:8080 in your browser to see the result.

    Core Features of Bottle

    1. Powerful Routing System

    Bottle’s routing engine lets you define dynamic URLs with placeholders, regular expressions, and HTTP method constraints.

    @route('/user/<id:int>', method='GET')
    def get_user(id):
        return f"User ID: {id}"

    Supported converters include int, float, path, and custom regex patterns, giving you fine‑grained control over URL matching.

    2. Request and Response Objects

    Access request data via bottle.request and manipulate outgoing headers with bottle.response:

    from bottle import request, response
    
    @route('/login', method='POST')
    def login():
        username = request.forms.get('username')
        response.set_cookie('user', username, secret='my_secret')
        return f"Welcome, {username}!"

    3. Built‑In Template Engine

    Bottle includes a simple yet powerful template language that supports variable interpolation, loops, and conditionals.

    # templates/hello.tpl
    <h1>Hello {{name}}!</h1>
    {% if items %}
      <ul>
      {% for item in items %}
        <li>{{item}}</li>
      {% endfor %}
      </ul>
    {% else %}
      <p>No items found.</p>
    {% endif %}

    Render the template from a route:

    from bottle import template
    
    @route('/greet/<name>')
    def greet(name):
        items = ['Apple', 'Banana', 'Cherry']
        return template('hello', name=name, items=items)

    4. Serving Static Files

    Static assets (CSS, JavaScript, images) can be served with a single decorator:

    @route('/static/<filename:path>')
    def static(filename):
        return static_file(filename, root='./static')

    This approach eliminates the need for a separate web server during development.

    5. Extensible Plugin Architecture

    Bottle’s plugin system lets you inject functionality—like database connections or authentication—into route handlers without cluttering your code.

    from bottle import PluginError
    
    class SimpleAuthPlugin(object):
        name = 'simple_auth'
        api = 2
    
        def __init__(self, secret):
            self.secret = secret
    
        def apply(self, callback, route):
            def wrapper(*args, **kwargs):
                token = request.get_cookie('auth', secret=self.secret)
                if not token:
                    return HTTPError(401, "Unauthorized")
                return callback(*args, **kwargs)
            return wrapper
    
    # Register the plugin
    app = Bottle()
    app.install(SimpleAuthPlugin(secret='my_secret'))

    When to Choose Bottle Over Other Frameworks

    • Minimal footprint: If you need a framework that fits on a single file or a tiny Docker image, Bottle wins.
    • Rapid prototyping: Write a full API in under an hour with zero configuration.
    • Embedded or serverless environments: Bottle’s low memory usage makes it ideal for AWS Lambda, Raspberry Pi, or IoT devices.
    • Learning fundamentals: Because Bottle strips away hidden magic, you see exactly how routing, request handling, and templating work under the hood.

    Conversely, for large‑scale applications with complex ORM needs, admin panels, or built‑in authentication, a more feature‑rich framework like Django or Flask (with extensions) might be a better fit.

    Best Practices for Production‑Ready Bottle Apps

    1. Separate configuration: Store settings (debug mode, secret keys, DB URLs) in environment variables or a dedicated .env file.
    2. Use a WSGI server: Deploy with gunicorn or uWSGI instead of Bottle’s built‑in server for better performance and stability.
    3. Enable logging: Configure Python’s logging module to capture request details and errors.
    4. Apply security headers: Add Content‑Security‑Policy, X‑Frame‑Options, and Strict‑Transport‑Security via a middleware plugin.
    5. Write unit tests: Use pytest with Bottle’s TestApp to simulate requests and verify responses.

    Example of running Bottle with Gunicorn:

    gunicorn -w 4 -b 0.0.0.0:8000 app:app

    Here app is the Bottle instance exported from app.py.

    Community, Documentation, and Learning Resources

    The Bottle ecosystem may be smaller than Flask’s, but it’s vibrant and well‑documented. Key resources include:

    • Official Bottle documentation – comprehensive API reference and tutorials.
    • GitHub repository – source code, issue tracker, and community contributions.
    • Community blogs and YouTube channels that showcase real‑world Bottle projects, such as building RESTful APIs, IoT dashboards, and serverless functions.

    Because Bottle is a single file, reading bottle.py itself is an excellent way to understand the inner workings of a web framework.

    Conclusion

    Python Bottle delivers a remarkable balance of lightweight design and , making it a go‑to choice for developers who value speed, simplicity, and full control over their code. Whether you’re building a quick API endpoint, a micro‑service for a larger architecture, or an embedded web interface on a device, Bottle’s single‑file footprint, powerful routing, and extensible plugin system provide everything you need without the bloat of larger frameworks. Install it today, experiment with the examples above, and discover how quickly you can turn a Python script into a production‑ready web application.

  • Python Tornado Async Framework Guide

    Python Tornado async framework guide – If you’re building real‑time web applications, WebSocket services, or high‑throughput APIs, Tornado’s non‑blocking I/O and native async/await support make it a top choice. In this comprehensive guide we’ll explore Tornado’s core concepts, walk through a step‑by‑step setup, and reveal best practices for writing clean, scalable asynchronous code. Whether you’re a seasoned Python developer or just starting with async programming, this article equips you with everything you need to master Tornado.

    Why Choose Tornado for Asynchronous Python?

    Tornado was created by FriendFeed (now part of Facebook) to handle thousands of simultaneous connections without spawning a new thread per request. Its key advantages include:

    • Event‑driven architecture: Powered by IOLoop, Tornado processes I/O events in a single thread, reducing context‑switch overhead.
    • Native async/await support: Since version 5.0, Tornado integrates seamlessly with Python’s asyncio library.
    • WebSocket & long‑polling: Built‑in handlers simplify real‑time communication.
    • Extensible request handling: Mix classic RequestHandler classes with coroutine‑based handlers for flexible code reuse.
    • Production‑ready: Tornado runs as a standalone server or behind a reverse proxy (NGINX, HAProxy) and scales horizontally with tornado.process.fork_processes.

    Getting Started: Installation and Project Layout

    Step 1 – Install Tornado

    Use pip to install the latest stable release. It’s recommended to work inside a virtual environment:

    python -m venv venv
    source venv/bin/activate  # Windows: venv\Scripts\activate
    pip install tornado
    

    Step 2 – Basic Project Structure

    A clean layout helps maintainability as the codebase grows:

    my_tornado_app/
    ├── app.py               # Entry point, IOLoop configuration
    ├── handlers/
    │   ├── __init__.py
    │   └── main_handler.py  # Example RequestHandler classes
    ├── static/              # CSS, JS, images
    └── templates/           # HTML/Jinja2 templates
    

    Core Concepts of Tornado

    The IOLoop

    The IOLoop is Tornado’s event loop. It watches file descriptors, timers, and callbacks, executing them when they become ready. In most applications you’ll interact with it like this:

    import tornado.ioloop
    
    if __name__ == "__main__":
        tornado.ioloop.IOLoop.current().start()
    

    When you use async def in a handler, Tornado automatically registers the coroutine with the running IOLoop.

    RequestHandler and Asynchronous Methods

    Every HTTP endpoint is represented by a subclass of tornado.web.RequestHandler. To make a handler asynchronous, simply define an async method and use await for I/O operations:

    import tornado.web
    import aiohttp
    
    class AsyncFetchHandler(tornado.web.RequestHandler):
        async def get(self):
            url = self.get_argument('url')
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as resp:
                    data = await resp.text()
            self.write({"fetched": data[:200]})
    

    WebSocketHandler

    Tornado’s WebSocketHandler abstracts the WebSocket handshake and message framing. Below is a minimal echo server:

    import tornado.websocket
    
    class EchoWebSocket(tornado.websocket.WebSocketHandler):
        async def open(self):
            print("WebSocket opened")
    
        async def on_message(self, message):
            await self.write_message(f"Echo: {message}")
    
        def on_close(self):
            print("WebSocket closed")
    

    Building a Real‑World Async API with Tornado

    1. Define URL Routes

    Routes map URLs to handler classes. Use a list of tuples and pass it to tornado.web.Application:

    import tornado.web
    from handlers.main_handler import AsyncFetchHandler, EchoWebSocket
    
    def make_app():
        return tornado.web.Application([
            (r"/fetch", AsyncFetchHandler),
            (r"/ws", EchoWebSocket),
        ], debug=True)  # Debug enables auto‑reload during development
    

    2. Run the Server

    The entry point creates the application, binds to a port, and starts the IOLoop:

    import tornado.ioloop
    from app import make_app
    
    if __name__ == "__main__":
        app = make_app()
        app.listen(8888)  # Change to 80/443 behind a proxy if needed
        print("Tornado server listening on http://localhost:8888")
        tornado.ioloop.IOLoop.current().start()
    

    3. Adding Database Access

    For async database operations, choose a driver that supports asyncio (e.g., aiopg for PostgreSQL, motor for MongoDB). Example with motor:

    import motor.motor_tornado
    
    client = motor.motor_tornado.MotorClient("mongodb://localhost:27017")
    db = client.mydb
    
    class UserHandler(tornado.web.RequestHandler):
        async def get(self, user_id):
            user = await db.users.find_one({"_id": user_id})
            if user:
                self.write(user)
            else:
                self.set_status(404)
                self.write({"error": "User not found"})
    

    Performance Tuning and Best Practices

    • Use asyncio compatible libraries: Mixing blocking I/O (e.g., requests) with Tornado will stall the event loop. Prefer aiohttp, aiomysql, etc.
    • Leverage tornado.httpclient.AsyncHTTPClient for internal calls: It reuses a connection pool and integrates natively with the IOLoop.
    • Enable debug=True only in development: Debug mode adds overhead and should be disabled in production.
    • Set proper timeouts: Use request_timeout and connect_timeout on HTTP clients to avoid hanging connections.
    • Scale with multiple processes: tornado.process.fork_processes spawns workers equal to CPU cores, distributing load without GIL contention.

    Example: Forking Workers

    import tornado.process
    import tornado.web
    
    def main():
        app = make_app()
        app.listen(8888)
        tornado.process.fork_processes(0)  # 0 = number of CPU cores
        tornado.ioloop.IOLoop.current().start()
    
    if __name__ == "__main__":
        main()
    

    Testing Tornado Applications

    Tornado provides AsyncHTTPTestCase for unit testing async handlers. Combine it with unittest or pytest for a full test suite.

    import tornado.testing
    import tornado.web
    from handlers.main_handler import AsyncFetchHandler
    
    class FetchTest(tornado.testing.AsyncHTTPTestCase):
        def get_app(self):
            return tornado.web.Application([(r"/fetch", AsyncFetchHandler)])
    
        @tornado.testing.gen_test
        async def test_fetch_success(self):
            response = await self.fetch("/fetch?url=http://example.com")
            self.assertEqual(response.code, 200)
            self.assertIn(b"fetched", response.body)
    

    Deploying Tornado in Production

    1. Containerize with Docker: Build a lightweight image using python:3.12-slim and expose port 8888.
    2. Use a reverse proxy: NGINX can handle TLS termination, static file serving, and request buffering.
    3. Monitor with Prometheus: Export metrics via tornado.metrics or custom endpoints.
    4. Graceful shutdown: Capture SIGTERM and stop the IOLoop after completing pending coroutines.

    Sample Dockerfile:

    FROM python:3.12-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install -r requirements.txt
    COPY . .
    EXPOSE 8888
    CMD ["python", "app.py"]
    

    Common Pitfalls and How to Avoid Them

    • Blocking code in async handlers: Even a single time.sleep() will block the entire server. Replace with await asyncio.sleep().
    • Improper exception handling: Uncaught exceptions terminate the coroutine silently. Use try/except blocks and self.set_status(500) for API errors.
    • Mixing asyncio loops: Ensure only one event loop runs per process. Avoid calling asyncio.run() inside a Tornado handler.
    • Large payloads without streaming: For file uploads/downloads, use stream_request_body=True and chunked responses to keep memory usage low.

    Conclusion

    Python Tornado remains a powerful, battle‑tested async framework that excels at handling thousands of concurrent connections with minimal

  • Python Microservices Grpc Tutorial

    Python microservices gRPC tutorial – In today’s cloud‑native world, building scalable, low‑latency services is essential. While REST APIs remain popular, gRPC offers a binary protocol, contract‑first design, and built‑in support for streaming, making it a perfect fit for Python microservices. This step‑by‑step guide walks you through setting up a gRPC‑based microservice architecture in Python, from installing dependencies to containerizing the service for production. By the end, you’ll have a fully functional example you can extend, test, and deploy with confidence.

    Why Choose gRPC for Python Microservices?

    • Performance: gRPC uses HTTP/2 and Protocol Buffers, delivering faster serialization and lower bandwidth usage than JSON over HTTP/1.1.
    • Strong typing: The .proto contract enforces data schemas across services, reducing runtime errors.
    • Streaming support: Built‑in client, server, and bidirectional streaming simplify real‑time data pipelines.
    • Cross‑language interoperability: A single .proto file can generate client and server code for Python, Go, Java, and more.
    • Built‑in load balancing & authentication: gRPC integrates with modern service meshes like Istio.

    Prerequisites

    1. Python 3.9+ installed on your workstation.
    2. Basic familiarity with microservice architecture concepts.
    3. Docker (optional, but recommended for deployment).
    4. Text editor or IDE of your choice.

    Step 1 – Set Up the Python Environment

    Create a virtual environment

    python -m venv grpc-env
    source grpc-env/bin/activate  # On Windows use `grpc-env\Scripts\activate`
    

    Install required packages

    pip install grpcio grpcio-tools protobuf
    

    These packages provide the core gRPC runtime (grpcio) and the code‑generation tools (grpcio-tools) that compile .proto definitions into Python modules.

    Step 2 – Define the Service Contract with Protocol Buffers

    All gRPC communication starts with a .proto file that describes messages and RPC methods. Create a folder proto/ and add calculator.proto:

    syntax = "proto3";
    
    package calculator;
    
    // The request message containing two numbers.
    message Operands {
      double a = 1;
      double b = 2;
    }
    
    // The response message containing the result.
    message Result {
      double value = 1;
    }
    
    // The Calculator service definition.
    service Calculator {
      // Unary RPC.
      rpc Add (Operands) returns (Result);
      rpc Subtract (Operands) returns (Result);
    
      // Server‑streaming RPC.
      rpc PrimeFactors (Operands) returns (stream Result);
    }
    

    This contract defines a simple Calculator microservice with two unary methods (Add, Subtract) and one server‑streaming method (PrimeFactors).

    Step 3 – Generate Python Code from the .proto File

    python -m grpc_tools.protoc -I./proto --python_out=./generated --grpc_python_out=./generated ./proto/calculator.proto
    

    The command creates two files in generated/:

    • calculator_pb2.py – contains the message classes.
    • calculator_pb2_grpc.py – contains the service stub and base classes.

    Step 4 – Implement the gRPC Server

    Server code (server.py)

    import grpc
    from concurrent import futures
    import time
    import math
    
    # Import the generated classes
    import generated.calculator_pb2 as calc_pb2
    import generated.calculator_pb2_grpc as calc_pb2_grpc
    
    class CalculatorServicer(calc_pb2_grpc.CalculatorServicer):
        """Implements the Calculator service defined in calculator.proto."""
    
        def Add(self, request, context):
            result = request.a + request.b
            return calc_pb2.Result(value=result)
    
        def Subtract(self, request, context):
            result = request.a - request.b
            return calc_pb2.Result(value=result)
    
        def PrimeFactors(self, request, context):
            """Server‑streaming RPC that yields prime factors of `a`."""
            n = int(request.a)
            factor = 2
            while n > 1:
                while n % factor == 0:
                    yield calc_pb2.Result(value=factor)
                    n //= factor
                factor += 1
    
    def serve():
        server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
        calc_pb2_grpc.add_CalculatorServicer_to_server(CalculatorServicer(), server)
        server.add_insecure_port('[::]:50051')
        server.start()
        print("🔧 gRPC server listening on port 50051...")
        try:
            while True:
                time.sleep(86400)  # Keep server alive for 1 day
        except KeyboardInterrupt:
            server.stop(0)
    
    if __name__ == '__main__':
        serve()
    

    Key points:

    • We use a ThreadPoolExecutor for concurrency, allowing multiple RPCs to be handled in parallel.
    • The PrimeFactors method demonstrates server‑streaming by yielding Result messages.

    Step 5 – Build the gRPC Client

    Client code (client.py)

    import grpc
    import generated.calculator_pb2 as calc_pb2
    import generated.calculator_pb2_grpc as calc_pb2_grpc
    
    def run():
        # Establish a channel to the server (insecure for demo purposes)
        channel = grpc.insecure_channel('localhost:50051')
        stub = calc_pb2_grpc.CalculatorStub(channel)
    
        # Unary calls
        add_response = stub.Add(calc_pb2.Operands(a=15, b=27))
        print(f"Add: 15 + 27 = {add_response.value}")
    
        sub_response = stub.Subtract(calc_pb2.Operands(a=15, b=27))
        print(f"Subtract: 15 - 27 = {sub_response.value}")
    
        # Server‑streaming call
        print("Prime factors of 84:")
        for factor in stub.PrimeFactors(calc_pb2.Operands(a=84, b=0)):
            print(f"  - {int(factor.value)}")
    
    if __name__ == '__main__':
        run()
    

    The client demonstrates both unary and streaming RPCs. In production, you’d replace the insecure channel with grpc.secure_channel and configure TLS certificates.

    Step 6 – Dockerize the Microservice

    Containerization ensures consistent runtime across environments. Create a Dockerfile in the project root:

    # Use official Python slim image
    FROM python:3.11-slim
    
    # Set working directory
    WORKDIR /app
    
    # Copy only requirements first for caching
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    # Copy source code
    COPY . /app
    
    # Expose gRPC port
    EXPOSE 50051
    
    # Default command runs the server
    CMD ["python", "server.py"]
    

    Generate requirements.txt:

    grpcio==1.64.0
    grpcio-tools==1.64.0
    protobuf==5.27.0
    

    Build and run the container:

    docker build -t python-grpc-calculator .
    docker run -p 50051:50051 python-grpc-calculator
    

    Now the service is accessible at localhost:50051 from any client, including the client.py script running on the host machine.

    Step 7 – Test the Service with Automated Unit Tests

    Testing with pytest and grpc-testing

    pip install pytest pytest-mock
    

    Create test_calculator.py:

    import grpc
    import pytest
    from concurrent import futures
    
    import generated.calculator_pb2 as calc_pb2
    import generated.calculator_pb2_grpc as calc_pb2_grpc
    from server import CalculatorServicer
    
    @pytest.fixture(scope="module")
    def grpc_server():
        server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
        calc_pb2_grpc.add_CalculatorServicer_to_server(CalculatorServicer(), server)
        port = server.add_insecure_port('localhost:0')
        server.start()
        yield f'localhost:{port}'
        server.stop(0)
    
    def test_add(grpc_server):
        channel = grpc.insecure_channel(grpc_server)
        stub = calc_pb2_grpc.CalculatorStub(channel)
        resp = stub.Add(calc_pb2.Operands(a=10, b=5))
        assert resp.value == 15
    
    def test_prime_factors(grpc_server):
        channel = grpc.insecure_channel(grpc_server)
        stub = calc_pb2_grpc.CalculatorStub(channel)
        factors = [int(r.value) for r in stub.PrimeFactors(calc_pb2.Operands(a=30, b=0))]
        assert factors == [2, 3, 5]
    

    Run the tests:

    pytest -q
    

    Best Practices for Python gRPC Microservices

    • Use TLS everywhere: Replace insecure_channel with
  • Python Openapi Auto Documentation Guide

    Generating up‑to‑date API documentation can feel like a never‑ending chore, especially when your Python services evolve quickly. Fortunately, the OpenAPI specification (formerly Swagger) and modern Python frameworks make it possible to automate the entire documentation pipeline with just a few lines of code. In this guide we’ll walk through everything you need to know to set up **Python OpenAPI auto documentation**, from choosing the right library to customizing the UI and deploying the spec for public consumption.

    Why Choose OpenAPI for Python Projects?

    OpenAPI is the industry‑standard format for describing RESTful APIs. It offers several tangible benefits for Python developers:

    • Language‑agnostic contracts: Clients in any language can generate SDKs directly from the spec.
    • Interactive documentation: Tools like Swagger UI and Redoc turn a JSON/YAML file into a live, testable API explorer.
    • Automation: When your code changes, the spec can be regenerated automatically, eliminating stale docs.
    • Validation & testing: Request/response schemas can be validated at runtime, catching errors early.

    Popular Python Frameworks with Built‑in OpenAPI Support

    Not all Python web frameworks handle OpenAPI equally. Below are the three most common choices for auto‑generated documentation.

    FastAPI

    FastAPI was built around OpenAPI from day one. Every endpoint automatically contributes to an OpenAPI schema, which is served at /openapi.json. Swagger UI and Redoc are available at /docs and /redoc respectively, without extra configuration.

    Flask + Flask‑RESTX

    Flask itself is minimal, but the flask-restx extension adds declarative route definitions and an api object that can generate a spec. The UI is served at /swagger/ by default.

    Django REST framework (DRF) + drf‑spectacular

    DRF focuses on serializers and viewsets. The drf-spectacular package introspects those components to produce a clean OpenAPI 3.0 document, which can be rendered with Swagger UI or Redoc.

    Step‑by‑Step: Auto‑Documenting a FastAPI Service

    FastAPI provides the smoothest experience, so we’ll use it as our primary example. Follow these steps to get a fully functional, auto‑generated OpenAPI doc.

    1. Install FastAPI and an ASGI server

    pip install fastapi uvicorn
    

    2. Create a simple API

    from fastapi import FastAPI, Path
    from pydantic import BaseModel
    
    app = FastAPI(title="Book Store API", version="1.0.0")
    
    class Book(BaseModel):
        id: int
        title: str
        author: str
        price: float
    
    # In‑memory store for demonstration
    books = {
        1: Book(id=1, title="1984", author="George Orwell", price=9.99),
        2: Book(id=2, title="Brave New World", author="Aldous Huxley", price=12.50),
    }
    
    @app.get("/books", response_model=list[Book])
    def list_books():
        """Return a list of all books."""
        return list(books.values())
    
    @app.get("/books/{book_id}", response_model=Book)
    def get_book(book_id: int = Path(..., gt=0)):
        """Retrieve a single book by its ID."""
        return books[book_id]
    
    @app.post("/books", response_model=Book, status_code=201)
    def create_book(book: Book):
        """Add a new book to the store."""
        books[book.id] = book
        return book
    

    3. Run the server

    uvicorn main:app --reload
    

    Once the server is up, visit http://127.0.0.1:8000/docs for Swagger UI or http://127.0.0.1:8000/redoc for Redoc. Both pages are generated from the OpenAPI spec automatically.

    4. Customizing the OpenAPI Metadata

    You can enrich the spec with additional information such as contact details, license, or external documentation.

    app = FastAPI(
        title="Book Store API",
        version="1.0.0",
        description="A tiny example API to demonstrate FastAPI's auto‑generated OpenAPI docs.",
        terms_of_service="https://example.com/terms/",
        contact={
            "name": "API Support",
            "url": "https://example.com/support",
            "email": "support@example.com",
        },
        license_info={"name": "MIT", "url": "https://opensource.org/licenses/MIT"},
    )
    

    5. Adding Tags and Summaries

    Tags group endpoints in the UI, while summary and description provide concise documentation.

    @app.get("/books", response_model=list[Book], tags=["Books"], summary="List all books")
    def list_books():
        ...
    

    Generating a Stand‑Alone OpenAPI File

    Sometimes you need the raw openapi.json or openapi.yaml for CI pipelines, SDK generation, or third‑party portals.

    • JSON: curl http://127.0.0.1:8000/openapi.json -o openapi.json
    • YAML: Use a conversion tool like pip install pyyaml and run a small script to dump the JSON as YAML.

    Auto‑Documentation with Flask‑RESTX

    If you’re already on Flask, flask-restx adds OpenAPI support with minimal boilerplate.

    Installation

    pip install flask-restx
    

    Basic Example

    from flask import Flask
    from flask_restx import Api, Resource, fields
    
    app = Flask(__name__)
    api = Api(app,
              version="1.0",
              title="Todo API",
              description="A simple Todo API with auto‑generated docs")
    
    todo = api.model('Todo', {
        'id': fields.Integer(readOnly=True, description='The unique identifier'),
        'task': fields.String(required=True, description='Task description')
    })
    
    TODOS = []
    
    @api.route('/todos')
    class TodoList(Resource):
        @api.marshal_list_with(todo)
        def get(self):
            """List all todos"""
            return TODOS
    
        @api.expect(todo)
        @api.marshal_with(todo, code=201)
        def post(self):
            """Create a new todo"""
            new = api.payload
            new['id'] = len(TODOS) + 1
            TODOS.append(new)
            return new, 201
    
    if __name__ == '__main__':
        app.run(debug=True)
    

    Navigate to http://localhost:5000/ and you’ll see Swagger UI rendered automatically.

    Integrating OpenAPI Generation into CI/CD

    Automation doesn’t stop at runtime. By adding a step to your CI pipeline you can ensure the spec is always in sync with the codebase.

    Typical Workflow

    1. Start the application in a headless mode (e.g., using uvicorn --port 0).
    2. Request /openapi.json and save it to the repository (or an artifact store).
    3. Run swagger-cli validate openapi.json to catch schema errors early.
    4. Optionally, generate client SDKs with openapi-generator-cli and publish them.

    Sample GitHub Actions Step

    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          - name: Set up Python
            uses: actions/setup-python@v5
            with:
              python-version: "3.12"
          - name: Install dependencies
            run: pip install fastapi uvicorn
          - name: Start API in background
            run: |
              uvicorn main:app --port 8000 &
              echo $! > uvicorn.pid
              sleep 5
          - name: Download OpenAPI spec
            run: |
              curl http://localhost:8000/openapi.json -o openapi.json
          - name: Validate spec
            run: npx @apidevtools/swagger-cli validate openapi.json
    

    Customizing the Documentation UI

    Both Swagger UI and Redoc are highly configurable through query parameters or a custom HTML template.

    Swagger UI Options

    • url – point to a remote spec file.
    • docExpansion – control how sections are collapsed (e.g., none, list, full).
    • defaultModelsExpandDepth – set the depth of model expansion.

    Example: http://localhost:8000/docs?url=/openapi.json&docExpansion=none

    Redoc Custom Theme

    Redoc allows a JSON theme object for branding.

    app = FastAPI(
        redoc_url="/redoc",
        redoc_opts={"theme": {"colors": {"primary": {"main": "#4caf50"}}}}
    )
    

    Best Practices for Maintaining Clean OpenAPI Specs

    • Version your
  • Python Graphql Api Consuming Guide

    If you’re a Python developer looking to tap into the power of GraphQL without building a server from scratch, you’ve landed in the right place. This comprehensive guide walks you through everything you need to know to consume a GraphQL API using Python—covering setup, query construction, error handling, authentication, and performance tricks. By the end of this article, you’ll be able to integrate GraphQL endpoints into your applications with confidence, making your data fetching more efficient and flexible than ever before.

    Why Choose GraphQL Over REST in Python Projects?

    GraphQL offers several compelling advantages that make it a natural fit for modern Python applications:

    • Exact data fetching: Clients request precisely the fields they need, reducing over‑ and under‑fetching.
    • Single endpoint: One HTTP endpoint handles all queries and mutations, simplifying routing and versioning.
    • Strong typing: The schema defines types and relationships, enabling better IDE support and automated documentation.
    • Real‑time capabilities: Subscriptions allow you to receive live updates directly from the server.

    These benefits translate into faster response times, lower bandwidth usage, and a smoother developer experience—especially when combined with Python’s rich ecosystem.

    Getting Started: Essential Tools and Libraries

    1. Choose a GraphQL client library

    While you could call a GraphQL endpoint with requests, dedicated clients provide conveniences such as query building, automatic retries, and caching. The most popular options are:

    • gql – A lightweight, async‑ready client that supports both queries and subscriptions.
    • sgqlc – Generates Python classes from a GraphQL schema for type‑safe queries.
    • Ariadne – Primarily a server library, but its graphql-core engine can be repurposed for client‑side validation.

    2. Install the chosen package

    pip install gql[all]      # installs gql with aiohttp and websockets support
    # or
    pip install sgqlc
    

    3. Optional helpers

    For advanced use cases, consider adding:

    • python-dotenv – Load API keys from .env files.
    • pydantic – Validate and parse GraphQL responses into Python models.

    Crafting Your First Query

    Understanding the GraphQL syntax

    A GraphQL request consists of a query (or mutation) string and an optional variables map. Here’s a simple example that fetches a list of books:

    query GetBooks($limit: Int!) {
      books(limit: $limit) {
        id
        title
        author {
          name
        }
        publishedYear
      }
    }
    

    Notice the use of $limit as a variable placeholder. This approach keeps queries reusable and safe from injection attacks.

    Executing the query with gql

    from gql import gql, Client
    from gql.transport.requests import RequestsHTTPTransport
    
    # 1️⃣ Define the transport (endpoint + optional headers)
    transport = RequestsHTTPTransport(
        url="https://api.example.com/graphql",
        headers={"Authorization": "Bearer YOUR_TOKEN"},
        verify=True,
        retries=3,
    )
    
    # 2️⃣ Create the client
    client = Client(transport=transport, fetch_schema_from_transport=True)
    
    # 3️⃣ Write the query
    query = gql("""
    query GetBooks($limit: Int!) {
      books(limit: $limit) {
        id
        title
        author { name }
        publishedYear
      }
    }
    """)
    
    # 4️⃣ Execute with variables
    params = {"limit": 5}
    result = client.execute(query, variable_values=params)
    
    print(result)
    

    The fetch_schema_from_transport=True flag introspects the server’s schema on first use, enabling autocomplete and validation in many IDEs.

    Handling Mutations and File Uploads

    Running a mutation

    Mutations follow the same pattern as queries but usually require authentication and may return updated objects. Example: creating a new user.

    mutation CreateUser($input: CreateUserInput!) {
      createUser(input: $input) {
        user {
          id
          email
          createdAt
        }
        token
      }
    }
    

    Python execution:

    mutation = gql("""...""")  # same string as above
    variables = {
        "input": {
            "email": "alice@example.com",
            "password": "StrongP@ssw0rd"
        }
    }
    response = client.execute(mutation, variable_values=variables)
    print(response["createUser"]["token"])
    

    Uploading files with GraphQL multipart request

    Some APIs support the GraphQL multipart request spec. The gql library can handle this via aiohttp transport.

    from gql.transport.aiohttp import AIOHTTPTransport
    import asyncio
    
    async def upload_image(file_path):
        transport = AIOHTTPTransport(
            url="https://api.example.com/graphql",
            headers={"Authorization": "Bearer TOKEN"},
        )
        async with Client(transport=transport, fetch_schema_from_transport=False) as session:
            mutation = gql("""
            mutation UploadAvatar($file: Upload!) {
              uploadAvatar(file: $file) {
                url
              }
            }
            """)
            with open(file_path, "rb") as f:
                result = await session.execute(
                    mutation,
                    variable_values={"file": f}
                )
            print(result["uploadAvatar"]["url"])
    
    asyncio.run(upload_image("avatar.png"))
    

    Authentication Strategies

    Most GraphQL services require some form of authentication. Here are the most common patterns and how to implement them in Python:

    1. Bearer token (OAuth2 or JWT) – Add an Authorization header to every request.
    2. API key – Pass a custom header like X-API-Key.
    3. Cookie‑based session – Use a requests.Session or aiohttp.ClientSession to persist cookies after a login mutation.

    Example of a token refresh workflow:

    def get_token():
        # Retrieve token from secure storage or request a new one
        ...
    
    def auth_headers():
        token = get_token()
        return {"Authorization": f"Bearer {token}"}
    
    transport = RequestsHTTPTransport(
        url="https://api.example.com/graphql",
        headers=auth_headers(),
        verify=True,
        retries=3,
    )
    

    Best Practices for Performance and Reliability

    Cache responses locally

    Because GraphQL queries are deterministic, caching at the client level can dramatically reduce network overhead. The gql library integrates with cachecontrol for HTTP‑level caching, or you can implement a simple in‑memory cache:

    from functools import lru_cache
    
    @lru_cache(maxsize=128)
    def fetch_books(limit):
        return client.execute(query, variable_values={"limit": limit})
    

    Batch multiple queries

    When you need several independent queries, consider sending them in a single HTTP request using query batching. Not all servers support it, but when they do, you can combine queries into an array:

    batch_payload = [
        {"query": query1, "variables": {"id": 1}},
        {"query": query2, "variables": {"search": "Python"}}
    ]
    response = requests.post(url, json=batch_payload, headers=headers).json()
    

    Handle errors gracefully

    GraphQL returns a errors array alongside the data field. Always check for errors before using the data:

    result = client.execute(query, variable_values=params)
    if result.get("errors"):
        for err in result["errors"]:
            print(f"Error: {err['message']}")
    else:
        books = result["books"]
        # Process books safely
    

    Use fragments for reusable field sets

    Fragments keep your queries DRY and make schema changes easier to propagate.

    fragment BookFields on Book {
      id
      title
      author { name }
      publishedYear
    }
    
    query GetFeaturedBooks {
      featuredBooks {
        ...BookFields
      }
    }
    

    Testing Your GraphQL Integration

    Automated tests ensure that changes to the API or your client code don’t break functionality. Here’s a quick pytest example using responses to mock HTTP calls:

    import pytest
    import responses
    from gql import gql, Client
    from gql.transport.requests import RequestsHTTPTransport
    
    @responses.activate
    def test_get_books():
        mock_data = {
            "data": {
                "books": [
                    {"id": "1", "title": "Python 101", "author": {"name": "Alice"}}
                ]
            }
        }
        responses.add(
            responses.POST,
            "https://api.example.com/graphql",
            json=mock_data,
            status=200
        )
    
        transport = RequestsHTTPTransport(url="https://api.example.com/graphql")
        client = Client(transport=transport, fetch_schema_from_transport=False)
    
        query = gql("""...""")  # same GetBooks query
        result = client.execute(query, variable_values={"limit": 1})
        assert result["books"][0]["title"] == "Python 101"
    

    Mocking allows you to run fast, deterministic unit tests without hitting the real API.

    Real‑World Use Cases

  • Python Webhook Receiver Api Setup

    In today’s fast‑moving API ecosystem, webhooks have become the go‑to method for real‑time communication between services. Whether you’re integrating Stripe payments, GitHub commits, or a custom IoT sensor, a reliable Python webhook receiver is the backbone that turns incoming HTTP callbacks into actionable data. In this guide we’ll walk through every step needed to set up a production‑ready webhook receiver API with Python, covering everything from routing and security to testing and deployment. By the end, you’ll have a clean, maintainable codebase that can handle high‑volume events while staying secure and SEO‑friendly.

    Why Use a Python Webhook Receiver?

    Python’s readability, extensive library ecosystem, and strong community support make it an ideal choice for building webhook endpoints. Here are a few reasons why developers favor Python for this task:

    • Rapid development: Frameworks like Flask and FastAPI let you spin up an endpoint in minutes.
    • Rich validation tools: Libraries such as pydantic and marshmallow simplify payload verification.
    • Scalable async support: FastAPI’s async capabilities handle thousands of concurrent requests with minimal overhead.
    • Built‑in testing: Python’s unittest and pytest make it easy to write comprehensive webhook tests.

    Choosing the Right Framework

    Two popular frameworks dominate the Python webhook scene:

    Flask

    Flask is lightweight, has a minimal learning curve, and is perfect for simple webhook receivers. It follows the WSGI standard and integrates seamlessly with many extensions.

    FastAPI

    FastAPI is built on Starlette and Pydantic, offering native async support, automatic OpenAPI documentation, and superior performance. If you expect high traffic or need type‑checked request bodies, FastAPI is the better choice.

    Step‑by‑Step Setup with FastAPI

    Below is a complete walkthrough for creating a secure webhook receiver using FastAPI, uvicorn as the ASGI server, and hmac for signature verification.

    1. Project Structure

    webhook_receiver/
    ├── app/
    │   ├── __init__.py
    │   ├── main.py
    │   ├── schemas.py
    │   └── security.py
    ├── tests/
    │   └── test_webhook.py
    ├── requirements.txt
    └── README.md
    

    2. Install Dependencies

    pip install fastapi uvicorn pydantic python-dotenv
    

    3. Define the Payload Schema (app/schemas.py)

    from pydantic import BaseModel, Field
    from typing import List, Optional
    
    class EventData(BaseModel):
        id: str = Field(..., description="Unique identifier of the event")
        type: str = Field(..., description="Event type, e.g., 'payment_intent.succeeded'")
        created: int = Field(..., description="Unix timestamp of when the event was created")
        data: dict = Field(..., description="Custom payload from the source service")
    
    class WebhookPayload(BaseModel):
        object: str = Field(..., description="Root object, usually 'event'")
        id: str = Field(..., description="Event ID")
        events: List[EventData] = Field(..., description="List of individual events")
    

    4. Implement Signature Verification (app/security.py)

    Most webhook providers sign their payloads with a secret key. The following helper validates the HMAC signature sent in the X‑Signature header.

    import hmac
    import hashlib
    from fastapi import HTTPException, Header
    
    def verify_signature(
        payload: bytes,
        signature: str,
        secret: str,
        algorithm: str = "sha256"
    ) -> None:
        """
        Raise HTTPException(400) if the signature is invalid.
        """
        expected = hmac.new(
            key=secret.encode(),
            msg=payload,
            digestmod=getattr(hashlib, algorithm)
        ).hexdigest()
    
        if not hmac.compare_digest(expected, signature):
            raise HTTPException(status_code=400, detail="Invalid webhook signature")
    

    5. Create the FastAPI App (app/main.py)

    import os
    from fastapi import FastAPI, Request, Header, HTTPException
    from fastapi.responses import JSONResponse
    from .schemas import WebhookPayload
    from .security import verify_signature
    from dotenv import load_dotenv
    
    load_dotenv()  # Loads .env file with WEBHOOK_SECRET
    
    app = FastAPI(
        title="Python Webhook Receiver API",
        description="Secure endpoint for handling real‑time webhook events.",
        version="1.0.0"
    )
    
    WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET")
    if not WEBHOOK_SECRET:
        raise RuntimeError("WEBHOOK_SECRET not set in environment variables")
    
    @app.post("/webhook", status_code=200)
    async def receive_webhook(
        request: Request,
        x_signature: str = Header(..., alias="X-Signature")
    ):
        """
        Entry point for all incoming webhook POST requests.
        """
        raw_body = await request.body()
    
        # 1️⃣ Verify the HMAC signature
        verify_signature(payload=raw_body, signature=x_signature, secret=WEBHOOK_SECRET)
    
        # 2️⃣ Parse and validate JSON payload
        try:
            payload = WebhookPayload.parse_raw(raw_body)
        except Exception as exc:
            raise HTTPException(status_code=422, detail=f"Payload validation error: {exc}")
    
        # 3️⃣ Process each event (business logic placeholder)
        for event in payload.events:
            # Example: log event, trigger async task, store in DB, etc.
            print(f"Received event {event.type} with ID {event.id}")
    
        # 4️⃣ Respond quickly to avoid timeouts
        return JSONResponse(content={"status": "received"}, status_code=200)
    

    6. Run the Server Locally

    uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    

    The --reload flag enables hot‑reloading during development. In production, you’ll typically run uvicorn behind a reverse proxy like Nginx or use a process manager such as gunicorn with uvicorn.workers.UvicornWorker.

    Security Best Practices

    Webhooks expose a public endpoint, so securing it is non‑negotiable. Follow these guidelines to keep your receiver safe:

    • Validate signatures: Always verify the HMAC signature before processing payloads.
    • Use HTTPS: Deploy behind TLS to protect data in transit.
    • Rate limit requests: Prevent abuse with tools like slowapi or Nginx limit‑req.
    • Restrict IP ranges: If the provider publishes IP blocks, whitelist them at the firewall level.
    • Log and monitor: Store request metadata (IP, headers, timestamps) for audit trails.

    Testing Your Webhook Receiver

    Automated tests guarantee that signature verification, payload parsing, and business logic work as expected.

    Unit Test Example (tests/test_webhook.py)

    import json
    import hmac
    import hashlib
    from fastapi.testclient import TestClient
    from app.main import app, WEBHOOK_SECRET
    
    client = TestClient(app)
    
    def generate_signature(payload: dict) -> str:
        raw = json.dumps(payload).encode()
        return hmac.new(
            key=WEBHOOK_SECRET.encode(),
            msg=raw,
            digestmod=hashlib.sha256
        ).hexdigest()
    
    def test_valid_webhook():
        payload = {
            "object": "event",
            "id": "evt_123",
            "events": [
                {
                    "id": "ev_001",
                    "type": "payment_intent.succeeded",
                    "created": 1700000000,
                    "data": {"amount": 5000}
                }
            ]
        }
        signature = generate_signature(payload)
        response = client.post(
            "/webhook",
            json=payload,
            headers={"X-Signature": signature}
        )
        assert response.status_code == 200
        assert response.json()["status"] == "received"
    
    def test_invalid_signature():
        payload = {"object": "event", "id": "evt_bad", "events": []}
        response = client.post(
            "/webhook",
            json=payload,
            headers={"X-Signature": "invalidsignature"}
        )
        assert response.status_code == 400
        assert response.json()["detail"] == "Invalid webhook signature"
    

    Deploying to Production

    When you’re ready to go live, consider these deployment steps:

    1. Containerize with Docker: Create a lightweight image based on python:3.12-slim.
    2. Use a Process Manager: Run gunicorn -k uvicorn.workers.UvicornWorker -w 4 app.main:app for multi‑worker concurrency.
    3. Configure a Reverse Proxy: Nginx can terminate TLS, handle static files, and forward requests to the ASGI server.
    4. Set Environment Variables: Store WEBHOOK_SECRET and other secrets in a secure vault (AWS Secrets Manager, HashiCorp Vault, etc.).
    5. Enable Health Checks: Expose a simple /health endpoint to let orchestration tools verify service health.

    Sample Dockerfile

    FROM python:3.12-slim

    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt

    COPY . .

    ENV PORT=8000
    EXPOSE 8000

    CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-

  • Python Seo Audit Automated Tool

    In today’s hyper‑competitive digital landscape, a solid SEO foundation can be the difference between soaring traffic and a stagnant website. Yet, conducting a thorough SEO audit manually is time‑consuming, error‑prone, and often incomplete. That’s where a Python‑powered SEO audit automated tool comes into play—offering speed, consistency, and deep insights at the click of a button. In this guide we’ll explore why automation matters, break down the essential components of a Python audit script, walk through a step‑by‑step build, and share best practices to keep your SEO health in top shape.

    Why Automate SEO Audits?

    Key Benefits of an Automated Approach

    • Speed and Scale: Scan thousands of URLs in minutes rather than hours.
    • Consistency: Eliminate human bias and ensure the same criteria are applied across every page.
    • Actionable Data: Generate structured reports that can be fed directly into issue‑tracking systems.
    • Continuous Monitoring: Schedule recurring audits to catch regressions before they impact rankings.
    • Cost Efficiency: Leverage open‑source Python libraries instead of expensive SaaS platforms.

    Core Components of a Python SEO Audit Tool

    Crawl Engine

    The crawl engine discovers pages, respects robots.txt, and manages request throttling. Popular choices include Scrapy, Requests‑HTML, and Playwright for JavaScript‑heavy sites.

    Data Extraction Layer

    Once a page is fetched, you need to extract SEO‑critical elements such as title tags, meta descriptions, heading hierarchy, image alt attributes, and structured data. BeautifulSoup and lxml excel at parsing HTML, while json‑ld can be extracted with simple regex or the extruct library.

    Analysis Modules

    These modules evaluate the extracted data against SEO best practices:

    • Length checks for titles and meta descriptions.
    • Duplicate content detection using fingerprinting (e.g., hashlib).
    • Broken link identification via HTTP status codes.
    • Page speed insights through the Google PageSpeed API.
    • Schema validation with schema‑org validators.

    Reporting Engine

    Finally, the tool should output results in a format that stakeholders can consume—CSV, JSON, or an interactive HTML dashboard. Libraries like pandas simplify CSV generation, while Jinja2 can render polished HTML reports.

    Building the Tool: A Step‑by‑Step Walkthrough

    1. Set Up Your Python Environment

    python -m venv seo‑audit
    source seo‑audit/bin/activate  # On Windows: .\seo‑audit\Scripts\activate
    pip install requests beautifulsoup4 lxml pandas jinja2 tqdm
    

    2. Create a Simple Crawler

    import requests
    from bs4 import BeautifulSoup
    from urllib.parse import urljoin, urlparse
    from tqdm import tqdm
    
    def crawl(start_url, max_pages=100):
        visited = set()
        queue = [start_url]
        results = []
    
        while queue and len(visited) < max_pages:
            url = queue.pop(0)
            if url in visited:
                continue
            try:
                resp = requests.get(url, timeout=10, headers={'User-Agent': 'SEO‑AuditBot/1.0'})
                if resp.status_code != 200:
                    continue
                soup = BeautifulSoup(resp.text, 'lxml')
                results.append({'url': url, 'html': resp.text})
                visited.add(url)
    
                # Enqueue internal links
                for a in soup.find_all('a', href=True):
                    link = urljoin(url, a['href'])
                    if urlparse(link).netloc == urlparse(start_url).netloc:
                        queue.append(link)
            except Exception as e:
                print(f"Error fetching {url}: {e}")
        return results
    

    3. Extract SEO Elements

    def extract_seo_data(page):
        soup = BeautifulSoup(page['html'], 'lxml')
        title = soup.title.string.strip() if soup.title else ''
        meta_desc = ''
        for meta in soup.find_all('meta'):
            if meta.get('name', '').lower() == 'description':
                meta_desc = meta.get('content', '').strip()
                break
        headings = [h.get_text(strip=True) for h in soup.find_all(['h1','h2','h3'])]
        images = [{'src': img.get('src'), 'alt': img.get('alt', '')} for img in soup.find_all('img')]
        return {
            'url': page['url'],
            'title': title,
            'meta_description': meta_desc,
            'headings': headings,
            'images': images
        }
    

    4. Run Analysis Checks

    def analyze_seo(seo_data):
        issues = []
    
        # Title length
        if not (50 <= len(seo_data['title']) <= 60):
            issues.append('Title length should be 50‑60 characters.')
    
        # Meta description length
        if not (120 <= len(seo_data['meta_description']) <= 160):
            issues.append('Meta description should be 120‑160 characters.')
    
        # H1 presence
        h1_tags = [h for h in seo_data['headings'] if h.startswith('h1')]
        if len(h1_tags) != 1:
            issues.append('Page should have exactly one H1 tag.')
    
        # Image alt attributes
        for img in seo_data['images']:
            if not img['alt']:
                issues.append(f"Image {img['src']} is missing an alt attribute.")
        return issues
    

    5. Generate a Report

    import pandas as pd
    from jinja2 import Environment, FileSystemLoader
    
    def create_report(audit_results, output_path='seo_report.html'):
        df = pd.DataFrame(audit_results)
        df.to_csv('seo_audit.csv', index=False)
    
        env = Environment(loader=FileSystemLoader('.'))
        template = env.get_template('report_template.html')
        html = template.render(rows=audit_results)
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(html)
    

    Popular Python Libraries for SEO Automation

    • Scrapy: Full‑featured crawling framework with built‑in throttling and pipelines.
    • Requests‑HTML: Simple API for rendering JavaScript‑heavy pages.
    • BeautifulSoup & lxml: Fast HTML parsing and element extraction.
    • extruct: Extracts structured data (JSON‑LD, Microdata, RDFa).
    • Google‑API‑Python‑Client: Access PageSpeed Insights, Search Console, and more.
    • pandas: Data manipulation and CSV/Excel export.
    • Jinja2: Template engine for creating clean HTML reports.

    Best Practices for Accurate SEO Audits

    1. Respect Crawl Budget: Honor robots.txt and use polite delays (e.g., time.sleep(1)).
    2. Handle Redirect Chains: Follow 301/302 redirects and flag excessive chains.
    3. Normalize URLs: Strip trailing slashes, sort query parameters, and deduplicate.
    4. Use Canonical Checks: Compare the rel="canonical" tag against the fetched URL.
    5. Validate Structured Data: Run schema against Google’s Structured Data Testing Tool API.
    6. Log Errors: Keep a detailed log file for failed requests to aid debugging.
    7. Version Control: Store your audit scripts in Git to track changes over time.

    Integrating the Audit into CI/CD Pipelines

    Continuous Monitoring with GitHub Actions

    By adding a simple workflow file, you can trigger an SEO audit on every push to main and fail the build if critical issues are detected.

    name: SEO Audit
    on:
    push:
    branches: [ main ]
    jobs:
    audit:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up Python
    uses: actions/setup-python@v4
    with:
    python-version: '3.11'
    - name: Install dependencies
    run: pip install -r requirements.txt
    - name: Run audit
    run: |
    python audit.py https://example.com
    if

  • Python Sitemap Generator Project

    Creating a reliable Python sitemap generator is one of the smartest moves you can make to boost your website’s SEO and improve crawler efficiency. A well‑structured XML sitemap tells search engines exactly which pages to index, how often they change, and how important each page is relative to the rest of your site. In this guide we’ll walk through a complete, production‑ready Python project that automatically crawls a website, builds a standards‑compliant sitemap, and writes it to sitemap.xml. Whether you’re a solo developer, a small business owner, or part of a larger dev team, you’ll get a clear roadmap, ready‑to‑run code snippets, and best‑practice tips that keep your sitemap fresh and search‑engine friendly.

    Why a Custom Python Sitemap Generator?

    Most CMS platforms offer built‑in sitemap tools, but they often lack flexibility, speed, or the ability to integrate with custom workflows. A Python‑based solution gives you:

    • Full control over crawl depth, URL filters, and priority assignment.
    • Easy integration with CI/CD pipelines to regenerate the sitemap on each deployment.
    • Scalability – you can switch from synchronous requests to asynchronous aiohttp with minimal code changes.
    • Transparent, open‑source code that you can audit for security and performance.

    Project Overview

    The project is organized into three core modules:

    1. crawler.py – Handles URL discovery, respects robots.txt, and respects crawl‑delay.
    2. sitemap_builder.py – Converts the collected URLs into a valid XML sitemap.
    3. run.py – Orchestrates the crawl, builds the sitemap, and writes the output file.

    All modules are pure Python, require only a handful of third‑party libraries, and can be extended with plug‑in hooks for authentication, API endpoints, or multi‑domain support.

    Setting Up the Development Environment

    Follow these steps to get the project up and running on any modern operating system.

    # 1. Clone the repository
    git clone https://github.com/yourusername/python-sitemap-generator.git
    cd python-sitemap-generator
    
    # 2. Create a virtual environment
    python -m venv venv
    source venv/bin/activate   # Windows: venv\Scripts\activate
    
    # 3. Install dependencies
    pip install -r requirements.txt
    

    The requirements.txt file typically includes:

    • requests – for HTTP requests (synchronous version).
    • aiohttp – for optional asynchronous crawling.
    • beautifulsoup4 – for parsing HTML and extracting links.
    • lxml – for fast XML generation.

    Core Logic: Crawling the Site

    1. Respecting robots.txt

    Before any page is fetched, the crawler checks the site’s robots.txt file. This ensures you stay within the webmaster’s guidelines and avoid accidental bans.

    import urllib.robotparser as rp
    
    def can_fetch(url, user_agent='*'):
        parser = rp.RobotFileParser()
        parser.set_url(f"{base_url}/robots.txt")
        parser.read()
        return parser.can_fetch(user_agent, url)
    

    2. Extracting Links

    Using BeautifulSoup, the crawler extracts href attributes, normalizes them, and filters out external domains.

    from bs4 import BeautifulSoup
    from urllib.parse import urljoin, urlparse
    
    def extract_links(html, current_url):
        soup = BeautifulSoup(html, 'html.parser')
        links = set()
        for tag in soup.find_all('a', href=True):
            href = urljoin(current_url, tag['href'])
            if urlparse(href).netloc == urlparse(base_url).netloc:
                links.add(href.split('#')[0])  # Remove fragments
        return links
    

    3. Managing Crawl Depth and Queue

    A simple breadth‑first search (BFS) queue guarantees that you never exceed the configured depth.

    from collections import deque
    
    def bfs_crawl(start_url, max_depth=3):
        visited = set()
        queue = deque([(start_url, 0)])
    
        while queue:
            url, depth = queue.popleft()
            if depth > max_depth or url in visited:
                continue
            if not can_fetch(url):
                continue
    
            response = requests.get(url, timeout=10)
            visited.add(url)
    
            if 'text/html' in response.headers.get('Content-Type', ''):
                for link in extract_links(response.text, url):
                    queue.append((link, depth + 1))
        return visited
    

    Generating a Valid XML Sitemap

    The sitemap_builder.py module turns the set of URLs into an XML document that complies with the Sitemap protocol.

    from lxml import etree
    from datetime import datetime
    
    def build_sitemap(urls, changefreq='weekly', priority=0.5):
        urlset = etree.Element('urlset', xmlns="http://www.sitemaps.org/schemas/sitemap/0.9")
        for url in sorted(urls):
            url_el = etree.SubElement(urlset, 'url')
            etree.SubElement(url_el, 'loc').text = url
            etree.SubElement(url_el, 'lastmod').text = datetime.utcnow().date().isoformat()
            etree.SubElement(url_el, 'changefreq').text = changefreq
            etree.SubElement(url_el, 'priority').text = str(priority)
        return etree.tostring(urlset, pretty_print=True, xml_declaration=True, encoding='UTF-8')
    

    Putting It All Together – The Entry Point

    The run.py script ties crawling and sitemap creation into a single, easy‑to‑execute command.

    import argparse
    from crawler import bfs_crawl
    from sitemap_builder import build_sitemap
    
    def main():
        parser = argparse.ArgumentParser(description='Generate an XML sitemap.')
        parser.add_argument('base_url', help='Root URL of the site to crawl')
        parser.add_argument('--depth', type=int, default=3, help='Maximum crawl depth')
        parser.add_argument('--output', default='sitemap.xml', help='Output file name')
        args = parser.parse_args()
    
        urls = bfs_crawl(args.base_url, max_depth=args.depth)
        sitemap_xml = build_sitemap(urls)
    
        with open(args.output, 'wb') as f:
            f.write(sitemap_xml)
        print(f"Sitemap generated with {len(urls)} URLs → {args.output}")
    
    if __name__ == '__main__':
        main()
    

    Advanced Features You Can Add

    Asynchronous Crawling for Speed

    If you need to crawl thousands of pages, replace the synchronous requests calls with aiohttp and an async queue. The core logic stays the same, but you’ll see a 3‑5× speed boost on large sites.

    Custom Priority Logic

    Assign higher priority to pages that contain key keywords, have a lot of inbound links, or belong to a specific URL pattern. Modify build_sitemap to accept a mapping of url → priority.

    Incremental Updates

    Store the last crawl’s URL list in a JSON file. On the next run, compare the new list with the old one and only rewrite sitemap.xml if changes are detected. This reduces unnecessary server load and avoids search‑engine churn.

    Testing and Validation

    Before deploying, run these checks:

    • Unit tests – Verify that extract_links correctly normalizes URLs.
    • Integration test – Crawl a small staging site and assert that the generated XML validates against the official XSD.
    • SEO audit – Use Google Search Console’s “Sitemap” tool to submit the file and watch for parsing errors.

    Deploying the Sitemap Generator

    There are several ways to keep your sitemap up‑to‑date:

    1. CI/CD pipeline – Add a step in GitHub Actions, GitLab CI, or Jenkins that runs python run.py https://example.com --depth 4 after each successful deployment.
    2. Scheduled cron job – On a Linux server, create a cron entry like 0 2 * * * /path/to/venv/bin/python /path/to/run.py https://example.com --output /var/www/html/sitemap.xml to regenerate nightly.
    3. Serverless function – Deploy the script as an AWS Lambda or Google Cloud Function, trigger it via HTTP, and store the result in a CDN bucket.

    SEO Benefits of a Fresh Sitemap

    Search engines treat a well‑maintained sitemap as a trust signal. Here’s what you gain:

    • Faster indexing of new or updated content.
    • Reduced crawl budget waste – bots skip irrelevant pages and focus on high‑value URLs.
    • Better visibility for deep‑linked pages that might otherwise be orphaned.
    • Clear communication of changefreq and priority, helping search engines allocate resources intelligently.

  • Python Web Broken Link Checker Utility

    Imagine a bustling website where every click leads a visitor to a dead‑end, a 404 error, or an irrelevant page. Not only does this damage user experience, but it also hurts SEO rankings, conversion rates, and brand credibility. A Python web broken link checker utility can automatically scan your site, spot those problematic URLs, and give you a clear, actionable report. In this guide we’ll explore why broken links matter, outline the essential features of a robust checker, and walk you through building a lightweight yet powerful tool from scratch—all while keeping SEO best practices front and center.

    Why Broken Links Matter for SEO and User Experience

    Search engines treat broken links as a signal of poor site maintenance. When crawlers encounter dead ends, they may lower the page’s crawl budget, causing fresh content to be indexed slower. Meanwhile, real users who stumble upon a 404 page are more likely to bounce, increasing your bounce rate and decreasing dwell time—both key metrics in Google’s ranking algorithm.

    • Reduced crawl efficiency: Search bots waste time on dead URLs.
    • Negative brand perception: Visitors see a site as outdated or unreliable.
    • Lost revenue: E‑commerce pages with broken product links directly affect sales.

    Regularly scanning your site with a Python‑based utility ensures you stay ahead of these issues, keeping both users and search engines happy.

    Key Features of an Effective Python Link Checker

    A good broken link checker does more than just flag 404 errors. Below are the must‑have capabilities you should look for—or implement—in your own utility.

    • HTTP status verification: Detect 4xx and 5xx responses, redirects, and successful 2xx codes.
    • Support for internal and external links: Crawl your domain while optionally checking outbound references.
    • Depth‑controlled crawling: Limit recursion to avoid infinite loops on large sites.
    • Concurrent requests: Use threading or async I/O to speed up scans.
    • Report generation: Output results in CSV, JSON, or plain‑text formats for easy analysis.
    • Exclusion patterns: Skip URLs that match specific regexes (e.g., admin panels, query strings).

    Building the Utility Step‑by‑Step

    Let’s create a simple, extensible broken link checker using Python 3.12+, requests for HTTP calls, and beautifulsoup4 for HTML parsing. The code is deliberately modular so you can swap in aiohttp for async support later.

    1. Set Up the Environment

    python -m venv venv
    source venv/bin/activate  # Windows: venv\Scripts\activate
    pip install requests beautifulsoup4 tqdm
    

    2. Define Helper Functions

    import re
    import sys
    import csv
    from urllib.parse import urljoin, urlparse
    
    import requests
    from bs4 import BeautifulSoup
    from tqdm import tqdm
    
    # ---------- URL Normalization ----------
    def is_valid_url(url: str) -> bool:
        """Return True if the URL has a proper scheme and netloc."""
        parsed = urlparse(url)
        return bool(parsed.scheme) and bool(parsed.netloc)
    
    # ---------- HTTP Status Check ----------
    def fetch_status(url: str, timeout: int = 10) -> int:
        """Perform a HEAD request; fall back to GET if HEAD is not allowed."""
        try:
            response = requests.head(url, allow_redirects=True, timeout=timeout)
            if response.status_code == 405:  # Method Not Allowed
                response = requests.get(url, stream=True, timeout=timeout)
            return response.status_code
        except requests.RequestException:
            return 0  # Network error or timeout
    

    3. Crawl a Page and Extract Links

    def extract_links(html: str, base_url: str) -> set:
        """Parse HTML and return a set of absolute URLs found on the page."""
        soup = BeautifulSoup(html, "html.parser")
        links = set()
        for tag in soup.find_all("a", href=True):
            href = tag["href"].strip()
            # Skip mailto:, tel:, javascript:, #
            if href.startswith(("mailto:", "tel:", "javascript:", "#")):
                continue
            absolute = urljoin(base_url, href)
            if is_valid_url(absolute):
                links.add(absolute)
        return links
    

    4. Core Crawling Logic with Depth Control

    def crawl(start_url: str, max_depth: int = 2, exclude: list = None):
        if exclude is None:
            exclude = []
    
        visited = set()
        broken = []
        queue = [(start_url, 0)]
    
        while queue:
            current_url, depth = queue.pop(0)
            if current_url in visited or depth > max_depth:
                continue
    
            visited.add(current_url)
            status = fetch_status(current_url)
    
            if status == 0 or status >= 400:
                broken.append((current_url, status))
                continue
    
            # Only parse HTML pages (skip PDFs, images, etc.)
            if "text/html" not in requests.head(current_url, allow_redirects=True).headers.get("Content-Type", ""):
                continue
    
            try:
                page = requests.get(current_url, timeout=10).text
            except requests.RequestException:
                broken.append((current_url, 0))
                continue
    
            links = extract_links(page, current_url)
            for link in links:
                # Apply exclusion patterns
                if any(re.search(pattern, link) for pattern in exclude):
                    continue
                # Only follow internal links (same netloc)
                if urlparse(link).netloc == urlparse(start_url).netloc:
                    queue.append((link, depth + 1))
    
        return broken
    

    5. Generate a CSV Report

    def write_report(broken_links: list, filename: str = "broken_links_report.csv"):
        with open(filename, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["URL", "HTTP Status"])
            for url, status in broken_links:
                writer.writerow([url, status])
        print(f"Report saved to {filename}")
    

    6. Glue It All Together

    if __name__ == "__main__":
        if len(sys.argv) < 2:
            print("Usage: python link_checker.py  [max_depth]")
            sys.exit(1)
    
        start = sys.argv[1]
        depth = int(sys.argv[2]) if len(sys.argv) > 2 else 2
        exclude_patterns = [r"/admin", r"\?session="]  # Example exclusions
    
        print(f"Scanning {start} up to depth {depth}…")
        broken_links = crawl(start, max_depth=depth, exclude=exclude_patterns)
    
        if broken_links:
            write_report(broken_links)
            print(f"Found {len(broken_links)} broken link(s).")
        else:
            print("✅ No broken links found!")
    

    Save the script as link_checker.py and run it from the command line. The utility will crawl your site, respect the depth you set, skip excluded patterns, and produce a tidy CSV file you can import into Google Sheets or your favorite analytics tool.

    Testing and Running the Script

    Before you schedule regular scans, give the tool a quick sanity check on a small test site. Follow these steps:

    1. Run a dry‑run: Use --max-depth 0 to only validate the homepage.
    2. Validate output: Open the generated CSV and verify that known 404 pages appear correctly.
    3. Check performance: For larger sites, enable tqdm progress bars or switch to an async version to reduce scan time.

    Once you’re confident, integrate the script into a CI/CD pipeline or a cron job. For example, a daily cron entry could look like:

    0 2 * * * /path/to/venv/bin/python /path/to/link_checker.py https://example.com 3 >> /var/log/link_checker.log 2>&1
    

    This runs the checker at 2 AM every day, scanning three levels deep and appending results to a log file for later review.

    Advanced Tips & Best Practices

    To turn a simple script into an enterprise‑grade solution, consider the following enhancements.

    Parallel Requests with ThreadPoolExecutor

    from concurrent.futures import ThreadPoolExecutor, as_completed
    
    def fetch_status_parallel(urls: list, max_workers: int = 10):
        results = {}
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            future_to_url = {executor.submit(fetch_status, u): u for u in urls}
            for future in as_completed(future_to_url):
                url = future_to_url[future]
                try:
                    results[url] = future.result()
                except Exception:
                    results[url] = 0
        return results
    

    Replace the sequential fetch_status calls in crawl() with this parallel version to cut scan times dramatically on sites with hundreds of pages.

    Integrate with Google Search Console API

    Google Search Console already reports crawl errors. By pulling that data via the API and merging it with your own scanner’s output, you get a comprehensive view of both internal and external link health.

    Store Results in a Database

    Instead of a

  • Python Rss Feed Parser And Aggregator

    In the fast‑paced world of online content, staying updated with the latest articles, podcasts, and news headlines is essential for developers, marketers, and curious readers alike. A Python RSS feed parser and aggregator gives you the power to automatically fetch, parse, and display updates from multiple sources in a single, customizable dashboard. In this guide we’ll explore why RSS remains relevant, walk through the most popular Python libraries, and build a simple yet robust aggregator that can be expanded for any project—from personal hobby sites to enterprise‑grade monitoring tools.

    Why Build Your Own RSS Aggregator with Python?

    Even though social media and email newsletters dominate content distribution, RSS (Really Simple Syndication) still offers several advantages that make it a compelling choice for developers:

    • Standardized format: XML‑based feeds are universally supported, ensuring you can pull data from thousands of sites without dealing with proprietary APIs.
    • Low bandwidth: Feeds contain only the essential metadata (title, link, summary), making them lightweight compared to full‑page scrapes.
    • Control and privacy: You decide how often to poll, what to store, and how to present the information, keeping user data out of third‑party platforms.
    • Automation friendly: Python’s extensive ecosystem makes it easy to schedule regular updates, filter content, and integrate with databases or web frameworks.

    Choosing the Right Python Library

    Several libraries simplify RSS handling, each with its own strengths. Below is a quick comparison to help you decide which tool fits your project.

    feedparser

    The de‑facto standard for parsing RSS and Atom feeds. It gracefully handles malformed XML, supports a wide range of feed versions, and returns a Pythonic dictionary‑like object.

    import feedparser
    
    feed = feedparser.parse('https://example.com/rss')
    for entry in feed.entries[:5]:
        print(entry.title, entry.link)
    

    requests + xml.etree.ElementTree

    For developers who need fine‑grained control over HTTP headers, authentication, or custom XML processing, combining requests with the built‑in xml module offers flexibility without adding extra dependencies.

    import requests
    import xml.etree.ElementTree as ET
    
    response = requests.get('https://example.com/rss')
    root = ET.fromstring(response.content)
    
    for item in root.findall('.//item'):
        title = item.find('title').text
        link = item.find('link').text
        print(title, link)
    

    aiohttp + async‑rss

    If you plan to fetch dozens or hundreds of feeds concurrently, an asynchronous approach reduces latency dramatically. The async‑rss package works hand‑in‑hand with aiohttp to provide non‑blocking parsing.

    import asyncio
    import aiohttp
    from async_rss import parse
    
    async def fetch(url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                text = await resp.text()
                return await parse(text)
    
    async def main(urls):
        tasks = [fetch(u) for u in urls]
        results = await asyncio.gather(*tasks)
        for feed in results:
            for entry in feed.entries[:3]:
                print(entry.title)
    
    urls = ['https://site1.com/rss', 'https://site2.com/rss']
    asyncio.run(main(urls))
    

    Step‑by‑Step: Building a Minimal RSS Aggregator

    Let’s assemble a lightweight aggregator that reads a list of feed URLs, stores the latest entries in a SQLite database, and serves them through a Flask web page. This example balances readability with production‑ready practices such as error handling and scheduling.

    1. Project Structure

    • aggregator/ – Root folder
    • aggregator/app.py – Flask web server
    • aggregator/feeds.py – Feed fetching & parsing logic
    • aggregator/models.py – SQLAlchemy models
    • aggregator/config.py – List of RSS URLs and settings

    2. Define the Database Model

    # models.py
    from sqlalchemy import Column, Integer, String, Text, DateTime, create_engine
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import sessionmaker
    import datetime
    
    Base = declarative_base()
    
    class Entry(Base):
        __tablename__ = 'entries'
        id = Column(Integer, primary_key=True)
        title = Column(String(255), nullable=False)
        link = Column(String(500), nullable=False, unique=True)
        summary = Column(Text)
        published = Column(DateTime, default=datetime.datetime.utcnow)
    
    engine = create_engine('sqlite:///rss.db')
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    

    3. Fetch and Store New Items

    # feeds.py
    import feedparser
    from models import Session, Entry
    from datetime import datetime
    
    FEED_URLS = [
        'https://realpython.com/atom.xml',
        'https://planetpython.org/rss20.xml',
        # add more URLs as needed
    ]
    
    def update_feeds():
        session = Session()
        for url in FEED_URLS:
            try:
                feed = feedparser.parse(url)
                for e in feed.entries:
                    # Skip if entry already exists
                    if session.query(Entry).filter_by(link=e.link).first():
                        continue
                    entry = Entry(
                        title=e.title,
                        link=e.link,
                        summary=e.get('summary', ''),
                        published=datetime(*e.published_parsed[:6]) if e.get('published_parsed') else datetime.utcnow()
                    )
                    session.add(entry)
            except Exception as exc:
                print(f'Error processing {url}: {exc}')
        session.commit()
        session.close()
    

    4. Schedule Regular Updates

    Using APScheduler we can run update_feeds() every 15 minutes without leaving the Flask process.

    # app.py (partial)
    from flask import Flask, render_template_string
    from apscheduler.schedulers.background import BackgroundScheduler
    from feeds import update_feeds
    from models import Session, Entry
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        session = Session()
        entries = session.query(Entry).order_by(Entry.published.desc()).limit(30).all()
        session.close()
        html = '''
        

    Latest News

      {% for e in entries %}
    • {{ e.title }} ({{ e.published.strftime('%Y-%m-%d') }})
    • {% endfor %}
    ''' return render_template_string(html, entries=entries) if __name__ == '__main__': scheduler = BackgroundScheduler() scheduler.add_job(func=update_feeds, trigger='interval', minutes=15) scheduler.start() update_feeds() # initial load app.run(debug=True)

    5. Running the Aggregator

    1. Install dependencies: pip install flask feedparser sqlalchemy apscheduler
    2. Run python app.py and open http://127.0.0.1:5000/ in a browser.
    3. Watch the list refresh automatically as the scheduler pulls new items.

    Advanced Features You Can Add Next

    Once the basic pipeline is stable, consider extending the aggregator with one or more of the following enhancements:

    • Full‑text extraction: Use BeautifulSoup to pull the article body from the linked page for richer summaries.
    • Keyword filtering: Store user‑defined tags and only display entries that match specific terms.
    • Push notifications: Integrate with services like Pushover or Slack to alert teams when high‑priority items appear.
    • Docker containerization: Package the app with a Dockerfile for easy deployment on cloud platforms.
    • GraphQL or REST API: Expose the aggregated feed as an API for other applications to consume.

    Best Practices for a Production‑Ready RSS Aggregator

    Building a reliable aggregator goes beyond code snippets. Follow these guidelines to ensure stability, security, and scalability:

    • Respect robots.txt and pubSubHubbub: Some publishers provide hub endpoints for real‑time updates; using them reduces unnecessary polling.
    • Implement exponential back‑off: When a feed returns HTTP 429 (Too Many Requests), delay subsequent attempts to avoid being blocked.
    • Validate and sanitize inputs: Never trust feed content blindly; escape HTML before rendering to prevent XSS attacks.
    • Monitor health: Log fetch successes/failures and set up alerts for prolonged downtime of critical feeds.
    • Cache responses: Store raw XML for a short period (e.g., 5 minutes) to reduce network overhead when multiple workers request the same feed.

    SEO Benefits of Publishing Your Own Aggregated Feed

    When you expose a clean, well‑structured RSS or JSON endpoint from your aggregator, you create additional SEO value:

    • Search engines can crawl your consolidated feed, discovering fresh content faster.
    • Internal linking from aggregated summaries to original articles improves link equity and user dwell time.
    • Structured data (e.g., Article schema) can be added to the rendered page, helping Google understand the content hierarchy.

    Conclusion

    A Python