Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Changelog

All notable changes to OxyRoute are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - unreleased

### Removed (breaking)

- The optional ASGI 3.0 compatibility bridge (`oxyroute.asgi`) was removed. `App` is no
longer an ASGI callable; `App.__call__`, `App._asgi3`, the `asgi_to_rsgi` helper and the
`WebSocket` helper class are gone. Run OxyRoute exclusively under
``granian --interface rsgi``.
- The ASGI-based `@app.websocket(path)` decorator and the
`App._handle_asgi_websocket` plumbing were removed alongside the bridge. Native RSGI
WebSocket support will be reintroduced in a follow-up release.

### Migration

- If you ran `uvicorn` or `granian --interface asgi` against an OxyRoute app, switch to
`granian --interface rsgi`. RSGI is now the only supported transport.
- For unit tests that drove the app via `httpx.ASGITransport(app=app)`, import the
test-only shim from the test tree:

```python
from tests._rsgi_test_transport import asgi_test_app
transport = httpx.ASGITransport(app=asgi_test_app(app))
```

The shim is **only** built for the test suite; production code must not import it.

## [0.2.0] - 2026-04

Initial public release. See `git log v0.2.0` for details.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "oxyroute"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
description = "RSGI web framework: Rust hot path, Python handlers"
license = "MIT"
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
- **JWT (HMAC)** verification on the Rust path before your handler runs (`require_jwt`, HS256/384/512)
- **Optional** `GET /openapi.json` with a minimal OpenAPI-style document
- **Dependencies**: linear list of named factories (`Depends`, sync or async) passed as kwargs
- **Optional ASGI 3** bridge: `async def __call__(scope, receive, send)` for servers that speak ASGI (see [docs/asgi.md](docs/asgi.md))
- Native extension wheel (abi3) for **Python ≥ 3.10**

Full documentation: **[docs/index.md](docs/index.md)**
Expand Down Expand Up @@ -73,11 +72,11 @@ granian --interface rsgi examples.rsgi_app:app

