microdot
repository·main·Indexed 24 days ago
https://github.com/miguelgrinberg/microdotA 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.
What's inside microdot
- Microdot is a minimalistic Python web framework inspired by Flask. It is designed to be extremely small, making it suitable for systems with limited resources like microcontrollers. It supports both standard Python (CPython) and MicroPython.
Overview of utemplate
mainutemplateis 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)}}.
Handle client disconnections in WebSocket handlers
mainWhen 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...exceptblock to catchasyncio.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!')Configure handler scope when mounting sub-applications
mainWhen 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=Trueargument in themount()method.Share data across handlers using the 'g' object
mainThe
request.gobject allows you to store data during the lifetime of a single request so it can be shared betweenbefore_requesthandlers, route functions,after_requesthandlers, 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}!'Compare uTemplate and Jinja template engines
mainMicrodot provides two template engine extensions depending on your environment and requirements:
Feature uTemplate Jinja Compatibility CPython & MicroPython CPython only Dependency utemplateJinja2Async Support Supported Supported (via enable_async=True)Default Directory templatestemplatesChoose a utemplate Loader
mainutemplateprovides different loader classes depending on your environment and needs:utemplate.compiled.Loader: Use this for production. It loads templates that have already been compiled into Python modules.utemplate.source.Loader: Compiles templates on the fly if they are not already compiled. Useful for simpler workflows.utemplate.recompile.Loader: The most convenient for development. It automatically recompiles a template module if the source.tplfile changes. Note that this performs extra processing and is not recommended for finished/deployed applications.
Construct responses using return values
mainMicrodot 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:
- Body only: Returns a single value (e.g., a string). Defaults to status
200andContent-Type: text/plain. - Body and Status: Returns a tuple of
(body, status_code). Overrides the default200status. - Body, Status, and Headers: Returns a tuple of
(body, status_code, headers_dict). Use this to set custom headers likeContent-Type. - Body and Headers: Returns a tuple of
(body, headers_dict). Uses the default200status. - Status only: Returns a single integer (e.g.,
204) if no body is needed. - Response Object: Returns a single
microdot.Responseobject 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- Body only: Returns a single value (e.g., a string). Defaults to status
Template Naming and Compilation Conventions
mainTo ensure proper loading and importing, follow these conventions:
- Extension: Use
.tplfor template files (e.g.,example.tpl). - Compiled Filenames: When a template is compiled, dots (
.) are replaced with underscores (_) and.pyis appended. For example,example.tplbecomesexample_tpl.py, which can be imported viaimport 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}} %}.
- For static includes, use the full name with extension:
- Extension: Use
Stream responses using generators
mainTo return a response generated in chunks, return a Python generator.
Runtime differences:
- CPython: Supports
defgenerators,async defgenerators, and class-based generators. - MicroPython: Does not support asynchronous generator functions (
async def). Use standarddefgenerators 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()- CPython: Supports
Handle synchronous def handlers in Microdot
mainMicrodot 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
defhandler will block the entire asynchronous loop, making the application unresponsive.
Recommendation: Always prefer
async defhandlers, especially when targeting MicroPython.Enable CORS support in Microdot
mainTo enable Cross-Origin Resource Sharing (CORS), instantiate the
CORSclass frommicrodot.corsand pass yourMicrodotapplication 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.pyfile must be present in yourmicrodotsource 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)