Flask simplicity. Async power.
Project description
Flasio
███████╗██╗ █████╗ ███████╗██╗ ██████╗
██╔════╝██║ ██╔══██╗██╔════╝██║██╔═══██╗
█████╗ ██║ ███████║███████╗██║██║ ██║
██╔══╝ ██║ ██╔══██║╚════██║██║██║ ██║
██║ ███████╗██║ ██║███████║██║╚██████╔╝
╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝
⚡ Flask simplicity. Async power.
What is Flasio?
Flasio is a minimal, production-ready async web framework for Python. It is designed for developers who love Flask's simplicity but need the performance and scalability of ASGI. Flasio lets you write clean, readable async handlers with zero boilerplate, while running on uvicorn for maximum throughput.
Why Flasio instead of Flask?
| Feature | Flask | Flasio |
|---|---|---|
| Protocol | WSGI (sync) | ASGI (async) |
| Handler style | def |
async def |
| Concurrency | Thread-per-request | Coroutine-based |
| Built-in JWT auth | ❌ | ✅ |
| Built-in email | ❌ | ✅ |
| Built-in DB adapter | ❌ | ✅ |
| CLI runner | flask run |
flasio run |
| Core dependencies | Werkzeug + more | uvicorn only |
Table of Contents
- Installation
- Quickstart
- Project Structure
- Core Concepts
- Services
- CLI Reference
- Configuration
- Deployment
- Performance
- FAQ
Installation
Minimum install (uvicorn only):
pip install flasio
With Jinja2 templating:
pip install "flasio[jinja2]"
With async database support:
pip install "flasio[sqlite]" # SQLite via aiosqlite
pip install "flasio[postgres]" # PostgreSQL via asyncpg
Everything:
pip install "flasio[full]"
From source (development):
git clone https://github.com/your-org/flasio.git
cd flasio
pip install -e ".[full]"
Quickstart
Create app.py:
from flasio import Flasio
app = Flasio()
@app.route("/")
async def index(req):
return {"message": "Hello, Flasio!", "status": "running"}
@app.get("/ping")
async def ping(req):
return "pong"
Run it:
flasio run
Visit http://127.0.0.1:8000 or test with curl:
curl http://127.0.0.1:8000/
# {"message": "Hello, Flasio!", "status": "running"}
Project Structure
flasio/ <- root of the repository
│
├── flasio/ <- installable Python package
│ ├── __init__.py <- public API (Flasio, Blueprint, Response, etc.)
│ │
│ ├── core/ <- framework internals (~400 lines total)
│ │ ├── app.py <- Flasio class, ASGI __call__, dispatch
│ │ ├── router.py <- two-tier router (static O(1) + dynamic regex)
│ │ ├── request.py <- Request object wrapping ASGI scope/receive
│ │ ├── response.py <- Response, JSONResponse, HTMLResponse, Redirect
│ │ ├── exceptions.py <- HTTPException hierarchy
│ │ └── middleware.py <- async call_next middleware chain
│ │
│ ├── blueprint/
│ │ └── blueprint.py <- Blueprint class with URL prefix
│ │
│ ├── services/ <- optional batteries
│ │ ├── auth.py <- JWT (HS256, pure stdlib)
│ │ ├── email.py <- SMTP email sender
│ │ ├── database.py <- AsyncSQLite + AsyncPostgres adapters
│ │ └── cloudinary.py <- Cloudinary upload helper
│ │
│ ├── templating/
│ │ └── engine.py <- Jinja2 auto-detect + micro-engine fallback
│ │
│ └── cli/
│ └── main.py <- `flasio run` command with banner
│
├── examples/
│ └── app.py <- full demo application
│
├── benchmarks/
│ └── test.sh <- wrk benchmark script
│
├── pyproject.toml
├── README.md <- you are here
├── CONTRIBUTING.md <- contribution guidelines
└── SETUP.md <- build, test, dev environment setup
Core Concepts
Application
The Flasio class is the central object — create one per application:
from flasio import Flasio
app = Flasio(
static_dir = "static", # directory for static files
template_dir = "templates", # directory for templates
debug = False, # full tracebacks in responses when True
)
In debug mode (debug=True), unhandled exceptions return a full Python traceback as plain text instead of a generic 500 JSON. Never enable this in production.
Routing
Flasio uses a two-tier router:
- Static routes (
/,/about,/api/v1/status) resolve in O(1) via a plain dict. - Dynamic routes (
/users/{id}) scan an ordered list of compiled regex patterns.
Decorator API
# Explicit methods list
@app.route("/path", methods=["GET", "POST"])
async def handler(req):
...
# Shorthand decorators
@app.get("/items")
async def list_items(req): ...
@app.post("/items")
async def create_item(req): ...
@app.put("/items/{id}")
async def replace_item(req): ...
@app.patch("/items/{id}")
async def update_item(req): ...
@app.delete("/items/{id}")
async def remove_item(req): ...
Dynamic Path Parameters
@app.get("/users/{user_id}/posts/{slug}")
async def get_post(req):
user_id = req.path_params["user_id"] # always a str
slug = req.path_params["slug"]
return {"user": user_id, "post": slug}
Path params are always strings — cast explicitly when needed:
uid = int(req.path_params["user_id"])
Request Object
Every handler receives a Request as its first (and only) argument:
@app.post("/submit")
async def submit(req):
method = req.method # "POST"
path = req.path # "/submit"
client = req.client # ("127.0.0.1", 54321) or None
# Headers — dict with lowercase keys
ct = req.headers.get("content-type", "")
auth = req.headers.get("authorization", "")
# Query params — /submit?page=2&tag=async&tag=python
page = req.query_params.get("page") # "2"
tags = req.query_params.get("tag") # ["async", "python"]
# Path params (only present on dynamic routes)
uid = req.path_params.get("user_id")
# Body reading — all async, body bytes cached after first read
raw = await req.body() # bytes
text = await req.text() # str (UTF-8)
data = await req.json() # dict or list
form = await req.form() # dict from application/x-www-form-urlencoded
return {"ok": True}
Response System
Auto-coercion from handler return values
async def a(req): return {"key": "val"} # 200 application/json
async def b(req): return [1, 2, 3] # 200 application/json
async def c(req): return "<h1>Hi</h1>" # 200 text/html
async def d(req): return b"\x89PNG\r\n..." # 200 application/octet-stream
async def e(req): return {"ok": True}, 201 # 201 application/json
async def f(req): return "ok", 200, {"X-ID": "1"} # with extra headers
Explicit Response classes
from flasio import JSONResponse, HTMLResponse, PlainTextResponse, RedirectResponse, Response
return JSONResponse({"error": "not found"}, status_code=404)
return HTMLResponse("<html><body>Hello</body></html>")
return PlainTextResponse("Service unavailable", status_code=503)
return RedirectResponse("/login")
return Response(b"...", status_code=200, media_type="image/png")
Blueprints
# api/users.py
from flasio import Blueprint
users_bp = Blueprint("users", prefix="/users")
@users_bp.get("/")
async def list_users(req):
return {"users": []}
@users_bp.get("/{user_id}")
async def get_user(req):
return {"id": req.path_params["user_id"]}
@users_bp.post("/")
async def create_user(req):
body = await req.json()
return body, 201
# app.py
from flasio import Flasio
from api.users import users_bp
app = Flasio()
app.register_blueprint(users_bp)
# Registered routes:
# GET /users/
# GET /users/{user_id}
# POST /users/
Middleware
async def my_middleware(request, call_next):
# runs before the handler
response = await call_next(request)
# runs after the handler
return response
app.use(my_middleware)
Multiple middlewares execute in registration order (first registered = outermost wrapper):
app.use(logging_middleware) # outermost
app.use(auth_middleware)
app.use(cors_middleware) # innermost
CORS example
async def cors_middleware(request, call_next):
response = await call_next(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
return response
Request timing
import time
async def timing_middleware(request, call_next):
t0 = time.perf_counter()
response = await call_next(request)
ms = (time.perf_counter() - t0) * 1000
response.headers["X-Response-Time"] = f"{ms:.2f}ms"
return response
Error Handling
Raise any HTTPException subclass anywhere — it is caught and returned as JSON:
from flasio import NotFound, BadRequest, Unauthorized, HTTPException
@app.get("/items/{id}")
async def get_item(req):
item = db.get(int(req.path_params["id"]))
if not item:
raise NotFound(f"Item not found")
return item
# Custom status code
raise HTTPException("Payment required", status_code=402)
Error JSON shape:
{"error": "Item not found", "status": 404}
Lifecycle Hooks
@app.on_startup
async def startup():
app.db = await create_db_pool()
@app.on_shutdown
async def shutdown():
await app.db.close()
Static Files
Files in static/ are served at /static/<filename> automatically. MIME type is auto-detected.
GET /static/style.css -> 200 text/css
GET /static/logo.png -> 200 image/png
Templates
@app.get("/dashboard")
async def dashboard(req):
html = app.render("dashboard.html", {"user": "Alice", "items": ["A", "B"]})
return HTMLResponse(html)
Jinja2 is used when installed (pip install "flasio[jinja2]"), otherwise the built-in micro-engine handles {{ var }}, {% if %}, {% for %}.
Services
JWT Auth Service
from flasio.services.auth import AuthService
auth = AuthService(secret="your-secret", expires_in=3600)
token = auth.generate({"user_id": 42, "role": "admin"})
payload = auth.verify(token) # raises ValueError if invalid/expired
Email Service
from flasio.services.email import EmailService
mailer = EmailService(
host="smtp.gmail.com", port=587,
username="you@gmail.com", password="app-password",
)
await mailer.send(
to="friend@example.com",
subject="Hello",
body="Plain text body",
html="<h1>HTML body</h1>",
)
Database Service
from flasio.services.database import AsyncSQLite, AsyncPostgres
async with AsyncSQLite("app.db") as db:
await db.execute("INSERT INTO users (name) VALUES (?)", ["Alice"])
rows = await db.fetch("SELECT * FROM users")
# PostgreSQL
db = AsyncPostgres("postgresql://user:pass@localhost/mydb")
await db.connect()
rows = await db.fetch("SELECT * FROM users")
await db.disconnect()
Cloudinary Service
from flasio.services.cloudinary import CloudinaryService
cdn = CloudinaryService(cloud_name="...", api_key="...", api_secret="...")
result = await cdn.upload("photo.jpg", folder="avatars")
print(result["secure_url"])
await cdn.delete("public_id_here")
CLI Reference
flasio run [MODULE:APP] [--host HOST] [--port PORT] [--reload]
[--no-reload] [--prod] [--log-level LEVEL]
| Flag | Default | Description |
|---|---|---|
MODULE:APP |
app:app |
Module path and ASGI app attribute |
--host |
127.0.0.1 |
Bind address |
--port |
8000 |
Bind port |
--reload |
off | Force auto-reload on |
--no-reload |
off | Force auto-reload off |
--prod |
off | Production mode (no banner, no reload) |
--log-level |
info/warning |
Uvicorn log verbosity |
Configuration
Flasio has no built-in config system. Use environment variables:
import os
from flasio import Flasio
from flasio.services.auth import AuthService
app = Flasio(debug=os.getenv("DEBUG", "false") == "true")
auth = AuthService(secret=os.environ["JWT_SECRET"])
Deployment
uvicorn directly:
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
gunicorn + uvicorn workers:
gunicorn app:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
Docker:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir "flasio[full]"
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Performance
Flasio avoids overhead at every level:
- Static routes resolve in O(1)
- No automatic body parsing — body only read on demand
- No built-in request validation
- No ORM or reflection-heavy logic
- Middleware stack is a flat list, not a class hierarchy
Run the benchmark:
uvicorn examples.app:app --host 0.0.0.0 --port 8000 &
chmod +x benchmarks/test.sh && ./benchmarks/test.sh
FAQ
WebSocket support? Not yet — planned for a future release.
Can I use Pydantic? Yes — call await req.json() and pass it to your Pydantic model.
Sync handlers? Handlers must be async def. Wrap sync code with run_in_executor.
Multiple apps? Each Flasio() instance is independent. Mount them behind a reverse proxy.
License
MIT — see 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
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file flasio-0.1.1.tar.gz.
File metadata
- Download URL: flasio-0.1.1.tar.gz
- Upload date:
- Size: 29.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
464ad2af518e9bdeb8c61ff8dc94f7e3ed7269f0f3524cf55553dd7911cdd2d4
|
|
| MD5 |
477e2954948ce473cffc3c3133595117
|
|
| BLAKE2b-256 |
6168c765468329138c5bed16f9d7fcdc34729a7f5d91fd707fdaec70d452f684
|
File details
Details for the file flasio-0.1.1-py3-none-any.whl.
File metadata
- Download URL: flasio-0.1.1-py3-none-any.whl
- Upload date:
- Size: 27.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66aafc3d504c84b0ff7ed0963f964c61b9262e64ef9929ca43ffaae379c6e3fe
|
|
| MD5 |
0a9f5d5fcbdbbbac4d3a625ed4fc63b2
|
|
| BLAKE2b-256 |
c9dba5202f8d4921a6447f7442bbdc923b056f36c642f1449a294f5c1f0ef5cf
|