µHTTP - Pythonic Web Development
Why
- Easy: intuitive, clear logic
- Simple: small code base, no external dependencies
- Modular: application mounting, custom route behavior
- Flexible: unopinionated, paradigm-free
- Fast: minimal overhead
- Safe: small attack surface
Installation
µHTTP is on PyPI.
pip install uhttp
Also, an ASGI server might be needed.
pip install uvicorn
Hello, world!
from uhttp import Application
app = Application()
@app.get('/')
def hello(request):
return f'Hello, {request.ip}!'
if __name__ == '__main__':
import uvicorn
uvicorn.run('__main__:app')
Documentation
Application
An ASGI application. Called once per request by the server.
Application(*, routes=None, startup=None, shutdown=None, before=None, after=None, max_content=1048576)
E.g.:
app = Application(
startup=[open_db],
before=[counter, auth],
routes={
'/': {
'GET': lambda request: 'HI!',
'POST': new
},
'/users/': {
'GET': users,
'PUT': users
}
},
after=[logger],
shutdown=[close_db]
)
Application Mounting
Mounts another application at the specified prefix.
app.mount(another, prefix='')
E.g.:
utils = Application()
@utils.before
def incoming(request):
print('Incoming from {request.ip}')
app.mount(utils)
Application Lifespan (Startup)
Append the decorated function to the list of functions called at the beginning of the Lifespan protocol.
@app.startup
[async] def func(state)
E.g.:
@app.startup
async def open_db(state):
state['db'] = await aiosqlite.connect('db.sqlite')
Application Lifespan (Shutdown)
Appends the decorated function to the list of functions called at the end of the Lifespan protocol.
@app.shutdown
[async] def func(state)
E.g.:
@app.shutdown
async def close_db(state):
await state['db'].close()
Application Middleware (Before)
Appends the decorated function to the list of functions called before a response is made.
@app.before
[async] def func(request)
E.g.:
@app.before
def restricted(request):
user = request.state['session'].get('user')
if user != 'admin':
raise Response(401)
Application Middleware (After)
Appends the decorated function to the list of functions called after a response is made.
@app.after
[async] def func(request, response)
E.g.:
@app.after
def logger(request, response):
print(request, '-->', response)
Application Routing
Inserts the decorated function to the routing table.
@app.route(path, methods=('GET',))
[async] def func(request)
Paths are compiled at startup as regular expression patterns. Named groups define path parameters.
If the request path doesn't match any route pattern, a 404 Not Found response is returned.
If the request method isn't in the route methods, a 405 Method Not Allowed response is returned.
Decorators for the standard methods are also available:
@app.get(path)
@app.head(path)
@app.post(path)
@app.put(path)
@app.delete(path)
@app.connect(path)
@app.options(path)
@app.trace(path)
@app.patch(path)
E.g.:
@app.route('/', methods=('GET', 'POST'))
def index(request):
return f'{request.method}ing from {request.ip}'
@app.get(r'/user/(?P<id>\d+)')
def profile(request):
user = request.state['db'].get_or_404(request.params['id'])
return '{user.name} has {user.friends} friends and lives in {user.location}'
Request
An HTTP request. Created every time the application is called on the HTTP protocol with a shallow copy of the state.
Request(method, path, *, ip='', params=None, args=None, headers=None, cookies=None, body=b'', json=None, form=None, state=None)
Response
An HTTP Response.
Response(status, *, headers=None, cookies=None, body=b'')
E.g.:
@app.startup
def open_db(state):
state['db'] = {
1: {
'name': 'admin',
'likes': ['terminal', 'old computers']
},
2: {
'name': 'john',
'likes': ['animals']
}
}
def get_or_404(db, id):
if user := db.get(id):
return user
else:
raise Response(404)
@app.get(r'/user/(?P<id>\d+)')
def profile(request):
user = get_or_404(request.state['db'], request.params['id'])
if request.args.get('json'):
return user
else:
return "{user['name']} likes {', '.join(user['likes'])}"
Patterns
Sessions
Session implementation based on JavaScript Web Signatures. Sessions are stored in the client's browser as a tamper-proof cookie. Depends on PyJWT.
import os
import time
import jwt
from uhttp import Application, Response
app = Application()
secret = os.getenv('APP_SECRET', 'dev')
@app.before
def get_token(request):
session = request.cookies.get('session')
if session and session.value:
try:
request.state['session'] = jwt.decode(
jwt=session.value,
key=secret,
algorithms=['HS256']
)
except jwt.exceptions.PyJWTError:
request.state['session'] = {'exp': 0}
raise Response(400)
else:
request.state['session'] = {}
@app.after
def set_token(request, response):
if session := request.state.get('session'):
session.setdefault('exp', int(time.time()) + 604800)
response.cookies['session'] = jwt.encode(
payload=session,
key=secret,
algorithm='HS256'
)
response.cookies['session']['expires'] = time.strftime(
'%a, %d %b %Y %T GMT', time.gmtime(session['exp'])
)
response.cookies['session']['samesite'] = 'Lax'
response.cookies['session']['httponly'] = True
response.cookies['session']['secure'] = True
Multipart Forms
Support for multipart forms. Depends on python-multipart.
from multipart.multipart import FormParser, parse_options_header
from multipart.exceptions import FormParserError
from uhttp import Application, MultiDict, Response
app = Application()
def parse_form(request):
form = MultiDict()
def on_field(field):
form[field.field_name.decode()] = field.value.decode()
def on_file(file):
if file.field_name:
form[file.field_name.decode()] = file.file_object
content_type, options = parse_options_header(
request.headers.get('content-type', '')
)
try:
parser = FormParser(
content_type.decode(),
on_field,
on_file,
boundary=options.get(b'boundary'),
config={'MAX_MEMORY_FILE_SIZE': float('inf')} # app._max_content
)
parser.write(request.body)
parser.finalize()
except FormParserError:
raise Response(400)
return form
@app.before
def handle_multipart(request):
if 'multipart/form-data' in request.headers.get('content-type'):
request.form = parse_form(request)
Static Files
Static files for development.
import os
from mimetypes import guess_type
from uhttp import Application, Response
app = Application()
def send_file(path):
if not os.path.isfile(path):
raise RuntimeError('Invalid file')
mime_type = guess_type(path)[0] or 'application/octet-stream'
with open(path, 'rb') as file:
content = file.read()
return Response(
status=200,
headers={'content-type': mime_type},
body=content
)
@app.get('/assets/(?P<path>.*)')
def assets(request):
directory = 'assets'
path = os.path.realpath(
os.path.join(directory, request.params['path'])
)
if os.path.commonpath([directory, path]) == directory:
if os.path.isfile(path):
return send_file(path)
if os.path.isdir(path):
index = os.path.join(path, 'index.html')
if os.path.isfile(index):
return send_file(index)
return 404
Contributing
All contributions are welcomed.
License
Released under the MIT license.
Metadata
Release files for uhttp 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| uhttp-2.0.0.tar.gz | 7.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| uhttp-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 14.4 kB
Release files / uhttp-2.0.0.tar.gz
| Download URL | uhttp-2.0.0.tar.gz |
|---|---|
| Size | 7.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
55baf245be446ba604cae4b9f75a9884c6678d9ab22eb1749eb983ddbfc6bc6d
|
|
BLAKE2b-256 checksum How to use checksums |
8e6f3e814e2f35ae4b360248292ae9221ca9cb8f43c4e84ff35917a501c89d94
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/1.7.1 CPython/3.11.2 Linux/6.1.0-rpi7-rpi-v8
|
Release files / uhttp-2.0.0-py3-none-any.whl
| Download URL | uhttp-2.0.0-py3-none-any.whl |
|---|---|
| Size | 7.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f8a53bfb69d1d313549b1e52bd705e8c093e5dac6e76d984a3bf1c6a8104a077
|
|
BLAKE2b-256 checksum How to use checksums |
70aa424953fdc56392197a0a06cb5f5f61f2f9426c49f90d68045d2554f29f7a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/1.7.1 CPython/3.11.2 Linux/6.1.0-rpi7-rpi-v8
|