Skip to main content

An async-first Flask-style web framework built on ASGI.

Project description

exile

An async-first Python web framework with Flask-like APIs on top of ASGI.

Install

Install from local source:

python3 -m pip install -e .

After publishing to PyPI:

python3 -m pip install exile-line

Install template support:

python3 -m pip install -e .[templates]

Quickstart

from exile import Exile

app = Exile(__name__)

@app.get("/")
async def index():
    return {"hello": "exile"}

@app.get("/users/<int:user_id>")
async def user_detail(user_id: int):
    return {"user_id": user_id}

Run with uvicorn:

uvicorn app:app --reload

Core APIs

  • Routing: @app.route, @app.get, @app.post, @app.put, @app.patch, @app.delete
  • Request context: request, current_app, g
  • Response helpers: Response, JSONResponse, HTMLResponse, RedirectResponse, FileResponse
  • Helpers: abort, jsonify, make_response, url_for, render_template, render_template_async
  • Middleware:
    • Function middleware: @app.middleware("http")
    • ASGI class middleware: app.add_middleware(...)
  • Blueprint: Blueprint(...); app.register_blueprint(...)
  • Lifespan: @app.on_startup, @app.on_shutdown
  • Testing: AsyncTestClient

Routing

@app.get("/users/<int:user_id>")
async def user_detail(user_id: int):
    return {"user_id": user_id}

@app.post("/users")
async def create_user():
    data = await request.json()
    return {"created": data}, 201

Supported converters:

  • str (default): <name>
  • int: <int:user_id>
  • float: <float:amount>
  • path: <path:filepath>

Static files (/static/<path:filename>) include cache headers by default:

  • Cache-Control: public, max-age=3600
  • ETag
  • Last-Modified
  • Supports conditional requests (If-None-Match / If-Modified-Since) and returns 304 Not Modified
  • Supports byte range requests (Range: bytes=...) with 206 Partial Content and 416 Range Not Satisfiable

Reverse URL build:

path = app.url_for("user_detail", user_id=9, page=2)
# /users/9?page=2

Request And Response

Request helpers:

  • await request.body()
  • await request.text()
  • await request.json()
  • request.args (query string MultiDict)

Response helpers:

  • Return dict/list directly for JSON
  • Return tuple: (body, status) or (body, status, headers)
  • jsonify(...), make_response(...)
  • FileResponse, StreamingResponse, HTMLResponse, RedirectResponse

Template rendering strategy:

  • render_template(...): synchronous helper, Flask-like ergonomics
  • await render_template_async(...): render in worker thread to avoid blocking event loop in async views

Error Handling

from exile import abort

@app.errorhandler(404)
def not_found(exc):
    return {"error": "not found"}, 404

@app.get("/must-exist")
def must_exist():
    abort(404)

Blueprint-scoped error handler:

api = Blueprint("api", __name__)

@api.errorhandler(404)
def api_not_found(exc):
    return {"api_error": "not found"}, 404

Middleware

Function middleware:

@app.middleware("http")
async def add_header(req, call_next):
    response = await call_next(req)
    response.headers["X-App"] = "exile"
    return response

ASGI middleware class:

app.add_middleware(SomeASGIMiddleware, option="value")

Lifespan

@app.on_startup
def startup():
    ...

@app.on_shutdown
async def shutdown():
    ...

Blueprint Example

from exile import Blueprint

api = Blueprint("api", __name__, url_prefix="/v1")

@api.get("/ping")
async def ping():
    return {"ok": True}

app.register_blueprint(api, url_prefix="/api")

Async TestClient Example

import asyncio
from exile import AsyncTestClient

client = AsyncTestClient(app)

async def main():
    resp = await client.get("/users/1")
    assert resp.status_code == 200
    assert resp.json()["user_id"] == 1

asyncio.run(main())

Example App

Run:

python3 examples/basic_app.py

Run Tests

python3 -m unittest discover -s tests -v

Build Distribution

python3 -m pip install --upgrade build
python3 -m build

Release precheck:

./scripts/release_check.sh

Publish to TestPyPI (dry run before real release):

export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="<your-testpypi-token>"
./scripts/publish_testpypi.sh

Publish to PyPI:

export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="<your-pypi-token>"
./scripts/publish_pypi.sh

Changelog content check:

python3 scripts/check_changelog_content.py

Wheel smoke test:

python3 scripts/smoke_test_wheel.py

Dist artifact check:

python3 scripts/check_dist_artifacts.py

Tag/version verification:

python3 scripts/check_tag_version.py v0.1.0

Bump version:

python3 scripts/bump_version.py 0.1.1

Clean build artifacts:

./scripts/clean.sh

Check working tree before publish:

./scripts/check_worktree_clean.sh

Release

See release steps in:

RELEASE.md

Release notes file:

CHANGELOG.md

Make Targets

make test
make compile
make release-check
make bump-version V=0.1.1
make clean

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

exile_line-0.1.0.tar.gz (23.5 kB view details)

Uploaded Source

Built Distribution

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

exile_line-0.1.0-py3-none-any.whl (20.3 kB view details)

Uploaded Python 3

File details

Details for the file exile_line-0.1.0.tar.gz.

File metadata

  • Download URL: exile_line-0.1.0.tar.gz
  • Upload date:
  • Size: 23.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.1

File hashes

Hashes for exile_line-0.1.0.tar.gz
Algorithm Hash digest
SHA256 619d01397029ccbfb5508bb96bf447b4f0a0592f65675cee120624353c5a1b4d
MD5 f19757bff95dc065a22b9f3d99e6f6f2
BLAKE2b-256 38a2553995f862d14eccc45b63aa1a74a8736d9a0dc13641f763de8700a32b01

See more details on using hashes here.

File details

Details for the file exile_line-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: exile_line-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 20.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.1

File hashes

Hashes for exile_line-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f3e579c3488f80609a6fb2dd365bd65debef56ae8b53dfac88136d12cf04272e
MD5 c20393c8e136cbe4292e616b84c2e9d0
BLAKE2b-256 988b1379df2139d3f78dc172b309704a35f78b8411f12148b50aa6c72558d4b9

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