API#
This section documents all public interfaces of Muffin.
Application#
- class muffin.Application(*cfg_mods, **options)#
The Muffin Application.
- Parameters:
cfg_mods (str | ModuleType)
- import_submodules(*submodules, silent=False, **kwargs)#
Automatically import submodules.
# Somewhere in package "__init__.py" file # import all submodules app.import_submodules() # import only specific submodules (in specified order) app.import_submodules('submodule1', 'submodule2') # ignore import errors app.import_submodules(silent=True)
- run_after_response(*tasks)#
Schedule awaitables to run after the response is sent.
The response is sent first, then the tasks run to completion before the request cycle ends. A task that raises is logged and does not affect the already-sent response.
Call it only from a request handler. Scheduling from a background task has no effect: “after the response” has already passed by then, so the task is never awaited and its coroutine raises a RuntimeWarning when it is garbage-collected.
from muffin import Application app = Application() async def send_email(email, message): # send email here ... @app.route('/send') async def send(request): app.run_after_response(send_email('user@email.com', 'Hello!')) return 'OK'
- Parameters:
tasks (Awaitable)
- route(*paths, methods=None, hosts=None, **opts)#
Register a route handler.
- Parameters:
paths (TPath) – One or more URL paths to match
methods (Optional[TMethods]) – HTTP methods to match (GET, POST, etc.)
hosts (Optional[THostsArg]) – Restrict the route to the given host(s), compared case-insensitively and ignoring the port. Requires
use_hosts=Trueon the applicationopts (Any) – Additional options for the route
- Returns:
Decorator function
- Return type:
Callable[[TRouteHandler], TRouteHandler]
- on_startup(fn)#
Register a startup event handler.
- Parameters:
fn (Callable) – The function to call on startup
- Return type:
None
- on_shutdown(fn)#
Register a shutdown event handler.
- Parameters:
fn (Callable) – The function to call on shutdown
- Return type:
None
- on_error(etype)#
Register a custom exception handler for a given exception type.
- Parameters:
etype (type[BaseException]) – The exception type to handle
- Returns:
A decorator to register the handler
- Return type:
Callable
- Example:
>>> @app.on_error(TimeoutError) ... async def timeout_handler(request, error): ... return 'Timeout occurred'
- middleware(md, *, insert_first=False)#
Register a middleware for the application.
Middleware can be a coroutine (request/response) or a regular callable (lifespan).
- Parameters:
md (TCallable) – The middleware function or class
insert_first (bool) – If True, insert as the first middleware
- Returns:
The registered middleware
- Return type:
TCallable
- Example:
>>> @app.middleware ... async def log_requests(handler, request): ... print(f"Request: {request.method} {request.url}") ... return await handler(request)
Middleware#
A coroutine middleware receives the app callable and the raw ASGI arguments:
from muffin import Application, ResponseHTML
app = Application()
@app.middleware
async def simple_md(app, request, receive, send):
try:
response = await app(request, receive, send)
response.headers["x-simple-md"] = "passed"
return response
except RuntimeError:
return ResponseHTML("Middleware Exception")
Anything else passed to app.middleware() is treated as ASGI middleware and
bound to the lifespan, which is what most third-party ASGI packages expect:
from sentry_asgi import SentryMiddleware
app.middleware(SentryMiddleware)
See Usage for the request/response details.
Plugins#
- class muffin.plugins.BasePlugin(app=None, **options)#
Base class for Muffin plugins.
- Parameters:
app (Application | None)
- setup(app, *, name=None, **options)#
Bind app and update the plugin’s configuration.
Precedence (lowest to highest): plugin defaults, application config, options, environment variables.
- Parameters:
app (Application)
name (str | None)
- Return type:
- async startup()#
Startup the plugin.
- async shutdown()#
Shutdown the plugin.
- async restart()#
Restart the plugin.
- conftest()#
Return a context manager for pytest conftest.py.
The plugin contract is described in Plugins.
Class-Based Handlers#
- class muffin.Handler(request, **opts)#
Class-based view pattern for handling HTTP method dispatching.
Methods must be set explicitly (
methods) or inherited from a parent class. There is no automatic detection: definingasync def getdoes not registerGETon its own. A handler withoutmethodsreaches every request, and methods it does not implement end in a logged 500 instead of405, so declare them.@app.route('/hello', '/hello/{name}') class HelloHandler(Handler): methods = ['get', 'post'] async def get(self, request): name = request.path_params.get('name') or 'all' return f"GET: Hello {name}" async def post(self, request): name = request.path_params.get('name') or 'all' return f"POST: Hello {name}" @Handler.route('/hello/custom') async def custom(self, request): return 'Custom HELLO' # ... async def test_my_endpoint(client): response = await client.get('/hello') assert await response.text() == 'GET: Hello all' response = await client.get('/hello/john') assert await response.text() == 'GET: Hello john' response = await client.post('/hello') assert await response.text() == 'POST: Hello all' response = await client.get('/hello/custom') assert await response.text() == 'Custom HELLO' response = await client.delete('/hello') assert response.status_code == 405
- Parameters:
request (Request)
Request#
- class muffin.Request(scope, receive, send)#
Provides a convenient, high-level interface for incoming HTTP requests.
- Parameters:
scope (TASGIScope) – HTTP ASGI Scope
receive (TASGIReceive) – an asynchronous callable which lets the application receive event messages from the client
send (TASGISend) – an asynchronous callable which lets the application send event messages to the client
- scope#
- receive#
- send#
- property url: URL#
A lazy property that parses the current URL and returns
yarl.URLobject.request = Request(scope) assert str(request.url) == '... the full http URL ..' assert request.url.scheme assert request.url.host assert request.url.query is not None assert request.url.query_string is not None
See
yarldocumentation for further reference.
- property headers: CIMultiDict#
A lazy property that parses the current scope’s headers, decodes them as strings and returns case-insensitive multi-dict
multidict.CIMultiDict.request = Request(scope) assert request.headers['content-type'] assert request.headers['authorization']
See
multidictdocumentation for futher reference.
- property cookies: dict[str, str]#
A lazy property that parses the current scope’s cookies and returns a dictionary.
request = Request(scope) ses = request.cookies.get('session')
- property media: Mapping[str, str]#
A lazy property that parses the request’s
Content-Typeheader into a mapping of media options, with the media type under thecontent_typekey (e.g.{'content_type': 'text/html', 'charset': 'utf-8'}).
- property query: MultiDictProxy[str]#
A lazy property that parse the current query string and returns it as a
multidict.MultiDict.
- async stream()#
Stream the request’s body.
The method provides byte chunks without storing the entire body to memory. Any subsequent calls to
body(),form(),json()ordata()will raise an error.Warning
You can only read stream once. Second call raises an error. Save a readed stream into a variable if you need.
- Return type:
AsyncGenerator[bytes, None]
- async body()#
Read and return the request’s body as bytes.
body = await request.body()
- Return type:
- async text()#
Read and return the request’s body as a string.
text = await request.text()
- Return type:
- async json()#
Read and return the request’s body as a JSON.
json = await request.json()
- async form(max_size=10485760, upload_to=None)#
Read and return the request’s form data.
- Parameters:
max_size (int) – Maximum size of the form data in bytes
upload_to (Optional[UploadHandler]) – Callable to handle file uploads
- Returns:
Form data as MultiDict
- Return type:
MultiDict
- Raises:
ASGIRequestEntityTooLargeError – If the form data exceeds
max_size.Appturns this into an HTTP 413 response.
formdata = await request.form()
- async data(*, raise_errors=False)#
Read and return the request’s data based on content type.
- Parameters:
raise_errors (bool) – Raise an error if the given data is invalid.
- Returns:
Request data in appropriate format
- Return type:
- Raises:
ASGIDecodeError – If data cannot be decoded and raise_errors is True
data = await request.data()
If raise_errors is false (by default) and the given data is invalid (ex. invalid json) the request’s body would be returned.
Returns data from
json()for application/json,form()for application/x-www-form-urlencoded, multipart/form-data andtext()otherwise.
A request carries the parsed URL, headers, cookies and path parameters:
@app.route('/hello/{name}')
async def hello(request):
return f"{request.method} {request.url.path} {request.path_params['name']}"
The body is read once, with one of the awaitable parsers (stream, body,
text, json, form, data); Usage covers which to use.
Responses#
- class muffin.Response(content, *, status_code=None, content_type=None, headers=None, cookies=None)#
Base class for creating HTTP responses.
- Parameters:
- headers: MultiDict#
Multidict of response’s headers
- cookies: SimpleCookie#
Set/Update cookies
response.cookies[name] = value
str– set a cookie’s valueresponse.cookies[name][‘path’] = value
str– set a cookie’s pathresponse.cookies[name][‘expires’] = value
int– set a cookie’s expireresponse.cookies[name][‘domain’] = value
str– set a cookie’s domainresponse.cookies[name][‘max-age’] = value
int– set a cookie’s max-ageresponse.cookies[name][‘secure’] = value
bool– is the cookie should only be sent if request is SSLresponse.cookies[name][‘httponly’] = value
bool– is the cookie should be available through HTTP request only (not from JS)response.cookies[name][‘samesite’] = value
str– set a cookie’s strategy (‘lax’|’strict’|’none’)
Basic response example:
from muffin import Response
@app.route('/hello')
async def hello(request):
return Response('Hello, world!', content_type='text/plain')
Setting headers:
@app.route('/example')
async def example(request):
response = Response('OK')
response.headers["X-Version"] = "42"
return response
Setting cookies:
@app.route('/example')
async def example(request):
response = Response('OK')
response.cookies["session"] = "xyz"
response.cookies["session"]["path"] = "/"
return response
ResponseText#
- class muffin.ResponseText(content, *, status_code=None, content_type=None, headers=None, cookies=None)#
Returns plain text responses (text/plain).
- Parameters:
content (TContent)
status_code (int)
content_type (str | None)
headers (MultiDict)
cookies (SimpleCookie)
@app.route('/example')
async def example(request):
return ResponseText('Hello, world!')
ResponseHTML#
- class muffin.ResponseHTML(content, *, status_code=None, content_type=None, headers=None, cookies=None)#
Returns HTML responses (text/html).
- Parameters:
content (TContent)
status_code (int)
content_type (str | None)
headers (MultiDict)
cookies (SimpleCookie)
@app.route('/example')
async def example(request):
return ResponseHTML('<h1>Hello, world!</h1>')
Note
Returning a string/bytes will automatically produce an HTML response.
ResponseJSON#
- class muffin.ResponseJSON(content, *, status_code=None, content_type=None, headers=None, cookies=None)#
Returns JSON responses (application/json).
The class optionally supports orjson for faster JSON handling. Install it to use instead of the standard library.
- Parameters:
content (TContent)
status_code (int)
content_type (str | None)
headers (MultiDict)
cookies (SimpleCookie)
@app.route('/example')
async def example(request):
return ResponseJSON({'hello': 'world'})
Note
Returning dicts, lists, or booleans produces a JSON response.
ResponseRedirect#
- class muffin.ResponseRedirect(url, status_code=None, **kwargs)#
Creates HTTP redirects. Uses a 307 status code by default.
@app.route('/example')
async def example(request):
return ResponseRedirect('/login')
Alternatively, raise as an exception:
@app.route('/example')
async def example(request):
if not request.cookies.get('session'):
raise ResponseRedirect('/login')
return 'OK'
ResponseError#
- class muffin.ResponseError(message=None, status_code=None, **kwargs)#
Helper for returning HTTP errors. Uses a 500 status code by default.
- Parameters:
You able to use
http.HTTPStatusproperties with the ResponseError classresponse = ResponseError.BAD_REQUEST('invalid data') response = ResponseError.NOT_FOUND() response = ResponseError.BAD_GATEWAY() # and etc
Raise HTTP errors:
@app.route('/example')
async def example(request):
data = await request.data()
if not data:
raise ResponseError('Invalid request data', 400)
return 'OK'
Supports http.HTTPStatus shortcuts:
response = ResponseError.BAD_REQUEST('invalid data')
response = ResponseError.NOT_FOUND()
response = ResponseError.BAD_GATEWAY()
ResponseStream#
- class muffin.ResponseStream(stream, **kwargs)#
Streams response body as chunks.
- Parameters:
stream (AsyncGenerator) – An async generator to stream the response’s body
from muffin import ResponseStream
async def stream_response():
for i in range(10):
await sleep(1)
yield f"chunk {i}"
@app.route('/stream')
async def stream(request):
return ResponseStream(stream_response())
ResponseSSE#
- class muffin.ResponseSSE(stream, **kwargs)#
Streams Server-Sent Events (SSE).
- Parameters:
stream (AsyncGenerator) – An async generator to stream the events
from muffin import ResponseSSE
async def sse_response():
while True:
await sleep(1)
yield {"event": "ping", "data": "pong"}
@app.route('/sse')
async def sse(request):
return ResponseSSE(sse_response())
ResponseFile#
- class muffin.ResponseFile(filepath, *, chunk_size=65536, filename=None, headers_only=False, **kwargs)#
Serves files as HTTP responses.
- Parameters:
Note
Under asyncio, serving files requires the optional aiofile package (
pip install aiofile). Trio and Curio use their native async file I/O and need nothing extra.
@app.route('/download')
async def download(request):
return ResponseFile('/path/to/file.txt', filename='file.txt')
ResponseWebSocket#
- class muffin.ResponseWebSocket(scope, receive=None, send=None)#
Provides a WebSocket handler interface.
- Parameters:
scope (TASGIScope)
receive (TASGIReceive | None)
send (TASGISend | None)
@app.route('/ws')
async def websocket(request):
async with ResponseWebSocket(request) as ws:
msg = await ws.receive()
await ws.send(f"Echo: {msg}")
Test Client#
- muffin.TestClient#
alias of
ASGITestClient
Use for testing your Muffin applications efficiently.