Metadata-Version: 2.4
Name: protorig
Version: 0.2.1
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Dist: anyio>=4.5.2
Requires-Dist: msgspec>=0.18.6
Requires-Dist: orjson>=3.10.15
License-File: LICENSE
Summary: Protorig is a small web framework supporting both ASGI and RSGI
Requires-Python: >=3.13
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# protorig

A small Python web framework with a Rust core, supporting both **ASGI** and **RSGI**. It takes inspiration from most of
the established web frameworks including sanic and FastAPI
## Features

- async only
- ASGI and RSGI compatible — run it under [uvicorn](https://www.uvicorn.org/), [granian](https://github.com/emmett-framework/granian), or any compliant server
- radix-tree router in rust based on go's httprouter
- Function-based routing with decorators
- Dependency injection via `Depends`
- Helpers for text, JSON, file, and redirect responses
- Customisable exception handlers
- Composable route groups with prefixes and mounting

## Requirements

- Python 3.13+
- orjson
- anyio
- msgspec
- A Rust toolchain (only needed to build from source)

## Installation

Pre-built wheels are published on [PyPI](https://pypi.org/project/protorig/), install with your preferred package manager:

```sh
pip install protorig
```

### Building from source

protorig is built with [maturin](https://www.maturin.rs/). To build and install from a checkout:

```sh
uv sync
uv run maturin develop
```

This compiles the Rust core (`protorig.core`) and installs the package into your environment.

## Quick start

```python
import uvicorn

from typing import Annotated

from protorig import responses
from protorig.application import Application
from protorig.depends import Depends
from protorig.routes import Routes

routes = Routes()


def get_db():
    return "clearly a database"


@routes.get("/user/:id")
async def get_user(id: int, db: Annotated[str, Depends(get_db)]):
    return responses.text(f"user({db}) -- {id}", 200)


app = Application(routes)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

## Routing

Create a `Routes` group and register handlers with the per-method decorators
(`get`, `post`, `put`, `delete`, `patch`, `options`, `head`) or the generic
`route` decorator:

```python
routes = Routes()

@routes.get("/health")
async def health():
    return responses.text("ok")

@routes.route(["GET", "POST"], "/echo")
async def echo(request: Request):
    return responses.json(await request.json())
```

### Path parameters

Path segments prefixed with `:` are captured and passed to the handler by name.
Annotate the parameter to have its value coerced to that type:

```python
@routes.get("/post/:slug/comment/:id")
async def comment(slug: str, id: int):
    ...
```

### Prefixes and mounting

A `Routes` group can be given a prefix, and groups can be composed by mounting
one into another — either directly or by import path:

```python
api = Routes(prefix="/api")

root = Routes()
root.mount(api)
root.mount("myapp.users:routes", prefix="/v1")
```

## Dependency injection

Declare dependencies with `Depends`. They are resolved per request, and the
resolved value is injected into your handler:

```python
from typing import Annotated
from protorig.depends import Depends

def get_db():
    return "db"

@routes.get("/items")
async def list_items(db: Annotated[str, Depends(get_db)]):
    ...
```

The current `Request` can be injected by annotating a parameter with the
`Request` type.

## Responses

The `protorig.responses` module provides helpers that return an `HTTPResponse`:

| Helper                                     | Purpose                                          |
|--------------------------------------------|--------------------------------------------------|
| `responses.text(body, status=200, ...)`    | Plain-text response                              |
| `responses.json(body, status=200, ...)`    | JSON response (serialised with `orjson`)         |
| `responses.file(path, ...)`                | File response with caching / conditional headers |
| `responses.redirect(url, status=302, ...)` | Redirect response                                |

## Exception handling

Register handlers for specific exception types with
`Application.exception_handler`. `HTTPException` is handled by default and
returns its `detail` and `status_code`:

```python
from protorig.exceptions import HTTPException

@app.exception_handler(HTTPException)
async def handle_http(request, exception: HTTPException):
    return responses.text(exception.detail, exception.status_code)
```

Unhandled exceptions are logged and produce a `500 Internal Server Error`.

## License

BSD-3-Clause