Per-worker setup (`__rsgi_init__`) is shown in [examples/rsgi_lifespan_app.py](examples/rsgi_lifespan_app.py) and [docs/rsgi.md](docs/rsgi.md#lifespan-optional).

ASGI and other servers are covered in [docs/asgi.md](docs/asgi.md).
OxyRoute v0.3.0 supports **only** Granian RSGI; the legacy ASGI bridge (`uvicorn` / `granian --interface asgi`) was removed.

## Project layout

- `oxyroute/` — Python package (`App`, `Depends`, optional ASGI bridge)
- `oxyroute/` — Python package (`App`, `Depends`)
- `src/` — Rust extension (`_oxyroute`, routing, dispatch, JWT helpers)
- `docs/` — detailed English documentation
- `tests/` — pytest suite (run from a temp directory or an installed wheel so the source tree does not shadow the package; see [docs/development.md](docs/development.md))
Expand Down
30 changes: 0 additions & 30 deletions docs/asgi.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ OxyRoute supports a **linear** list of **named** dependency factories. At reques

### Request context (optional)

If a factory’s signature includes a parameter named `request`, the extension passes a **dict** (once per request, shared) with string keys: `method`, `path`, `query_string`, and `headers` (a flat `str` → `str` map, when the underlying RSGI scope exposes headers—see the ASGI bridge in `oxyroute.asgi`). Factories that do **not** declare `request` are still called with **no** extra arguments when they have no prior dependencies, preserving older behavior.
If a factory’s signature includes a parameter named `request`, the extension passes a **dict** (once per request, shared) with string keys: `method`, `path`, `query_string`, and `headers` (a flat `str` → `str` map, when the underlying RSGI scope exposes headers). Factories that do **not** declare `request` are still called with **no** extra arguments when they have no prior dependencies, preserving older behavior.

## Declaring on a route

Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pip install "oxyroute[dev]" # in your dev env, from source after maturin develo

## Granian RSGI (end-to-end)

`tests/test_granian_e2e.py` starts a real **Granian** subprocess with `--interface rsgi`, sends HTTP requests with **httpx**, then stops the server. It is part of the normal **pytest** run when `granian` is installed (`oxyroute[dev]` includes it). The same file runs in **CI** on every matrix combination (Linux, macOS, Windows), so the native RSGI path is exercised against a real server, not only the ASGI in-process transport.
`tests/test_granian_e2e.py` starts a real **Granian** subprocess with `--interface rsgi`, sends HTTP requests with **httpx**, then stops the server. It is part of the normal **pytest** run when `granian` is installed (`oxyroute[dev]` includes it). The same file runs in **CI** on every matrix combination (Linux, macOS, Windows), so the native RSGI path is exercised against a real server, not only the in-process httpx test transport.

## Continuous integration

Expand Down
8 changes: 4 additions & 4 deletions docs/feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- Маршрутизация по методам и путям (`matchit`), path/query/json/body, **405 / Allow**, **HEAD** на GET, **OPTIONS**.
- **JWT** (HS/RS/… через `jsonwebtoken`), iss/aud/leeway, cookie, **зависимости** с `request`, линейный порядок, `freeze`.
- **Response** / dict с заголовками и cookies, частичный **OpenAPI**, Pydantic/schema для тела.
- Один **pre-route middleware** (`set_middleware`), **ASGI мост** только для `http` ([asgi](asgi.md)).
- Один **pre-route middleware** (`set_middleware`).
- CI, PyPI, E2E Granian RSGI.

Ниже — то, чего **нет** или что **слабо** относительно «больших» фреймворков.
Expand All @@ -22,10 +22,10 @@

| Тема | Зазор | Комментарий |
|------|--------|-------------|
| **WebSockets** | Частично (ASGI spike) | Есть `@app.websocket(...)` в ASGI bridge (см. [websocket.md](websocket.md)); RSGI/Rust-native WS path пока не реализован. |
| **SSE / длинный стрим ответа** | Частично | Есть `send_sse` (см. [sse.md](sse.md)); инкрементальный стрим зависит от `response_stream` в серверном протоколе, иначе fallback в буферизованный ответ. |
| **WebSockets** | Не реализованы | Поддержка ASGI WS была удалена в v0.3.0; native RSGI WebSocket — следующая работа. |
| **SSE / длинный стрим ответа** | Частично | Есть `send_sse` (см. [sse.md](sse.md)); инкрементальный стрим использует `response_stream` Granian RSGI. |
| **HTTP/2 push, trailers** | Не в фокусе | Обычно на стороне сервера; фреймворк редко экспонирует. |
| **ASGI: lifespan, `websocket`, background** | Частично | Мост ASGI — **только `http`**; нет полноценного ASGI-приложения с lifecycle из спеки. |
| **ASGI совместимость** | Удалена в v0.3.0 | Поддерживается только RSGI (Granian `--interface rsgi`). |

---

Expand Down
1 change: 0 additions & 1 deletion docs/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,3 @@ If you see `HTTP/1.1`, the fallback happened at server/proxy/TLS level, not in O
## See also

- [RSGI and Granian](rsgi.md)
- [ASGI bridge](asgi.md)
2 changes: 0 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,9 @@ Granian still invokes a Python `App` object; the “win” is doing routing, bod
| [CSRF](csrf.md) | Double-submit, `apply_csrf`, `csrf_layer` + CORS |
| [JWT](jwt.md) | `require_jwt`, HS* / RSA / EC PEM, `decode_jwt_hs` (HS* tests) |
| [SSE](sse.md) | `send_sse`, event framing, streaming caveats |
| [WebSocket](websocket.md) | ASGI spike API and current limitations |
| [HTTP/2 with Granian](http2.md) | Transport guarantees vs server/proxy responsibilities |
| [Dependencies](dependencies.md) | `Depends`, `dependencies=[...]`, `freeze` |
| [OpenAPI](openapi.md) | `openapi.json` route, title, `openapi_json()` |
| [ASGI bridge](asgi.md) | Optional `__call__` for ASGI 3, limitations |
| [Development](development.md) | Tests, CI, PyPI releases (tag `v*`), clippy, pytest |
| [Branching and PRs](development-workflow.md) | `dev` as base, issue branches, `Closes #N`, no mixing code with `ISSUE_BACKLOG` in one commit |
| [Feature gaps (research)](feature.md) | What is missing vs a “full” HTTP framework (multipart, WS, sub-routers, etc.) — Russian |
Expand Down
4 changes: 0 additions & 4 deletions docs/rsgi.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ app = create_app()
- [`examples/rsgi_app.py`](../examples/rsgi_app.py) — minimal RSGI app
- [`examples/rsgi_lifespan_app.py`](../examples/rsgi_lifespan_app.py) — subclass with `__rsgi_init__` / `__rsgi_del__` and `ready_at` used from handlers

## When to use ASGI instead

If your host only speaks **ASGI 3.0** (for example `uvicorn` with stock ASGI), you can use the optional ASGI bridge: `App` is also a callable `async def __call__(scope, receive, send)`. See [asgi.md](asgi.md) for trade-offs and limitations.

## See also

- [Handlers](handlers.md) — what the Rust core passes into your functions
Expand Down
4 changes: 2 additions & 2 deletions docs/sse.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ async def events(protocol):

## Streaming behavior

- On RSGI protocols exposing `response_stream`, chunks are written incrementally.
- On transports without streaming support (e.g. current ASGI bridge test path), OxyRoute falls back to one buffered `response_str` body with SSE framing.
- On RSGI protocols exposing `response_stream` (Granian RSGI), chunks are written incrementally.
- On transports without streaming support (e.g. the in-process httpx test transport), OxyRoute falls back to one buffered `response_str` body with SSE framing.

## Caveats

Expand Down
49 changes: 0 additions & 49 deletions docs/websocket.md

This file was deleted.

2 changes: 1 addition & 1 deletion oxyroute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
"send_sse",
"sse_done",
]
__version__ = "0.2.0"
__version__ = "0.3.0"
40 changes: 3 additions & 37 deletions oxyroute/app.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from __future__ import annotations

