Skip to main content

Fast, small ASGI-compliant webframework

Project description

shallot - a plugable "webframework"

Documentation Status PyPI version

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 shallots 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 shallots 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 dicts. The request get passed to a handler (which itself can be "middleware-decorated") and the handler produces a response. Basically shallot works like this:

  1. take the ASGI connection-scope (dict)
  2. read the body of the request and attach the body (bytes) to scope-dict
  3. pass the request-dict (scope + attached body) to a user-defined function (called handler)
  4. the result (response) of a handler has to be a dict. The response must at least provide a status-key with an integer. If provided a body-key for the response is provided, than the value must be of type bytes 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: a dict with all header-names as keys and the corresponding-values as values of the dict.

  • body: The body of the http-request as bytes. shallot always read the entire body and then calls the handler-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 a dict (for example: {"header1-name": "header1-value", ...})
  • stream [optional]: this must be an async-iterable yielding bytes. When the response contains a key named stream, than shallot will consume the iterable 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 shallots functionality is implemented via middlewares. That makes it possible to easily extend, configure or change shallots 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 shallots 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

shallot-0.2.0-py3-none-any.whl (31.1 kB view details)

Uploaded Python 3

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

Hashes for shallot-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ea9320ba154b16ef4949dcb5c1a20616688550c402ff89510ec79856da23a26b
MD5 6841d2e9d61cd58c4f8f79f313ebfe50
BLAKE2b-256 c1cf8c83d5541055b2f892051f48d2d5e28d5e31b7f06f09dfad632b3849fdb1

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page