Fast, small ASGI-compliant webframework
Project description
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 what shallot
(the [micro-] "webframework") tries to be.
shallot
is a small layer on top of an ASGI - compatible server, like: uvicorn, hypercorn, ... It is haveliy inspired
by ring. The main difference to other webframeworks is, that shallot
is easily plug able and extensible. Every component can be switched and new features can be added without touching shallot
s source-code. That is accomplished by using middlewares for nearly every functionality in shallot
.
shallot
tries hard, to provide a simple API. 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, extending shallot
s functionality requires you to understand the
middleware-concept and that is all. No class-hierarchies or plugin-frameworks are needed.
Architecture
shallot
is an ASGI - compatible webframework.
Basic-Concepts
shallot
models a http-request-response-cycle as single function call. It treats request
and response
as dict
s. The request get passed to a handler
(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-dict - pass 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 typebytes
and 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 as the ASGI-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
: adict
with all header-names askeys
and the corresponding-values asvalues
of the dict. -
body
: The body of the http-request asbytes
.shallot
always read the entire body and then calls thehandler
-function. [bytes
] -
note: many fields are missing! please refer to the documentation
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 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-iterable
yieldingbytes
. When theresponse
contains a key namedstream
, thanshallot
will consume theiterable
and 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 shallot
s functionality is implemented via middlewares. That makes it possible to easily extend, configure or change shallot
s behaviour. 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.
application
the minimal deployable thing, one can build is this:
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)
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))
Features
Nothing is enabled by default. Every functionality has its own middleware.
Routing
To include shallot
s builtin routing functionality, use the routing-middleware: wrap_routes
.
routing is one essential and by far, the most opinionated part of any webframeworks-api. shallot
is no exception there. Routing is defined completely via a data-structure:
async def hello_world(request):
return text("hi user!")
# is attached to a "dynamic"-route with one parseable url-part
async def handle_index(request, idx):
return text(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, routes
is a list of tuples with:
1. the (potentially dynamic) route
2. the allowed methods
3. the handler
Routes 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 the request
and as many arguments as there are {tag}
s.
JSON
to easily work with json-data, use the json-middleware wrap_json
:
every request, that contains a content-type application/json
will be parsed and the result will be attached to the request under the key json
.
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 the shallot.response
- function json
:
from shallot.response import json
async def json_handler(request):
client_json_data = request.get("json")
assert isinstance(client_json_data, dict)
return json({"hello": "world"})
Static-Files
shallot
is 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 the wrap_static
- middleware.
This middleware depends on aiofiles
.
import os
here = os.path.dirname(__file__)
wrap_static("/static/data", root_path=here) # will always assume the folder is located : <this_file>.py/static/data
Browser-caches will be honored. For that, last-modified
and etag
- headers will be sent accordingly.
Requests with a path containing "../" will be automatically responded with 404-Not Found
.
Websockets
In shallot, websockets are modeled as async-generators. Except that, websockets-handlers are more or less equal to http-handlers.
They receive data, str
or bytes
from the generator (receiver
) and a dict
from the opening http-request (request
). As a
result a websocket-handler yields back data (dict
), in the example below, constructed via ws_send
@websocket
async def echo_server(request, receiver):
async for message in receiver:
yield ws_send(f"@echo: {message}")
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
File details
Details for the file shallot-0.2.0-py3-none-any.whl
.
File metadata
- Download URL: shallot-0.2.0-py3-none-any.whl
- Upload date:
- Size: 31.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/39.0.1 requests-toolbelt/0.9.1 tqdm/4.45.0 CPython/3.6.9
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | ea9320ba154b16ef4949dcb5c1a20616688550c402ff89510ec79856da23a26b |
|
MD5 | 6841d2e9d61cd58c4f8f79f313ebfe50 |
|
BLAKE2b-256 | c1cf8c83d5541055b2f892051f48d2d5e28d5e31b7f06f09dfad632b3849fdb1 |