Skip to main content

Pythonic web development

Project description

µHTTP

Pythonic web development

µHTTP emerged from the need of a simple, hassle-free web framework. It's great for microservices, single page applications, AND monolithic monsters.

In µHTTP there is no hidden logic. Everything is what it seems.

Why

  • Stupid simple, seriously there are maybe 15 lines of "real" code in it. No external dependencies.
  • Extremely modular, entire extensions can just follow the simple App pattern.
  • Flexible, say what you will about wrong responses, they work.
  • Fast, because it doesn't really do much.
  • Very opinionated, to the point where it has no opinions.
  • Not typist.

Installation

µHTTP is on PyPI.

pip install uhttp

You might also need an ASGI server. I recommend Uvicorn.

pip install uvicorn

Hello, world!

#!/usr/bin/env python3

from uhttp import App


app = App()


@app.get('/')
def hello(request):
    return 'Hello, world!'


if __name__ == '__main__':
    import uvicorn
    uvicorn.run('__main__:app')

Inspirations

The rant.

TODO

  • Tests
  • Multipart requests
  • Tutorial

API Reference

Application

class App:
    _routes: dict
    _startup: list
    _shutdown: list
    _before: list
    _after: list
    _max_content: int

    def mount(self, app, prefix=''):
        self._startup += app._startup
        self._shutdown += app._shutdown
        self._before += app._before
        self._after += app._after
        self._routes.update({prefix + k: v for k, v in app._routes.items()})
        self._max_content = max(self._max_content, app._max_content)

In particular, this Django-like pattern is possible:

app = App(
    startup=[open_db, dance],
    before=[auth],
    routes={
        '/': {
            'GET': index,
            'POST': filter
        },
        '/users/': {
            'GET': users,
            'PUT': users
        }
    },
    after=[logger],
    shutdown=[close_db]
)

app.mount is µHTTP's modularity.

In users.py you have:

from uhttp import App

app = App()

@app.before
def auth(request):
    ...

@app.route('/', methods=('GET', 'PUT'))
def users(request):
    ...

In db.py:

from uhttp import App

app = App()

@app.startup
async def open_db(state):
    ...

@app.shutdown
async def close_db(state):
    ...

Finally, in main.py:

from uhttp import App
import users
import db

app = App()
app.mount(users.app, prefix='/users')
app.mount(db.app)

@app.get('/')
def index(request):
    ...

Entire extensions can be just apps!

Lifespan functions

Lifespan functions are based on the Lifespan Protocol.

There are two decorators: @app.startup and @app.shutdown. Decorated functions receive one argument: state.

This is a great place to setup database connections and other dependencies that your application might need.

A shallow copy of the state is passed to each request.

Middleware

µHTTP provides two decorators @app.before and @app.after.

Any value returned from the decorated functions will set the response and break the control flow.

@app.before functions receive only a request argument. They are called before a response is made, i.e. before the route function (if there is one). Particularly, request.params is still empty at this point. This is a great place to handle bad requests. The early response pattern:

from uhttp import App, Response

app = App()

@app.before
def auth(request):
    if 'user' not in requet.state:
        raise Response(401)
    if request.state['user']['credits'] < 1:
        return 402

@app.after functions receive a request and a response. They are called after a response is made. You should modify the response here.

@app.after
def dancing(request, response):
    response.cookies['dancing'] = 'in the street'
    ...

Route functions

The main route decorator is @app.route(path, methods=('GET',)). There's also route decorators for all the standard methods: @app.get, @app.head, @app.post, @app.put, @app.delete, @app.connect, @app.options, @app.trace, @app.patch.

The path parameter is present on all decorators. µHTTP handles paths as regular expressions. To define path parameters like /user/<id> you can use named groups:

@app.get('/users/(?P<id>\d+)')
def users(request):
    user_id = request.params['id']
    return {'user': request.state['db']['user_id']}

To improve performance, all path regular expressions are compiled at startup.

Route functions will only be called if no @app.before middleware has set the response.

The response comes from the return value of the decorated function. If there is no return, the response defaults to 204 No Content. The return values can be: int (status), str (body), bytes (raw body), dict (JSON) and Response.

If the request doesn't match any path, response is set to 404 Not Found. If the request doesn't match any method of the path, response is set to 405 Method Not Allowed.

Static files

µHTTP doesn't support static files. It shouldn't (a real web server like Unit should handle them). But in development they might come handy:

import os
import mimetypes

