microdot

repository·main·Indexed 24 days ago

https://github.com/miguelgrinberg/microdot

A minimalistic web framework for Python and MicroPython, inspired by Flask. Designed to be small enough for microcontrollers while remaining compatible with standard CPython environments. Version 2.6.2 includes features such as CSRF protection, static file serving, TLS support, and integration with utemplate, a memory-efficient template engine.

Tokens
24.9K
Snippets
75
Records
145
Agent score
76%

What's inside microdot

  1. Overview of utemplate

    main

    utemplate is a lightweight, memory-efficient template engine for Python. It is specifically optimized for use with Pycopy, but is fully compatible with CPython and other compliant Python implementations.

    Key Characteristics

    • Memory Efficiency: It compiles templates into Python generator functions. This allows for minimal memory usage during substitution (starting from mere hundreds of bytes on Pycopy).
    • Syntax: The syntax is inspired by Django/Jinja2 (e.g., {% if %}, {{var}}).
    • Function Calls: Instead of using filter syntax like {{var|filter}}, you call functions directly: {{filter(var)}}.
  2. Handle client disconnections in WebSocket handlers

    main

    When a client closes the WebSocket connection, the route handler function is cancelled by the event loop. To perform cleanup tasks (such as logging or releasing resources) when a disconnection occurs, wrap your handler logic in a try...except block to catch asyncio.CancelledError.

    import asyncio
    from microdot.websocket import with_websocket
    
    @app.route('/echo')
    @with_websocket
    async def echo(request, ws):
        try:
            while True:
                message = await ws.receive()
                await ws.send(message)
        except asyncio.CancelledError:
            print('Client disconnected!')
  3. Configure handler scope when mounting sub-applications

    main

    When you mount a sub-application, its before-request, after-request, and error handlers are copied to the main application. By default, these handlers will apply to the entire application.

    To restrict these handlers so they only apply to the specific sub-application they were defined in, use the local=True argument in the mount() method.

  4. Share data across handlers using the 'g' object

    main

    The request.g object allows you to store data during the lifetime of a single request so it can be shared between before_request handlers, route functions, after_request handlers, and error handlers.

    @app.before_request
    async def authorize(request):
        username = authenticate_user(request)
        if not username:
            return 'Unauthorized', 401
        request.g.username = username
    
    @app.get('/')
    async def index(request):
        return f'Hello, {request.g.username}!'
  5. Choose a utemplate Loader

    main

    utemplate provides different loader classes depending on your environment and needs:

    1. utemplate.compiled.Loader: Use this for production. It loads templates that have already been compiled into Python modules.
    2. utemplate.source.Loader: Compiles templates on the fly if they are not already compiled. Useful for simpler workflows.
    3. utemplate.recompile.Loader: The most convenient for development. It automatically recompiles a template module if the source .tpl file changes. Note that this performs extra processing and is not recommended for finished/deployed applications.
  6. Construct responses using return values

    main

    Microdot route functions can return one, two, or three values to construct a response. The structure determines how the status code, body, and headers are handled:

    1. Body only: Returns a single value (e.g., a string). Defaults to status 200 and Content-Type: text/plain.
    2. Body and Status: Returns a tuple of (body, status_code). Overrides the default 200 status.
    3. Body, Status, and Headers: Returns a tuple of (body, status_code, headers_dict). Use this to set custom headers like Content-Type.
    4. Body and Headers: Returns a tuple of (body, headers_dict). Uses the default 200 status.
    5. Status only: Returns a single integer (e.g., 204) if no body is needed.
    6. Response Object: Returns a single microdot.Response object containing all response details.
    # Body only (200 OK, text/plain)
    @app.get('/')
    async def index(request):
        return 'Hello, World!'
    
    # Body and Status (202 Accepted)
    @app.get('/')
    async def index(request):
        return 'Hello, World!', 202
    
    # Body, Status, and Headers (HTML response)
    @app.get('/')
    async def index(request):
        return '<h1>Hello, World!</h1>', 202, {'Content-Type': 'text/html'}
    
    # Body and Headers (200 OK, HTML response)
    @app.get('/')
    async def index(request):
        return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}
    
    # Status only (204 No Content)
    @app.get('/')
    async def index(request):
        return 204
  7. Template Naming and Compilation Conventions

    main

    To ensure proper loading and importing, follow these conventions:

    • Extension: Use .tpl for template files (e.g., example.tpl).
    • Compiled Filenames: When a template is compiled, dots (.) are replaced with underscores (_) and .py is appended. For example, example.tpl becomes example_tpl.py, which can be imported via import example_tpl.
    • Include Statements:
      • For static includes, use the full name with extension: {% include "example.tpl" %}.
      • For dynamic includes, the variable must contain the full name: {% set name = "example.tpl" %} followed by {% include {{name}} %}.
  8. Stream responses using generators

    main

    To return a response generated in chunks, return a Python generator.

    Runtime differences:

    • CPython: Supports def generators, async def generators, and class-based generators.
    • MicroPython: Does not support asynchronous generator functions (async def). Use standard def generators or asynchronous class-based generators.
    @app.get('/fibonacci')
    async def fibonacci(request):
        async def generate_fibonacci():
            a, b = 0, 1
            while a < 100:
                yield str(a) + '\n'
                a, b = b, a + b
    
        return generate_fibonacci()
  9. Handle synchronous def handlers in Microdot

    main

    Microdot supports standard def (synchronous) route handlers, but their behavior depends on your Python runtime:

    • CPython: Synchronous handlers are executed in a thread executor (using a thread pool). This prevents them from blocking the main asynchronous event loop, though they are still subject to the Global Interpreter Lock (GIL).
    • MicroPython: Synchronous handlers run in the main thread. Because many microcontrollers have limited or no threading support, a long-running or blocking def handler will block the entire asynchronous loop, making the application unresponsive.

    Recommendation: Always prefer async def handlers, especially when targeting MicroPython.

  10. Enable CORS support in Microdot

    main

    To enable Cross-Origin Resource Sharing (CORS), instantiate the CORS class from microdot.cors and pass your Microdot application instance to it. You can configure which origins are allowed and whether credentials (like cookies or authorization headers) are permitted using the constructor options.

    Compatibility:

    • CPython
    • MicroPython

    Requirements:

    • The cors.py file must be present in your microdot source directory.
    • No external dependencies are required.
    from microdot import Microdot
    from microdot.cors import CORS
    
    app = Microdot()
    cors = CORS(app, allowed_origins=['https://example.com'],
                allow_credentials=True)