Package: shallot
shallot - a plugable "webframework"What is a shallot?It is a small onion. It has only small and few layers. When you use it (cut it for cooking), it does not make
you cry (that much).The above description of the vegetable, is a good mission-statement for whatshallot(the [micro-] "webframework") tries to be.shallotis a small layer on top of an ASGI - compatible server, like: uvicorn, hypercorn, ... It is haveliy inspired
byring. The main difference to other webframeworks is, thatshallotis easily plug able and extensible. Every component can be switched and new features can be added without touchingshallots source-code. That is accomplished by using middlewares for nearly every functionality inshallot.shallottries hard, to provide asimpleAPI. For that, only standard-types and functions (and one decorator) are used. The goal is, that a user can
freely choose her / his tools for testing, documentation and so on. Another benefit, extendingshallots functionality requires you to understand the
middleware-concept and that is all. No class-hierarchies or plugin-frameworks are needed.Architectureshallotis anASGI- compatible webframework.Basic-Conceptsshallotmodels a http-request-response-cycle as single function call. It treatsrequestandresponseasdicts. The request get passed to ahandler(which itself can be "middleware-decorated") and thehandlerproduces a response.
Basicallyshallotworks like this:take the ASGIconnection-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)    |
+----------+           +----------+             +------------+requestTherequestis always the first argument that gets passed to yourhandler-function. It is of typedict. It has basically the same content as theASGI-connection-scope.A request will at least have the following structure:type: http [string]method: the http-verb in uppercase (for example: "GET", "PUT", "POST", ...) [string]headers: adictwith all header-names askeysand the corresponding-values asvaluesof the dict.body: The body of the http-request asbytes.shallotalways read the entire body and then calls thehandler-function. [bytes]note: many fields are missing! please refer to the documentationresponseTheresponseis the result of the function-call to the handler (with therequestas first argument). Theresponsehas to be adict. The reponse 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.handlershallotassembles a request-dict and calls a user-provided handler. Ahandleris an async-function that takes a request and returns a response (dict).asyncdefhandler(request):return{"status":200}middlewareMost ofshallots  functionality is implemented via middlewares. That makes it possible to easily extend, configure or changeshallots behaviour. In fact: if you don't like the implementation of a certain middleware, just write your own and use it instead (or better: enhanceshallotvia 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.applicationthe minimal deployable thing, one can build is this:asyncdefminimal(request):"""answer EVERY request with 200 and NO body"""return{"status":200}server=build_server(minimal)if__name__=="__main__":importuvicorn# shallot is not tied to uvicorn, its just fastuvicorn.run(server)to configure/run a real application, one would typically chain/apply a pile of middlewares and a handler:middleware_pile=apply_middleware(wrap_content_type(),wrap_static("/static/data"),wrap_routes(routes),wrap_parameters(),wrap_cookies,wrap_json,)server=build_server(middleware_pile(standard_not_found))FeaturesNothing is enabled by default. Every functionality has its own middleware.RoutingTo includeshallots builtin routing functionality, use the routing-middleware:wrap_routes.routing is one essential and by far, the most opinionated part of any webframeworks-api.shallotis no exception there. Routing is defined completely via a data-structure:asyncdefhello_world(request):returntext("hi user!")# is attached to a "dynamic"-route with one parseable url-partasyncdefhandle_index(request,idx):returntext(f"hi user number:{idx}")routes=[("/",["GET"],hello_world),("/hello",["GET"],hello_world),("/hello/{index}",["GET"],handle_index),("/echo",["GET","PUT","POST"],post_echo),("/json",["GET","PUT"],show_and_accept_json),]as shown above,routesis a list of tuples with:1. the (potentially dynamic) route
2. the allowed methods
3. the handlerRoutes with an{tag}in it, are considered dynamic-routes. The router will parse the value from the url and transfered it (as string) to the handler-function. Therfore the handler function must accept therequestand as many arguments as there are{tag}s.JSONto easily work with json-data, use the json-middlewarewrap_json:every request, that contains a content-typeapplication/jsonwill be parsed and the result will be attached to the request under the keyjson.
When data body is not parseable as json, the middleware will respond with{"status": 400, "body": "Malformed JSON"}.when you want to return json-data as your response, use theshallot.response- functionjson:fromshallot.responseimportjsonasyncdefjson_handler(request):client_json_data=request.get("json")assertisinstance(client_json_data,dict)returnjson({"hello":"world"})Static-Filesshallotis not optimized to work as static-file-server. Although it goes to great lengths, to provide a solid experience for serving static content.To work with static-files use thewrap_static- middleware.This middleware depends onaiofiles.importoshere=os.path.dirname(__file__)wrap_static("/static/data",root_path=here)# will always assume the folder is located : <this_file>.py/static/dataBrowser-caches will be honored. For that,last-modifiedandetag- headers will be sent accordingly.
Requests with a path containing "../" will be automatically responded with404-Not Found.WebsocketsIn shallot, websockets are modeled as async-generators. Except that, websockets-handlers are more or less equal to http-handlers.
They receive data,strorbytesfrom the generator (receiver) and adictfrom the opening http-request (request). As a
result a websocket-handler yields back data (dict), in the example below, constructed viaws_send@websocketasyncdefecho_server(request,receiver):asyncformessageinreceiver:yieldws_send(f"@echo:{message}")