Pythonic Web Development
Project description
µ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.
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.
Source Distribution
Built Distribution
File details
Details for the file uhttp-2.0.0.tar.gz
.
File metadata
- Download URL: uhttp-2.0.0.tar.gz
- Upload date:
- Size: 7.0 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
Algorithm | Hash digest | |
---|---|---|
SHA256 | 55baf245be446ba604cae4b9f75a9884c6678d9ab22eb1749eb983ddbfc6bc6d |
|
MD5 | 319951caeb644e757e5f201b219abc1b |
|
BLAKE2b-256 | 8e6f3e814e2f35ae4b360248292ae9221ca9cb8f43c4e84ff35917a501c89d94 |
File details
Details for the file uhttp-2.0.0-py3-none-any.whl
.
File metadata
- Download URL: uhttp-2.0.0-py3-none-any.whl
- Upload date:
- Size: 7.4 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
Algorithm | Hash digest | |
---|---|---|
SHA256 | f8a53bfb69d1d313549b1e52bd705e8c093e5dac6e76d984a3bf1c6a8104a077 |
|
MD5 | c9126b074758c609bf713c6af3a96963 |
|
BLAKE2b-256 | 70aa424953fdc56392197a0a06cb5f5f61f2f9426c49f90d68045d2554f29f7a |