Skip to main content

kyro

CI

Symfony-style attribute routing for Django: declare routes with decorators directly on your views and controllers instead of maintaining a long urls.py.

Status: pre-alpha, under active development.

Installation

pip install kyro-routing   # or: uv add kyro-routing

The package is imported as kyro. Add "kyro" to INSTALLED_APPS for the management commands and the system checks.

Quick start

# blog/views.py
from django.http import HttpResponse
from kyro import route

from blog.models import Post


@route("/hello/{name}", name="hello")
def hello(request, name):
    return HttpResponse(f"Hello {name}")


@route("/blog", name="blog_")
class BlogController:
    @route("", name="index", methods="GET")
    def index(self, request): ...

    @route("", name="create", methods="POST")
    def create(self, request): ...

    @route(r"/page/{page<\d+>?1}", name="list")  # before /{slug}, which would catch /page
    def list(self, request, page: int): ...

    @route("/{slug}", name="show")
    def show(self, request, post: Post):  # fetched by slug, 404 if missing
        ...
# project/urls.py
import kyro

urlpatterns = kyro.discover()  # imports the `views` module of every installed app

Routes are plain Django URL patterns: reverse("blog_show", kwargs={"slug": "hello"}) and {% url "blog_index" %} work as usual, and so do include() and namespaces.

Paths

Both syntaxes are accepted, even in the same path:

Syntax Meaning
<name>, <int:id> Django parameter and converter
{name} parameter (anything but /)
{id<\d+>} parameter with a regex requirement
{page?1} optional parameter with default "1"
{page?} optional parameter with default None

Trailing parameters with a default (inline or in defaults=) are optional: /blog/{page?1} matches /blog and /blog/2.

@route arguments

Argument
path the path, relative to the class prefix if any
name route name, appended to the class name prefix; generated when omitted
methods "GET" or ["GET", "POST"]; others get a 405. GET also allows HEAD
requirements {"id": r"\d+"}
defaults defaults of optional parameters and extra kwargs passed to the view
priority higher priority routes are matched first; otherwise declaration order

Several routes may share a path with different methods: kyro dispatches each request to the right view. The path is exempt from CSRF checks (csrf_exempt) or from LoginRequiredMiddleware (login_not_required) only when all its views are. @route can be stacked to give a view several routes.

What can be routed

  • Functions, sync or async. Put @route above other decorators, or below decorators that use functools.wraps. A route hidden by a decorator that does not is refused (ImproperlyConfigured): it would serve the view without that decorator.
  • Methods of any class: the class is instantiated for each request. A @route on the class is a prefix (path, name, requirements, defaults, methods) for its methods. Classes defined inside a function must be decorated (@route() for no prefix).
  • A class with __call__ and no routed method, or a Django View: the class route is the route of the class itself. The methods of a Django View cannot be routed: kyro would call them without View.dispatch(), skipping mixins, authentication and permissions.

Django REST Framework

Put @route on APIView and generic view classes, and let them handle the HTTP methods (no methods=): authentication, permissions, CSRF, error responses (400 to 405) and schema generation work as usual.

from rest_framework.generics import RetrieveUpdateAPIView


@route("/members/<uuid:pk>", name="member")
class MemberDetail(RetrieveUpdateAPIView):
    queryset = Member.objects.all()
    serializer_class = MemberSerializer
  • With methods=, kyro answers the other methods itself, with a plain Django 405, and schema generators still list them.
  • @route may be put above @api_view, not below it (@api_view does not use functools.wraps).
  • The methods of an APIView cannot be routed, and viewsets are not supported: route one view class per path.
  • kyro converts no parameter for view classes: use Django converters (<uuid:pk>).

Parameter conversion

Annotated view arguments are converted from the URL:

  • int, float, Decimal, bool, UUID, date, datetime, Enum: an invalid value is a 404.

  • Django models, fetched like Symfony's entity resolver:

    • {post} for post: Post: lookup by primary key;
    • {slug} for post: Post: lookup by fields named like URL parameters (the parameter is only passed to the view if its signature accepts it);
    • post: Annotated[Post, MapModel(slug="post_slug")]: explicit mapping.

    A missing object is a 404, or None when the annotation is optional (Post | None).

Debugging routes