@app.startup
def static(state):  # Non-recursive, keeps files in memory
    for entry in os.scandir('static'):
        if entry.is_file():
            with open(entry.path, 'rb') as f:
                content = f.read()
            content_type, _ = mimetypes.guess_type(entry.path)
            app._routes['/' + entry.path] = {
                    'GET': lambda _: Response(
                        status=200,
                        body=content,
                        headers={'content-type': content_type or ''}
                    )
                }

Requests

class Request:
    method: str
    path: str
    params: dict
    args: MultiDict
    cookies: SimpleCookie
    body: bytes
    json: Any
    form: MultiDict
    state: dict

Multipart requests

Currently, µHTTP doesn't support multipart/form-data requests. Here's an implementation with multipart:

from io import BytesIO
from multipart import MultipartError, MultipartParser, parse_options_header

@app.before
def parse_multipart(request):
    content_type = request.headers.get('content-type', '')
    content_type, options = parse_options_header(content_type)
    content_length = int(request.headers.get('content-length', '-1'))
    if content_type == 'multipart/form-data':
        request.form['files'] = {}
        try:
            stream = BytesIO(request.body)
            boundary = options.get('boundary', '')
            if not boundary:
                raise MultipartError
            for part in MultipartParser(stream, boundary, content_length):
                if part.filename:
                    request.form['files'][part.name] = part.raw
                else:
                    request.form[part.name] = part.value
        except MultipartError:
            raise Response(400)

Responses

class Response(Exception):
    status: int
    description: str
    headers: MultiDict
    cookies: SimpleCookie
    body: bytes

    def from_any(any: Any) -> Response:
        ...

The fact that Response inherits from Exception is what makes µHTTP so flexible.

def pay(request, cost):
    user = request.state.get('user')
    if not user:
        raise Response(401)
    if user.money < cost:
        raise Response(402, body=b'Insufficient funds!')
    user.money -= cost
    return user

@app.get('/buy/bananas')
def see_bananas(request):
    return "Hey there, see any bananas that you'd like?"

@app.post('/buy/bananas')
def buy_bananas(request):
    user = pay(request, 5)
    return f'Congratulations! {user.name} just bought a banana!'

Templates

µHTTP doesn't support templating engines. However, implementing Jinja is very easy:

import jinja2

@app.startup
def load_jinja(state):
    state['jinja'] = jinja2.Environment(
        loader=jinja2.FileSystemLoader('templates')
    )


@app.route('/')
def hello(request):
    template = request.state['jinja'].get_template('hello.html')
    return template.render(name=request.args.get('name'))

Internals

async def asyncfy(func, /, *args, **kwargs):
    if iscoroutinefunction(func):
        return await func(*args, **kwargs)
    else:
        return await to_thread(func, *args, **kwargs)

µHTTP runs all synchronous code in a separate thread, so as to not block the main loop. As long as your code is thread-safe things should be fine. E.g. instead of opening one sqlite3 connection at startup, consider opening one for every request or just using aiosqlite.

class MultiDict(dict):
    ...

Because of HTTP 1.1 quirks, µHTTP has a MultiDict implementation. But, you shouldn't really care about that.

Contributing

Feel free to fork, complain, improve, document, write extensions.

License

Released under the MIT License.

Project details


Download files

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

Source Distribution

uhttp-1.2.1.tar.gz (7.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

uhttp-1.2.1-py3-none-any.whl (8.3 kB view details)

Uploaded Python 3

File details

Details for the file uhttp-1.2.1.tar.gz.

File metadata

  • Download URL: uhttp-1.2.1.tar.gz
  • Upload date:
  • Size: 7.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.11.2 Linux/6.1.0-rpi7-rpi-v8

File hashes

Hashes for uhttp-1.2.1.tar.gz
Algorithm Hash digest
SHA256 db9d810c329d50d8d2fd55b045e551a2c3d41ab52ac50cd217e6cc6ed4a81e90
MD5 ab83a19e9091e7f655c4809857c49b49
BLAKE2b-256 8ddd78832d5b8c85210f1787bf3ff3b2324f3d0b956c449610b2e15299096db2

See more details on using hashes here.

File details

Details for the file uhttp-1.2.1-py3-none-any.whl.

File metadata

  • Download URL: uhttp-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 8.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.11.2 Linux/6.1.0-rpi7-rpi-v8

File hashes

Hashes for uhttp-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 010b1538be535ade3908a9fa369a516a444f2c3f3183f865daefc0b7a4954151
MD5 c8c3c65d039b61a4f952409b2c687b5e
BLAKE2b-256 614afde625034a376362cf465c05b957e81a185428454cc876fc33fe756ef87f

See more details on using hashes here.

Supported by

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