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
- Flask:
from flask import *
- FastAPI:
Union[Any, None]
- Sanic: A walking contradiction
- Bottle: One file, 3500+ LOC
- Django
Reference
Application
In µHTTP everything is an app.
class App
The parameters are:
routes
: Adict
of your routes, following:{'/path': {'METHOD': func}}
startup
: Alist
of functions that run at the beginning of the lifespanshutdown
: Alist
of functions that run at the end of the lifespanbefore
: Alist
of functions that run before the responseafter
: Alist
of functions that run after the responsemax_content
: Anint
, 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:
- Appends
other_app
middleware and lifespan functions toapp
- Maps
other_app
routes toapp
withprefix
- Sets
app.max_content
as amax
betweenother_app
andapp
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
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 fromstatus
)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
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.