With "kyro" in INSTALLED_APPS, manage.py debug_router lists every route of the project, kyro or not, in matching order:

$ python manage.py debug_router
 -------------- ---------- ------------------
  Name           Method     Path
 -------------- ---------- ------------------
  blog_index     GET|HEAD   /blog
  blog_create    POST       /blog
  blog_show      ANY        /blog/{slug}
 -------------- ---------- ------------------
  • debug_router blog_show shows one route in detail: Django routes, requirements, defaults, priority, view and the file and line where it is declared. A partial name shows the detail when it matches one route, the list of matches otherwise.
  • --show-views adds the view of each route to the list, --format json outputs JSON and --urlconf inspects another URLconf than ROOT_URLCONF.

manage.py router_match tells which route serves a request, method dispatch included:

$ python manage.py router_match /blog --method POST
 ------------- ---------- ------- --------------------
  Name          Method     Path    Result
 ------------- ---------- ------- --------------------
  blog_index    GET|HEAD   /blog   method not allowed
  blog_create   POST       /blog   match
 ------------- ---------- ------- --------------------

Route 'blog_create' matches.

It then shows the route in detail with the kwargs its view receives. Routes matching the path but not the method, or hidden by an earlier route (shadowed), are listed; -v 2 lists every route. The command fails when no route serves the request (404 or 405). --format json and --urlconf work as for debug_router.

System checks

With "kyro" in INSTALLED_APPS, manage.py check (and runserver) warns about:

Id Problem
kyro.W001 a route without parameters is served by an earlier route, for all or some of its methods (/blog/{slug} declared before /blog/new)
kyro.W002 a route name is shared with other routes, so reverse() only returns one of them
kyro.W003 routes declared with @route are not in the URLconf (kyro.discover() forgotten?)
kyro.W004 a parameter has a default but is not trailing, so it cannot be omitted

Silence one with SILENCED_SYSTEM_CHECKS = ["kyro.W003"].

Settings

  • KYRO_DISCOVER (default ["views"]): app submodules imported by kyro.discover().

kyro.discover(["blog"]) imports the given modules (for a package, its KYRO_DISCOVER submodules too) and returns only their routes, to mount them under a prefix: path("blog/", include(kyro.discover(["blog"]))).

Development

uv sync              # create .venv and install dependencies
uv run pytest        # run the test suite
uv run ruff check .  # lint
uv run ruff format . # format
uv run python -m mypy # type check (strict)

Releasing

  1. Set the version in src/kyro/__init__.py (__version__), and in CHANGELOG.md rename [Unreleased] to [x.y.z] - YYYY-MM-DD and update the links at the bottom.
  2. Optionally, try the release out on TestPyPI: Actions → Release → Run workflow.
  3. Commit, tag and push: git tag vx.y.z && git push origin main vx.y.z. The Release workflow publishes to TestPyPI, then PyPI.

Metadata

Release files for kyro-routing 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kyro-routing 0.1.1
File Size Uploaded Provenance Actions
kyro_routing-0.1.1.tar.gz 73.0 kB With provenance Details for kyro_routing-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for kyro-routing 0.1.1
File Interpreter ABI Platform Actions
kyro_routing-0.1.1-py3-none-any.whl Python 3 none any Details for kyro_routing-0.1.1-py3-none-any.whl

Total release size: 101.2 kB

Release files / kyro_routing-0.1.1.tar.gz

Download URL kyro_routing-0.1.1.tar.gz
Size 73.0 kB
Tags Source
SHA-256 checksum
How to use checksums
8436ac2dbb56a6f781f0121417f35a79eaa540d4031c361ff79d6977eed37cde
BLAKE2b-256 checksum
How to use checksums
3743481fce9abc89235271e29ef38b0813a435b733a0aa59373f68e7d08de0a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Release files / kyro_routing-0.1.1-py3-none-any.whl

Download URL kyro_routing-0.1.1-py3-none-any.whl
Size 28.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
98138c0cffda9b6380b46e1fc70cf1f7b3c596dd12246cfad49193a8d6edfa3b
BLAKE2b-256 checksum
How to use checksums
7ecde1fa195da9e52c7419f9050a470a27fc18a4c98637d03fd79f34f3bbce21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Security for kyro-routing 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for kyro_routing-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page