The basic concepts¶
Overview¶
shallot models a http-request-response-cycle as single function call. It treats request and response as dicts. The request get passed to a handler-function (which itself can be “middleware-decorated”) and the handler produces a response.
Basically shallot works like this:
take the ASGI connection-scope (
dict)read the body of the request and attach the body (
bytes) to scope-dictpass the request-
dict(scope + attached body) to a user-defined function (calledhandler)the result (
response) of a handler has to be adict. The response must at least provide astatus-key with an integer. If provided abody-key for the response is provided, than the value must be of typebytesand to will be transferred to the client.
data-flow¶
+----------+ +----------+ +------------+
| | | | | |
| +-----------> request +-------------> middlewares+-----------+
| | | | | (enter) | |
| | +----------+ +------------+ |
| A | |
| S | |
| G | |
| I | |
| | +---------v--------+
| | | | |
| | | handler |
| S | | |
| E | +---------+--------+
| R | |
| V | |
| E | |
| R | +----------+ +------------+ |
| | | | | | |
| <-----------+ response <-------------+ middlewares<-----------+
| | | | | (leave) |
+----------+ +----------+ +------------+
request¶
The request is always the first argument that gets passed to your handler-function. It is of type dict. It has basically the same content than the ASGI-connection-scope.
A request will at least have the following structure:
type: http [string]http_version: one of1.0,1.1or2[string]method: the http-verb in uppercase (for example: “GET”, “PUT”, “POST”, …) [string]scheme[optional, but not empty]: the url-scheme (for example: “http”, “https”) [string]query_string: Byte-string with the url-query-path content (everything after the first?) [bytes]root_path: mounting-point of your application [string]client: A two-item iterable of[host, port], where host is a unicode string of the remote host’s IPv4 or IPv6 address, and port is the remote port as an integer. Optional, defaults to None. [list|tuple]server: A two-item iterable of[host, port], where host is the listening address for this server as a unicode string, and port is the integer listening port. Optional, defaults to None. [list|tuple]headers: adictwith all header-names askeysand the corresponding-values asvaluesof the dict. Duplicatedheaderswill be joined “comma-separated”. All header-names are lower-cased. [dict]headers_list: the originalheaders-data-structure form the ASGI-connection-scope. This is alistcontainingtuplesin the form:[(header-name1, header-value1), ...]. The header-names can be duplicated. [This is the basis forheaders]body: The body of the http-request asbytes.shallotalways read the entire body and then calls thehandler-function. [bytes]
response¶
The response is the result of the function-call to the handler (with the request as first argument). The response has to be a dict. The response must have the following structure:
status: the http-return-code [int]body[optional]: the body of the http-response [bytes]headers[optional]: the http-response-headers to be used. The value is adict(for example:{"header1-name": "header1-value", ...})stream[optional]: this must be anasync-iterableyieldingbytes. When theresponsecontains a key namedstream, thanshallotwill consume theiterableand will stream the provided data to the client. This is specially useful for large response-bodies.
handler¶
shallot assembles a request-dict and calls a user-provided handler. A handler is an async-function that takes a request and returns a response (dict).
async def handler(request):
return {"status": 200}
middleware¶
Most of shallots functionality is implemented via middlewares. That makes it possible to easily extend, configure or change shallots behavior. In fact: if you don’t like the implementation of a certain middleware, just write your own and use it instead (or better: enhance shallot via PR)!
The general functionality of a middleware is, that it wraps a handler-function-call. Middlewares are designed that way, that they can be composed / chained together. So for a middleware-chain with 3 different middlewares, a call chain might look like:
|-> middleware 1 (enter)
|-> middleware 2 (enter)
|-> middleware 3 (enter)
|-> handler (execute)
|<- middleware 3 (leave)
|<- middleware 2 (leave)
|<- middleware 1 (leave)
A good analogy for a middleware is a python-decorator. A decorator wraps a function and returns another function to provide extended functionality.
middleware signature¶
in order to make middlewares composeable / work together, thy must implement the following signature:
def wrap_print_logging(next_middleware):
async def _log_request_response(handler, request):
print(f"Request to the handler: {request}")
response = await next_middleware(handler, request) # IMPORTANT: here we call the middlewares and wait for them to run
print(f"Response from the handler: {response}")
return response
return _log_request_response
The above example shows a middleware that would simply printout the request and the response from the handler. Every middleware will run for EVERY request that comes to your application!
composing middlewares together¶
middlewares are great because they can be composed/chained together. In that way every middleware can enhance the request / response or choose a different handler to add functionality. Chaining middlewares is done via the apply_middleware - function provided by shallot:
from shallot.middlewares import apply_middleware
middlewares = apply_middleware(middleware1, middleware2, middleware3)
enhanced_handler = middlewares(default_handler)
The result of chain_middleware is a middleware-chain. A middleware-chain is a function that accepts another function, the default_handler. This is the handler-function that gets called after the request is passed through all middlewares. After instantiating the middleware-chain with a handler, the result is another-function. The function behaves just like a normal handler-function and can be used with build_server
differences to ring-middleware (discussion)¶
While the function-signature of a shallot-handler is the same as with ring, the middleware-signature is different and slightly more complex. This is, to support “request-routing” as a middleware. This way, the router can be just another middleware choosing a new handler, instead of enhancing the request. This way, other middlewares (possible type-annotation-aware middlewares) can be chained after the router and have access to the handler-function.
run an application with an ASGI-server¶
the minimal deployable thing one can build is this:
from shallot import build_server
async def minimal(request):
"""
answer EVERY request with 200 and NO body
"""
return {"status": 200}
server = build_server(minimal)
if __name__ == "__main__":
import uvicorn # shallot is not tied to uvicorn, its just fast
uvicorn.run(server, "127.0.0.1", 5000, log_level="info", debug=True)
to configure/run a real application, one would typically chain/apply a pile of middlewares and a handler:
from shallot import build_server
from shallot.middlewares import wrap_parameters, wrap_cookies, wrap_json, apply_middleware, wrap_routes
async def handle_404(request):
return {"status": 404}
middleware_pile = apply_middleware(
wrap_cors(),
wrap_content_type(),
wrap_static("/static/data"),
wrap_routes(routes),
wrap_cookies(),
wrap_json(),
)
server = build_server(middlewre_pile(handle_404))