import asyncio
import inspect
import json
from collections.abc import Callable, Mapping
from types import SimpleNamespace
from typing import Any, TypeVar

from . import _oxyroute
from .asgi import build_asgi_caller
from .router import APIRouter, join_path

F = TypeVar("F", bound=Callable[..., Any])
Expand Down Expand Up @@ -37,7 +35,8 @@ def Depends(call: Callable[..., Any]) -> _oxyroute.PyDepends:
class App:
"""
Application object: pass a module instance to Granian, e.g.
``granian app:app --interface rsgi`` or for ASGI ``granian app:app --interface asgi``.
``granian app:app --interface rsgi``. RSGI is the only supported transport;
the legacy ASGI bridge was removed in v0.3.0.

``state`` is an empty ``types.SimpleNamespace`` for per-process data; set fields in
``__rsgi_init__`` or a factory, or on a subclass. In-memory data is not shared across
Expand All @@ -48,10 +47,8 @@ def __init__(self, title: str = "OxyRoute", *, include_openapi: bool = True) ->
self._app = _oxyroute.App(include_openapi=include_openapi)
self._app.set_openapi_title(title)
self.title = title
self._websocket_routes: dict[str, Callable[..., Any]] = {}
# Per-process mutable bag for ``__rsgi_init__`` / factory setup (DB pool, clients, …).
self.state: SimpleNamespace = SimpleNamespace()
self._asgi3: Callable[..., Any] = build_asgi_caller(self)

def freeze(self) -> None:
"""After ``freeze()``, no more route registration (matches Rust app state)."""
Expand Down Expand Up @@ -299,34 +296,6 @@ def options(
jwt_cookie=jwt_cookie,
)

def websocket(self, path: str) -> Callable[[F], F]:
"""
Register an ASGI websocket handler for exact `path`.

This route family is available on the optional ASGI bridge (`App.__call__`), not
on the RSGI request path.
"""

def wrap(handler: F) -> F:
self._websocket_routes[path] = handler
return handler

return wrap

async def _handle_asgi_websocket(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
path = str(scope.get("path", "/") or "/")
handler = self._websocket_routes.get(path)
if handler is None:
await send({"type": "websocket.close", "code": 1000})
return
from .asgi import WebSocket

ws = WebSocket(receive, send)
if inspect.iscoroutinefunction(handler):
await handler(ws)
return
await asyncio.get_running_loop().run_in_executor(None, lambda: handler(ws))

def _route(
self,
method: str,
Expand Down Expand Up @@ -393,11 +362,8 @@ async def __rsgi__(self, scope: Any, protocol: Any) -> None:
return await self._app.handle_rsgi(scope, protocol)

def handle_rsgi(self, scope: Any, protocol: Any) -> Any:
"""Forward to the native app (used by the ASGI bridge)."""
"""Forward to the native ``handle_rsgi`` coroutine."""
return self._app.handle_rsgi(scope, protocol)

async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
return await self._asgi3(scope, receive, send)

def openapi_json(self) -> str:
return self._app.openapi_json()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "oxyroute"
version = "0.2.0"
version = "0.3.0"
description = "RSGI-first web framework: routing, JSON, and JWT in Rust (PyO3), Python handlers"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl AppState {
pub fn new() -> Self {
let openapi = serde_json::json!({
"openapi": "3.0.0",
"info": { "title": "OxyRoute", "version": "0.2.0" },
"info": { "title": "OxyRoute", "version": "0.3.0" },
"paths": {}
});
Self {
Expand Down
Loading
Loading