Skip to content

Repository files navigation

oberoon

Oberoon

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.

PyPI CI License Python 3.12+ Purpose: Learning GitHub Stars

Source Code · Changelog · PyPI

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/

About

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

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages