Routing
The openviper.routing package provides a fast, regex-backed URL router
with support for typed path parameters, HTTP method filtering, sub-routers
(blueprints), and per-router middleware.
Overview
Router is the central class. Register
handlers with method-specific decorators (@router.get, @router.post,
etc.) or with the generic @router.route decorator. Routers can be
composed hierarchically via include_router or the include() helper.
Path parameters are declared inside curly braces. An optional type specifier converts the value automatically:
Syntax |
Converter |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The router resolves routes by specificity: paths with more literal segments
are tried before paths with parameters, so /users/me beats
/users/{id:int}.
Key Classes
- class openviper.routing.router.Router(prefix='', middlewares=None, tags=None, namespace=None)
URL router.
- route(path, methods, name=None, middlewares=None)
Register a handler for path + methods. middlewares is an optional list of per-route ASGI middleware callables applied only to this route.
- get(path, name=None, middlewares=None)
- post(path, name=None, middlewares=None)
- put(path, name=None, middlewares=None)
- patch(path, name=None, middlewares=None)
- delete(path, name=None, middlewares=None)
- options(path, name=None, middlewares=None)
Convenience decorators for the respective HTTP methods.
- any(path, name=None, middlewares=None)
Register a handler that matches
GET,POST,PUT,PATCH,DELETE,HEAD, andOPTIONS.
- add(path, handler, methods=None, namespace=None, middlewares=None)
Register a handler programmatically (non-decorator form). When handler comes from
View.as_view()and methods is omitted, class-view methods are discovered the same way asView.register(...).
- include_router(router, prefix='', namespace=None)
Mount a sub-router as a live reference. Routes added to the sub-router later are automatically visible through this router. When namespace is given, all route names become
"namespace:route_name"in this router’s name index.
- resolve(method, path) tuple[Route, dict[str, str | int | float]]
Match method + path against registered routes. Returns the matched
Routeand a dict of extracted path parameters. RaisesNotFound,MethodNotAllowed, orPathSecurityErroron failure.
- url_for(name, **path_params) str
Reverse-generate a URL from a named route. path_params values must be
str,int, orfloat. Values containing null bytes,.., or/raiseValueError. Returns the path string with parameters filled in. RaisesKeyErrorif the name is not registered.
- routes -> list[Route]
All routes including sub-router routes, flattened and cached. Indices (dispatch, name, exact-match) are built lazily on first access and invalidated whenever routes or sub-routers change.
- class openviper.routing.router.Route
Dataclass representing a single route registration.
- middlewares: list[Middleware]
Per-route middleware stack.
- class openviper.routing.router.PathSecurityError
Raised when a request path contains disallowed security-sensitive patterns (null bytes, encoded slashes, or directory traversal).
Path Security
- openviper.routing.sanitize_request_path(path) str
Sanitize and normalize a request path before routing. Rejects null bytes, encoded slashes (
%2F,%5C%2F), directory traversal (..segments), and percent-encoded traversal sequences such as%2e%2e(encoded dots). Collapses consecutive slashes. RaisesPathSecurityErroron malicious input.
Path Compilation
- openviper.routing.router.compile_path(path) tuple[Pattern, dict]
Convert a path template (e.g.
/users/{id:int}) to a compiled regex and a dict of parameter converters. Cached with@lru_cache(maxsize=256).
View Inference Helpers
Security Constants
- openviper.routing.router.NULL_BYTE_RE
Compiled regex matching null bytes (
\\x00) in request paths.
- openviper.routing.router.TRAVERSAL_RE
Compiled regex matching directory traversal (
..) segments.
- openviper.routing.router.ENCODED_SLASH_RE
Compiled regex matching encoded slashes (
%2F,%5C%2F).
- openviper.routing.router.MULTI_SLASH_RE
Compiled regex matching two or more consecutive slashes.
- openviper.routing.router.PARAM_PLACEHOLDER_RE
Compiled regex matching
{name}or{name:type}placeholders.
- openviper.routing.router.ANY_PARAM_RE
Compiled regex matching any
{…}segment for validation.
- openviper.routing.router.VALID_PARAM_RE
Compiled regex validating path parameter names as Python identifiers.
- openviper.routing.router.ANNOTATION_CONVERTERS
Mapping from Python type annotations to converter names (e.g.
int→"int").
- openviper.routing.router.CONVERTERS
Mapping of converter names to
(regex, callable)pairs.
- openviper.routing.router.DYNAMIC
Sentinel key (
"__dynamic__") in the dispatch index for routes whose first segment is a parameter.
Type Aliases
- openviper.routing.router.Handler
Callable[..., Awaitable[Any]]- async handler signature.
- openviper.routing.router.Middleware
Callable[[Any, Any], Awaitable[Any]]- async middleware signature.
Example Usage
See also
Working projects that demonstrate routing patterns:
examples/flexible/ - decorator-based routing (
@app.get,@app.post)examples/ai_moderation_platform/ -
Routerclass, class-based views, typed path paramsexamples/ecommerce_clone/ - multi-router mounting at
/api
Basic Route Registration
from openviper.routing.router import Router
from openviper.http.request import Request
from openviper.http.response import JSONResponse
router = Router()
@router.get("/")
async def index(request: Request) -> JSONResponse:
return JSONResponse({"status": "ok"})
@router.get("/users/{user_id:int}")
async def get_user(request: Request, user_id: int) -> JSONResponse:
user = await User.objects.get(id=user_id)
return JSONResponse(user._to_dict())
@router.post("/users")
async def create_user(request: Request) -> JSONResponse:
data = await request.json()
user = await User.objects.create(**data)
return JSONResponse(user._to_dict(), status_code=201)
Named Routes and URL Reversal
@router.get("/posts/{post_id:int}", name="post-detail")
async def post_detail(request: Request, post_id: int) -> JSONResponse: ...
# Reverse the URL
url = router.url_for("post-detail", post_id=42) # "/posts/42"
# Slug-based route
@router.get("/blog/{slug:slug}", name="blog-post")
async def blog_post(request: Request, slug: str) -> JSONResponse: ...
url = router.url_for("blog-post", slug="my-first-post")
Non-Decorator Registration
async def my_handler(request: Request) -> JSONResponse:
return JSONResponse({"hello": "world"})
router.add("/hello", my_handler, methods=["GET", "POST"], namespace="hello")
Sub-Router / Blueprint Pattern
from openviper.routing.router import Router, include
api_v1 = Router(prefix="/api/v1")
blog_router = Router()
@blog_router.get("/posts")
async def list_posts(request: Request) -> JSONResponse: ...
@blog_router.get("/posts/{post_id:int}")
async def get_post(request: Request, post_id: int) -> JSONResponse: ...
api_v1.include_router(include(blog_router, prefix="/blog"))
# Routes now at /api/v1/blog/posts and /api/v1/blog/posts/{post_id:int}
Router-level Middleware
from openviper.middleware.ratelimit import RateLimitMiddleware
# Attach middleware to the entire sub-router
api_router = Router(prefix="/api", middlewares=[my_auth_middleware])
@api_router.get("/data")
async def get_data(request: Request) -> JSONResponse: ...
Per-Route Middleware
from openviper.middleware.ratelimit import rate_limit
@router.get(
"/expensive",
middlewares=[some_custom_middleware],
)
async def expensive_view(request: Request) -> JSONResponse: ...
Class-Based Views
Use View with the router. See HTTP - Requests, Responses & Views
for the full View API. Parameters declared after request on standard
HTTP handlers are appended to the registered path automatically.
from openviper.http.views import View
from openviper.http.response import JSONResponse
class PostView(View):
async def get(self, request: Request, post_id: int) -> JSONResponse:
post = await Post.objects.get(id=post_id)
return JSONResponse(post._to_dict())
async def put(self, request: Request, post_id: int) -> JSONResponse:
post = await Post.objects.get(id=post_id)
data = await request.json()
for k, v in data.items():
setattr(post, k, v)
await post.save()
return JSONResponse(post._to_dict())
async def delete(self, request: Request, post_id: int) -> JSONResponse:
post = await Post.objects.get(id=post_id)
await post.delete()
return JSONResponse({"deleted": True})
# Register all implemented HTTP methods automatically.
# These handlers are mounted at /posts/{post_id:int}.
PostView.register(router, "/posts", name="post-detail")
# Equivalent shorthand when methods= is omitted:
router.add("/posts", PostView.as_view(), namespace="post-detail")
Collection and detail routes may be generated from one view class when the method signatures differ:
class PostView(View):
async def post(self, request: Request) -> JSONResponse:
data = await request.json()
return JSONResponse(data, status_code=201)
async def get(self, request: Request, post_id: int) -> JSONResponse:
post = await Post.objects.get(id=post_id)
return JSONResponse(post._to_dict())
async def put(self, request: Request, post_id: int) -> JSONResponse:
data = await request.json()
post = await Post.objects.get(id=post_id)
for key, value in data.items():
setattr(post, key, value)
await post.save()
return JSONResponse(post._to_dict())
PostView.register(router, "/posts")
This registers POST /posts plus GET and PUT at
/posts/{post_id:int}. OPTIONS remains available at runtime for
class views, but it is omitted from generated OpenAPI operation lists.
Mounting in the Application
# settings.py or app setup
from openviper.routing.router import Router
from myapp.views import router as app_router
main_router = Router()
main_router.include_router(app_router)
# routes.py (used by OpenViper app discovery)
route_paths = [
("/", main_router),
]