Skip to main content

Pythonic web development

Project description

µHTTP

Pythonic web development

About

µ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.

Installation

µHTTP is on PyPI.

pip install uhttp

You might also need a web server. µHTTP follows the ASGI specification. A nice implementation is 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

Reference

Application

In µHTTP everything is an app.

class App

The parameters are:

  • routes: A dict of your routes, following: {'/path': {'METHOD': func}}
  • startup: A list of functions that run at the beginning of the lifespan
  • shutdown: A list of functions that run at the end of the lifespan
  • before: A list of functions that run before the response
  • after: A list of functions that run after the response
  • max_content: An int, sets the request body size limit (defaults to 1 MB)

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(other_app, prefix='')

app.mount is what makes µHTTP so fraking modular. Here's how:

  1. Appends other_app middleware and lifespan functions to app
  2. Maps other_app routes to app with prefix
  3. Sets app.max_content as a max between other_app and app

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 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 Protocol.

There are two decorators: @app.startup and @app.shutdown. The 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.

@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)

@app.after functions receive a request and a response. They are called after a response is made. You should modify the response here. Responses cannot be raised at this point.

@app.after
def log(request, response):
    print(request.method, request.path)
    ...

Route functions

The main route decorator is @app.route(path, methods=('GET',)). There's also specific 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.patch('/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.

The response comes from the return value of the route 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 of the path methods, response is set to 405 Method Not Allowed.

µHTTP doesn't support static files. It shouldn't. But if you need them:

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

No, you don't need to import them.

class Request

Parameters / Attributes:

  • method: str
  • path: str
  • params: dict
  • args: MultiDict
  • headers: MultiDict
  • cookies: SimpleCookie
  • body: bytes
  • json: dict
  • form: MultiDict
  • state: dict

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

Yes, they are wrong.

class Response(Exception)

Parameters / Attributes:

  • status: int
  • description: str (attribute derived from status)
  • headers: MultiDict
  • cookies: SimpleCookie
  • body: bytes

Response inherits from Exception. This is quite handy: In @app.before functions you can raise early responses and @app.route may call other @app.route functions.

µ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'))

More

Read the source code. It will cost you all of 5 minutes.

Contributing

Feel free to fork, complain, improve, document, fix typos...

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.0.tar.gz (7.2 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.0-py3-none-any.whl (7.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: uhttp-1.0.tar.gz
  • Upload date:
  • Size: 7.2 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.0.tar.gz
Algorithm Hash digest
SHA256 568e7565fe4820fab87c1017270eaae2d31bebaa9625ad21ffeea79bd664c109
MD5 82d10ae6c17a43af3589e2bf44ce09d1
BLAKE2b-256 9305de930cd2262bf5ab5238029662ca1ca8cb6e043a346c40c116bb99dc9344

See more details on using hashes here.

File details

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

File metadata

  • Download URL: uhttp-1.0-py3-none-any.whl
  • Upload date:
  • Size: 7.7 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9671888fb810258add00705e64a721b98c33e1999b8202b21d303f0e2a3707c5
MD5 e90a9023dd750a36ca70704526b9a537
BLAKE2b-256 c95c5cd822fae60f303020bd122ba1f1d5032ed731f165d408dafbad1748c07d

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