Skip to main content

Flask simplicity. Async power.

Project description

Flasio

███████╗██╗      █████╗ ███████╗██╗ ██████╗ 
██╔════╝██║     ██╔══██╗██╔════╝██║██╔═══██╗
█████╗  ██║     ███████║███████╗██║██║   ██║
██╔══╝  ██║     ██╔══██║╚════██║██║██║   ██║
██║     ███████╗██║  ██║███████║██║╚██████╔╝
╚═╝     ╚══════╝╚═╝  ╚═╝╚══════╝╚═╝ ╚═════╝

⚡ Flask simplicity. Async power.

Python ASGI License


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

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.


Built with ⚡ by the Flasio contributors

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

flasio-0.1.1.tar.gz (29.2 kB view details)

Uploaded Source

Built Distribution

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

flasio-0.1.1-py3-none-any.whl (27.2 kB view details)

Uploaded Python 3

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

Hashes for flasio-0.1.1.tar.gz
Algorithm Hash digest
SHA256 464ad2af518e9bdeb8c61ff8dc94f7e3ed7269f0f3524cf55553dd7911cdd2d4
MD5 477e2954948ce473cffc3c3133595117
BLAKE2b-256 6168c765468329138c5bed16f9d7fcdc34729a7f5d91fd707fdaec70d452f684

See more details on using hashes here.

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

Hashes for flasio-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 66aafc3d504c84b0ff7ed0963f964c61b9262e64ef9929ca43ffaae379c6e3fe
MD5 0a9f5d5fcbdbbbac4d3a625ed4fc63b2
BLAKE2b-256 c9dba5202f8d4921a6447f7442bbdc923b056f36c642f1449a294f5c1f0ef5cf

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