Skip to main content

A lightweight ASGI web framework for Python, inspired by Starlette

Project description

Oberoon

purpose PyPI - Version

A lightweight ASGI web framework for Python, inspired by Starlette and FastAPI. Built from scratch as a learning project to understand the internals of modern async web frameworks.

Source code: https://github.com/Samandar-Komilov/oberoon

Features

  • Pure ASGI interface — works with Uvicorn, Hypercorn, Daphne
  • Async request handlers with automatic JSON serialization via msgspec
  • Request body validation with BaseModel and Annotated[type, Field(...)] constraints
  • Query parameter validation with type coercion — Annotated[int, Query(ge=1)]
  • Header parameter validation — Annotated[str, Header()] with underscore-to-hyphen mapping
  • Path parameters with type conversion ({id:int}, {name:str}, {filepath:path})
  • Method-based routing (@app.get, @app.post, etc.)
  • Nested routers with prefix mounting (app.include_router)
  • Exception handler registry (@app.exception_handler) with debug mode
  • Typed return annotations — auto-serialization for -> Book, -> list[Book], -> dict, -> None (204)
  • Zero magic — small codebase, easy to read and learn from

Installation

pip install oberoon

Quick Start

from typing import Annotated

from oberoon import Oberoon, BaseModel, Field, Query, Header, Request

app = Oberoon()


class Book(BaseModel):
    id: int
    title: str
    author: str


class CreateBook(BaseModel):
    title: Annotated[str, Field(min_length=1, max_length=200)]
    author: Annotated[str, Field(min_length=1, max_length=100)]


books = {1: Book(id=1, title="Dune", author="Frank Herbert")}
next_id = 2


@app.get("/books")
async def list_books(
    request: Request,
    page: Annotated[int, Query(ge=1)] = 1,
    limit: Annotated[int, Query(ge=1, le=50)] = 10,
) -> list[Book]:
    items = list(books.values())
    start = (page - 1) * limit
    return items[start : start + limit]


@app.get("/books/{book_id:int}")
async def get_book(request: Request, book_id: int) -> Book:
    from oberoon import HTTPException
    if book_id not in books:
        raise HTTPException(status_code=404, detail="Book not found")
    return books[book_id]


@app.post("/books")
async def create_book(
    request: Request,
    body: CreateBook,
    authorization: Annotated[str, Header()],
) -> Book:
    global next_id
    book = Book(id=next_id, title=body.title, author=body.author)
    books[next_id] = book
    next_id += 1
    return book


@app.delete("/books/{book_id:int}")
async def delete_book(request: Request, book_id: int) -> None:
    books.pop(book_id, None)

Run with any ASGI server:

uvicorn app:app --reload
# List with pagination
curl "localhost:8000/books?page=1&limit=5"

# Create (body + header validation)
curl -X POST localhost:8000/books \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer token" \
  -d '{"title": "1984", "author": "George Orwell"}'

# Validation error (422)
curl -X POST localhost:8000/books \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer token" \
  -d '{"title": "", "author": "X"}'

# Delete (204 No Content)
curl -X DELETE localhost:8000/books/1

Validation

oberoon uses msgspec for high-performance validation and serialization.

Request body

class CreateUser(BaseModel):
    name: Annotated[str, Field(min_length=1, max_length=100)]
    email: str
    age: Annotated[int, Field(ge=0, le=150)] = 0

Query parameters

@app.get("/search")
async def search(
    request: Request,
    q: Annotated[str, Query(min_length=1)],
    page: Annotated[int, Query(ge=1)] = 1,
) -> list[dict]:
    ...

Header parameters

@app.get("/protected")
async def protected(
    request: Request,
    authorization: Annotated[str, Header()],
    x_request_id: Annotated[str, Header()] = "",  # maps to x-request-id
) -> dict:
    ...

Error Handling

All errors return consistent JSON responses. Register custom handlers with @app.exception_handler:

app = Oberoon(debug=True)  # debug=True includes exception details in 500 responses

class RateLimitError(Exception):
    pass

@app.exception_handler(RateLimitError)
def handle_rate_limit(request, exc):
    return JSONResponse({"error": "Too many requests"}, status_code=429)

Default error responses:

Status Body
404 {"error": "Not Found"}
405 {"error": "Method Not Allowed"}
422 {"error": "Validation Error", "detail": [...]}
500 {"error": "Internal Server Error"}

Routers

from oberoon import Router

api = Router(prefix="/api")
books = Router(prefix="/books")

@books.get("/")
async def list_books(request: Request) -> list[dict]:
    return [{"title": "Dune"}]

api.include_router(books)    # /api/books/
app.include_router(api)

Roadmap

  • ASGI core with lifespan support
  • Regex-based routing with path parameter converters
  • Structured logging
  • Attachable routers with prefix nesting
  • msgspec-powered request/response serialization
  • Body, query, and header validation with constraints
  • Exception handler registry with debug mode
  • Middleware support
  • WebSocket support
  • Static files and templates
  • OpenAPI schema generation

Development

git clone https://github.com/Samandar-Komilov/oberoon.git
cd oberoon
pip install -e ".[dev]"
pytest tests/

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

oberoon-0.3.0.tar.gz (70.1 kB view details)

Uploaded Source

Built Distribution

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

oberoon-0.3.0-py3-none-any.whl (15.6 kB view details)

Uploaded Python 3

File details

Details for the file oberoon-0.3.0.tar.gz.

File metadata

  • Download URL: oberoon-0.3.0.tar.gz
  • Upload date:
  • Size: 70.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.0 {"installer":{"name":"uv","version":"0.11.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for oberoon-0.3.0.tar.gz
Algorithm Hash digest
SHA256 88bbdb8194ee97adf2aed1791eb4feacdb513db5094002ef28d823381864b6d1
MD5 280f185ccee0dbfb0131c513e68172c9
BLAKE2b-256 33019211518d02e88fd9c483622583a4d6cebc1e8d95834eed8925e86414dfba

See more details on using hashes here.

File details

Details for the file oberoon-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: oberoon-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 15.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.0 {"installer":{"name":"uv","version":"0.11.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for oberoon-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 46e6b7006c7b0800374260bdb1a24e0387869e3ce428152a716a795863137f1a
MD5 b6a197fb4aed28f2cb3089c9cc01bb4c
BLAKE2b-256 05515010965951fb638e605b4e025635e75dea80a80f8cf0d352ee8835d46324

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