kyro-routing 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kyro_routing-0.1.0/.github/workflows/ci.yml +60 -0
- kyro_routing-0.1.0/.github/workflows/release.yml +68 -0
- kyro_routing-0.1.0/.gitignore +17 -0
- kyro_routing-0.1.0/.python-version +1 -0
- kyro_routing-0.1.0/CHANGELOG.md +37 -0
- kyro_routing-0.1.0/LICENSE +21 -0
- kyro_routing-0.1.0/PKG-INFO +222 -0
- kyro_routing-0.1.0/README.md +190 -0
- kyro_routing-0.1.0/pyproject.toml +80 -0
- kyro_routing-0.1.0/src/kyro/__init__.py +11 -0
- kyro_routing-0.1.0/src/kyro/apps.py +9 -0
- kyro_routing-0.1.0/src/kyro/checks.py +132 -0
- kyro_routing-0.1.0/src/kyro/dispatch.py +72 -0
- kyro_routing-0.1.0/src/kyro/inspection.py +349 -0
- kyro_routing-0.1.0/src/kyro/management/__init__.py +0 -0
- kyro_routing-0.1.0/src/kyro/management/commands/__init__.py +0 -0
- kyro_routing-0.1.0/src/kyro/management/commands/debug_router.py +52 -0
- kyro_routing-0.1.0/src/kyro/management/commands/router_match.py +53 -0
- kyro_routing-0.1.0/src/kyro/management/output.py +53 -0
- kyro_routing-0.1.0/src/kyro/params.py +261 -0
- kyro_routing-0.1.0/src/kyro/py.typed +0 -0
- kyro_routing-0.1.0/src/kyro/router.py +392 -0
- kyro_routing-0.1.0/src/kyro/syntax.py +240 -0
- kyro_routing-0.1.0/tests/__init__.py +0 -0
- kyro_routing-0.1.0/tests/conftest.py +34 -0
- kyro_routing-0.1.0/tests/otherapp/__init__.py +3 -0
- kyro_routing-0.1.0/tests/otherapp/views.py +8 -0
- kyro_routing-0.1.0/tests/settings.py +15 -0
- kyro_routing-0.1.0/tests/test_checks.py +180 -0
- kyro_routing-0.1.0/tests/test_debug_router.py +108 -0
- kyro_routing-0.1.0/tests/test_discovery.py +53 -0
- kyro_routing-0.1.0/tests/test_params.py +207 -0
- kyro_routing-0.1.0/tests/test_router_match.py +160 -0
- kyro_routing-0.1.0/tests/test_routing.py +305 -0
- kyro_routing-0.1.0/tests/test_smoke.py +9 -0
- kyro_routing-0.1.0/tests/test_syntax.py +100 -0
- kyro_routing-0.1.0/tests/testapp/__init__.py +0 -0
- kyro_routing-0.1.0/tests/testapp/models.py +9 -0
- kyro_routing-0.1.0/tests/testapp/views.py +62 -0
- kyro_routing-0.1.0/tests/urls.py +3 -0
- kyro_routing-0.1.0/tests/urls_debug.py +15 -0
- kyro_routing-0.1.0/uv.lock +679 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v5
|
|
20
|
+
- uses: astral-sh/setup-uv@v6
|
|
21
|
+
- run: uv sync --locked
|
|
22
|
+
- run: uv run ruff check .
|
|
23
|
+
- run: uv run ruff format --check .
|
|
24
|
+
- run: uv run python -m mypy
|
|
25
|
+
|
|
26
|
+
test:
|
|
27
|
+
name: test (Python ${{ matrix.python }}, Django ${{ matrix.django }})
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
# Lowest and highest Python supported by each Django version.
|
|
33
|
+
include:
|
|
34
|
+
- { python: "3.10", django: "4.2" }
|
|
35
|
+
- { python: "3.12", django: "4.2" }
|
|
36
|
+
- { python: "3.10", django: "5.2" }
|
|
37
|
+
- { python: "3.13", django: "5.2" }
|
|
38
|
+
- { python: "3.12", django: "6.0" }
|
|
39
|
+
- { python: "3.14", django: "6.0" }
|
|
40
|
+
- { python: "3.12", django: "6.1" }
|
|
41
|
+
- { python: "3.14", django: "6.1" }
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v5
|
|
44
|
+
- uses: astral-sh/setup-uv@v6
|
|
45
|
+
with:
|
|
46
|
+
python-version: ${{ matrix.python }}
|
|
47
|
+
- run: uv sync --locked
|
|
48
|
+
- run: uv pip install "django~=${{ matrix.django }}.0"
|
|
49
|
+
- run: uv run --no-sync pytest
|
|
50
|
+
|
|
51
|
+
build:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v5
|
|
55
|
+
- uses: astral-sh/setup-uv@v6
|
|
56
|
+
- run: uv build
|
|
57
|
+
- uses: actions/upload-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# A version tag (v0.1.0) publishes to TestPyPI, then PyPI.
|
|
4
|
+
# A manual run publishes to TestPyPI only, to try a release out.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v[0-9]*"]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
|
+
- uses: astral-sh/setup-uv@v6
|
|
19
|
+
- run: uv build
|
|
20
|
+
- name: Check that the tag matches the package version
|
|
21
|
+
if: github.ref_type == 'tag'
|
|
22
|
+
run: |
|
|
23
|
+
wheel="dist/kyro_routing-${GITHUB_REF_NAME#v}-py3-none-any.whl"
|
|
24
|
+
if [ ! -f "$wheel" ]; then
|
|
25
|
+
echo "::error::Tag $GITHUB_REF_NAME does not match the built package: $(ls dist)"
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
- name: Check that the wheel installs and imports
|
|
29
|
+
run: uv run --isolated --no-project --with dist/*.whl python -c "import kyro.checks, kyro; print(kyro.__version__)"
|
|
30
|
+
- uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
testpypi:
|
|
36
|
+
needs: build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
environment:
|
|
39
|
+
name: testpypi
|
|
40
|
+
url: https://test.pypi.org/p/kyro-routing
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
49
|
+
with:
|
|
50
|
+
repository-url: https://test.pypi.org/legacy/
|
|
51
|
+
# A version already tried out with a manual run is kept as is.
|
|
52
|
+
skip-existing: true
|
|
53
|
+
|
|
54
|
+
pypi:
|
|
55
|
+
needs: testpypi
|
|
56
|
+
if: github.ref_type == 'tag'
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
environment:
|
|
59
|
+
name: pypi
|
|
60
|
+
url: https://pypi.org/p/kyro-routing
|
|
61
|
+
permissions:
|
|
62
|
+
id-token: write
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/download-artifact@v4
|
|
65
|
+
with:
|
|
66
|
+
name: dist
|
|
67
|
+
path: dist/
|
|
68
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
6
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
First release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `@route` decorator for functions (sync and async), methods of any class, classes with
|
|
17
|
+
`__call__` and Django class-based views. On a class with routed methods, the class route
|
|
18
|
+
is a prefix for path, name, requirements, defaults and methods.
|
|
19
|
+
- Paths in Django syntax (`<int:id>`) and Symfony syntax (`{id}`, `{id<\d+>}`, `{page?1}`,
|
|
20
|
+
`{page?}`), mixed freely. Trailing parameters with a default are optional.
|
|
21
|
+
- Route options: `name` (generated when omitted), `methods`, `requirements`, `defaults` and
|
|
22
|
+
`priority`. Several routes can share a path with different methods; other methods get a
|
|
23
|
+
405 with an `Allow` header.
|
|
24
|
+
- Parameter conversion from annotations: `int`, `float`, `Decimal`, `bool`, `UUID`, `date`,
|
|
25
|
+
`datetime` and `Enum`, plus Django models looked up by primary key, by field or with
|
|
26
|
+
`MapModel`. Invalid values and missing objects are a 404.
|
|
27
|
+
- `kyro.discover()` to build the URL patterns from the `views` modules of the installed
|
|
28
|
+
apps, or from given modules; `Router` for separate sets of routes.
|
|
29
|
+
- `debug_router` management command, listing the routes of the project or showing one in
|
|
30
|
+
detail.
|
|
31
|
+
- `router_match` management command, telling which route serves a request.
|
|
32
|
+
- System checks: shadowed routes (`kyro.W001`), shared route names (`kyro.W002`), routes
|
|
33
|
+
missing from the URLconf (`kyro.W003`) and ignored defaults (`kyro.W004`).
|
|
34
|
+
- Support for Python 3.10 to 3.14 and Django 4.2, 5.2, 6.0 and 6.1.
|
|
35
|
+
|
|
36
|
+
[Unreleased]: https://github.com/mistermerlin/kyro-routing/compare/v0.1.0...HEAD
|
|
37
|
+
[0.1.0]: https://github.com/mistermerlin/kyro-routing/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aziz Ouattara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kyro-routing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Symfony-style attribute routing for Django: declare routes with decorators on views and controllers.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mistermerlin/kyro-routing
|
|
6
|
+
Project-URL: Repository, https://github.com/mistermerlin/kyro-routing.git
|
|
7
|
+
Project-URL: Issues, https://github.com/mistermerlin/kyro-routing/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/mistermerlin/kyro-routing/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Aziz Ouattara <datoulobaben@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: decorator,django,router,routing,symfony,urls
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: Django :: 4.2
|
|
16
|
+
Classifier: Framework :: Django :: 5.2
|
|
17
|
+
Classifier: Framework :: Django :: 6.0
|
|
18
|
+
Classifier: Framework :: Django :: 6.1
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: django>=4.2
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# kyro
|
|
34
|
+
|
|
35
|
+
[](https://github.com/mistermerlin/kyro-routing/actions/workflows/ci.yml)
|
|
36
|
+
|
|
37
|
+
Symfony-style attribute routing for Django: declare routes with decorators directly on
|
|
38
|
+
your views and controllers instead of maintaining a long `urls.py`.
|
|
39
|
+
|
|
40
|
+
> Status: pre-alpha, under active development.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install kyro-routing # or: uv add kyro-routing
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The package is imported as `kyro`. Add `"kyro"` to `INSTALLED_APPS` for the management
|
|
49
|
+
commands and the system checks.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# blog/views.py
|
|
55
|
+
from django.http import HttpResponse
|
|
56
|
+
from kyro import route
|
|
57
|
+
|
|
58
|
+
from blog.models import Post
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@route("/hello/{name}", name="hello")
|
|
62
|
+
def hello(request, name):
|
|
63
|
+
return HttpResponse(f"Hello {name}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@route("/blog", name="blog_")
|
|
67
|
+
class BlogController:
|
|
68
|
+
@route("", name="index", methods="GET")
|
|
69
|
+
def index(self, request): ...
|
|
70
|
+
|
|
71
|
+
@route("", name="create", methods="POST")
|
|
72
|
+
def create(self, request): ...
|
|
73
|
+
|
|
74
|
+
@route(r"/page/{page<\d+>?1}", name="list") # before /{slug}, which would catch /page
|
|
75
|
+
def list(self, request, page: int): ...
|
|
76
|
+
|
|
77
|
+
@route("/{slug}", name="show")
|
|
78
|
+
def show(self, request, post: Post): # fetched by slug, 404 if missing
|
|
79
|
+
...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# project/urls.py
|
|
84
|
+
import kyro
|
|
85
|
+
|
|
86
|
+
urlpatterns = kyro.discover() # imports the `views` module of every installed app
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Routes are plain Django URL patterns: `reverse("blog_show", kwargs={"slug": "hello"})` and
|
|
90
|
+
`{% url "blog_index" %}` work as usual, and so do `include()` and namespaces.
|
|
91
|
+
|
|
92
|
+
## Paths
|
|
93
|
+
|
|
94
|
+
Both syntaxes are accepted, even in the same path:
|
|
95
|
+
|
|
96
|
+
| Syntax | Meaning |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `<name>`, `<int:id>` | Django parameter and converter |
|
|
99
|
+
| `{name}` | parameter (anything but `/`) |
|
|
100
|
+
| `{id<\d+>}` | parameter with a regex requirement |
|
|
101
|
+
| `{page?1}` | optional parameter with default `"1"` |
|
|
102
|
+
| `{page?}` | optional parameter with default `None` |
|
|
103
|
+
|
|
104
|
+
Trailing parameters with a default (inline or in `defaults=`) are optional:
|
|
105
|
+
`/blog/{page?1}` matches `/blog` and `/blog/2`.
|
|
106
|
+
|
|
107
|
+
## `@route` arguments
|
|
108
|
+
|
|
109
|
+
| Argument | |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `path` | the path, relative to the class prefix if any |
|
|
112
|
+
| `name` | route name, appended to the class `name` prefix; generated when omitted |
|
|
113
|
+
| `methods` | `"GET"` or `["GET", "POST"]`; others get a 405. `GET` also allows `HEAD` |
|
|
114
|
+
| `requirements` | `{"id": r"\d+"}` |
|
|
115
|
+
| `defaults` | defaults of optional parameters and extra kwargs passed to the view |
|
|
116
|
+
| `priority` | higher priority routes are matched first; otherwise declaration order |
|
|
117
|
+
|
|
118
|
+
Several routes may share a path with different `methods`: kyro dispatches each request to
|
|
119
|
+
the right view. `@route` can be stacked to give a view several routes.
|
|
120
|
+
|
|
121
|
+
## What can be routed
|
|
122
|
+
|
|
123
|
+
- Functions, sync or async. Put `@route` above or below other decorators.
|
|
124
|
+
- Methods of any class: the class is instantiated for each request. A `@route` on the class
|
|
125
|
+
is a prefix (path, name, requirements, defaults, methods) for its methods. Classes defined
|
|
126
|
+
inside a function must be decorated (`@route()` for no prefix).
|
|
127
|
+
- A class with `__call__` and no routed method, or a Django `View`: the class route is the
|
|
128
|
+
route of the class itself.
|
|
129
|
+
|
|
130
|
+
## Parameter conversion
|
|
131
|
+
|
|
132
|
+
Annotated view arguments are converted from the URL:
|
|
133
|
+
|
|
134
|
+
- `int`, `float`, `Decimal`, `bool`, `UUID`, `date`, `datetime`, `Enum`: an invalid value
|
|
135
|
+
is a 404.
|
|
136
|
+
- Django models, fetched like Symfony's entity resolver:
|
|
137
|
+
- `{post}` for `post: Post`: lookup by primary key;
|
|
138
|
+
- `{slug}` for `post: Post`: lookup by fields named like URL parameters (the parameter is
|
|
139
|
+
only passed to the view if its signature accepts it);
|
|
140
|
+
- `post: Annotated[Post, MapModel(slug="post_slug")]`: explicit mapping.
|
|
141
|
+
|
|
142
|
+
A missing object is a 404, or `None` when the annotation is optional (`Post | None`).
|
|
143
|
+
|
|
144
|
+
## Debugging routes
|
|
145
|
+
|
|
146
|
+
With `"kyro"` in `INSTALLED_APPS`, `manage.py debug_router` lists every route of the
|
|
147
|
+
project, kyro or not, in matching order:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
$ python manage.py debug_router
|
|
151
|
+
-------------- ---------- ------------------
|
|
152
|
+
Name Method Path
|
|
153
|
+
-------------- ---------- ------------------
|
|
154
|
+
blog_index GET|HEAD /blog
|
|
155
|
+
blog_create POST /blog
|
|
156
|
+
blog_show ANY /blog/{slug}
|
|
157
|
+
-------------- ---------- ------------------
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- `debug_router blog_show` shows one route in detail: Django routes, requirements,
|
|
161
|
+
defaults, priority, view and the file and line where it is declared. A partial name shows
|
|
162
|
+
the detail when it matches one route, the list of matches otherwise.
|
|
163
|
+
- `--show-views` adds the view of each route to the list, `--format json` outputs JSON and
|
|
164
|
+
`--urlconf` inspects another URLconf than `ROOT_URLCONF`.
|
|
165
|
+
|
|
166
|
+
`manage.py router_match` tells which route serves a request, method dispatch included:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
$ python manage.py router_match /blog --method POST
|
|
170
|
+
------------- ---------- ------- --------------------
|
|
171
|
+
Name Method Path Result
|
|
172
|
+
------------- ---------- ------- --------------------
|
|
173
|
+
blog_index GET|HEAD /blog method not allowed
|
|
174
|
+
blog_create POST /blog match
|
|
175
|
+
------------- ---------- ------- --------------------
|
|
176
|
+
|
|
177
|
+
Route 'blog_create' matches.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
It then shows the route in detail with the kwargs its view receives. Routes matching the
|
|
181
|
+
path but not the method, or hidden by an earlier route (`shadowed`), are listed; `-v 2`
|
|
182
|
+
lists every route. The command fails when no route serves the request (404 or 405).
|
|
183
|
+
`--format json` and `--urlconf` work as for `debug_router`.
|
|
184
|
+
|
|
185
|
+
## System checks
|
|
186
|
+
|
|
187
|
+
With `"kyro"` in `INSTALLED_APPS`, `manage.py check` (and `runserver`) warns about:
|
|
188
|
+
|
|
189
|
+
| Id | Problem |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `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`) |
|
|
192
|
+
| `kyro.W002` | a route name is shared with other routes, so `reverse()` only returns one of them |
|
|
193
|
+
| `kyro.W003` | routes declared with `@route` are not in the URLconf (`kyro.discover()` forgotten?) |
|
|
194
|
+
| `kyro.W004` | a parameter has a default but is not trailing, so it cannot be omitted |
|
|
195
|
+
|
|
196
|
+
Silence one with `SILENCED_SYSTEM_CHECKS = ["kyro.W003"]`.
|
|
197
|
+
|
|
198
|
+
## Settings
|
|
199
|
+
|
|
200
|
+
- `KYRO_DISCOVER` (default `["views"]`): app submodules imported by `kyro.discover()`.
|
|
201
|
+
|
|
202
|
+
`kyro.discover(["blog"])` imports the given modules (for a package, its `KYRO_DISCOVER`
|
|
203
|
+
submodules too) and returns only their routes, to mount them under a prefix:
|
|
204
|
+
`path("blog/", include(kyro.discover(["blog"])))`.
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
uv sync # create .venv and install dependencies
|
|
210
|
+
uv run pytest # run the test suite
|
|
211
|
+
uv run ruff check . # lint
|
|
212
|
+
uv run ruff format . # format
|
|
213
|
+
uv run python -m mypy # type check (strict)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Releasing
|
|
217
|
+
|
|
218
|
+
1. Set the version in `src/kyro/__init__.py` (`__version__`), and in `CHANGELOG.md` rename
|
|
219
|
+
`[Unreleased]` to `[x.y.z] - YYYY-MM-DD` and update the links at the bottom.
|
|
220
|
+
2. Optionally, try the release out on TestPyPI: *Actions → Release → Run workflow*.
|
|
221
|
+
3. Commit, tag and push: `git tag vx.y.z && git push origin main vx.y.z`. The `Release`
|
|
222
|
+
workflow publishes to TestPyPI, then PyPI.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# kyro
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mistermerlin/kyro-routing/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Symfony-style attribute routing for Django: declare routes with decorators directly on
|
|
6
|
+
your views and controllers instead of maintaining a long `urls.py`.
|
|
7
|
+
|
|
8
|
+
> Status: pre-alpha, under active development.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install kyro-routing # or: uv add kyro-routing
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The package is imported as `kyro`. Add `"kyro"` to `INSTALLED_APPS` for the management
|
|
17
|
+
commands and the system checks.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# blog/views.py
|
|
23
|
+
from django.http import HttpResponse
|
|
24
|
+
from kyro import route
|
|
25
|
+
|
|
26
|
+
from blog.models import Post
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@route("/hello/{name}", name="hello")
|
|
30
|
+
def hello(request, name):
|
|
31
|
+
return HttpResponse(f"Hello {name}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@route("/blog", name="blog_")
|
|
35
|
+
class BlogController:
|
|
36
|
+
@route("", name="index", methods="GET")
|
|
37
|
+
def index(self, request): ...
|
|
38
|
+
|
|
39
|
+
@route("", name="create", methods="POST")
|
|
40
|
+
def create(self, request): ...
|
|
41
|
+
|
|
42
|
+
@route(r"/page/{page<\d+>?1}", name="list") # before /{slug}, which would catch /page
|
|
43
|
+
def list(self, request, page: int): ...
|
|
44
|
+
|
|
45
|
+
@route("/{slug}", name="show")
|
|
46
|
+
def show(self, request, post: Post): # fetched by slug, 404 if missing
|
|
47
|
+
...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# project/urls.py
|
|
52
|
+
import kyro
|
|
53
|
+
|
|
54
|
+
urlpatterns = kyro.discover() # imports the `views` module of every installed app
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Routes are plain Django URL patterns: `reverse("blog_show", kwargs={"slug": "hello"})` and
|
|
58
|
+
`{% url "blog_index" %}` work as usual, and so do `include()` and namespaces.
|
|
59
|
+
|
|
60
|
+
## Paths
|
|
61
|
+
|
|
62
|
+
Both syntaxes are accepted, even in the same path:
|
|
63
|
+
|
|
64
|
+
| Syntax | Meaning |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `<name>`, `<int:id>` | Django parameter and converter |
|
|
67
|
+
| `{name}` | parameter (anything but `/`) |
|
|
68
|
+
| `{id<\d+>}` | parameter with a regex requirement |
|
|
69
|
+
| `{page?1}` | optional parameter with default `"1"` |
|
|
70
|
+
| `{page?}` | optional parameter with default `None` |
|
|
71
|
+
|
|
72
|
+
Trailing parameters with a default (inline or in `defaults=`) are optional:
|
|
73
|
+
`/blog/{page?1}` matches `/blog` and `/blog/2`.
|
|
74
|
+
|
|
75
|
+
## `@route` arguments
|
|
76
|
+
|
|
77
|
+
| Argument | |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `path` | the path, relative to the class prefix if any |
|
|
80
|
+
| `name` | route name, appended to the class `name` prefix; generated when omitted |
|
|
81
|
+
| `methods` | `"GET"` or `["GET", "POST"]`; others get a 405. `GET` also allows `HEAD` |
|
|
82
|
+
| `requirements` | `{"id": r"\d+"}` |
|
|
83
|
+
| `defaults` | defaults of optional parameters and extra kwargs passed to the view |
|
|
84
|
+
| `priority` | higher priority routes are matched first; otherwise declaration order |
|
|
85
|
+
|
|
86
|
+
Several routes may share a path with different `methods`: kyro dispatches each request to
|
|
87
|
+
the right view. `@route` can be stacked to give a view several routes.
|
|
88
|
+
|
|
89
|
+
## What can be routed
|
|
90
|
+
|
|
91
|
+
- Functions, sync or async. Put `@route` above or below other decorators.
|
|
92
|
+
- Methods of any class: the class is instantiated for each request. A `@route` on the class
|
|
93
|
+
is a prefix (path, name, requirements, defaults, methods) for its methods. Classes defined
|
|
94
|
+
inside a function must be decorated (`@route()` for no prefix).
|
|
95
|
+
- A class with `__call__` and no routed method, or a Django `View`: the class route is the
|
|
96
|
+
route of the class itself.
|
|
97
|
+
|
|
98
|
+
## Parameter conversion
|
|
99
|
+
|
|
100
|
+
Annotated view arguments are converted from the URL:
|
|
101
|
+
|
|
102
|
+
- `int`, `float`, `Decimal`, `bool`, `UUID`, `date`, `datetime`, `Enum`: an invalid value
|
|
103
|
+
is a 404.
|
|
104
|
+
- Django models, fetched like Symfony's entity resolver:
|
|
105
|
+
- `{post}` for `post: Post`: lookup by primary key;
|
|
106
|
+
- `{slug}` for `post: Post`: lookup by fields named like URL parameters (the parameter is
|
|
107
|
+
only passed to the view if its signature accepts it);
|
|
108
|
+
- `post: Annotated[Post, MapModel(slug="post_slug")]`: explicit mapping.
|
|
109
|
+
|
|
110
|
+
A missing object is a 404, or `None` when the annotation is optional (`Post | None`).
|
|
111
|
+
|
|
112
|
+
## Debugging routes
|
|
113
|
+
|
|
114
|
+
With `"kyro"` in `INSTALLED_APPS`, `manage.py debug_router` lists every route of the
|
|
115
|
+
project, kyro or not, in matching order:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
$ python manage.py debug_router
|
|
119
|
+
-------------- ---------- ------------------
|
|
120
|
+
Name Method Path
|
|
121
|
+
-------------- ---------- ------------------
|
|
122
|
+
blog_index GET|HEAD /blog
|
|
123
|
+
blog_create POST /blog
|
|
124
|
+
blog_show ANY /blog/{slug}
|
|
125
|
+
-------------- ---------- ------------------
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- `debug_router blog_show` shows one route in detail: Django routes, requirements,
|
|
129
|
+
defaults, priority, view and the file and line where it is declared. A partial name shows
|
|
130
|
+
the detail when it matches one route, the list of matches otherwise.
|
|
131
|
+
- `--show-views` adds the view of each route to the list, `--format json` outputs JSON and
|
|
132
|
+
`--urlconf` inspects another URLconf than `ROOT_URLCONF`.
|
|
133
|
+
|
|
134
|
+
`manage.py router_match` tells which route serves a request, method dispatch included:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
$ python manage.py router_match /blog --method POST
|
|
138
|
+
------------- ---------- ------- --------------------
|
|
139
|
+
Name Method Path Result
|
|
140
|
+
------------- ---------- ------- --------------------
|
|
141
|
+
blog_index GET|HEAD /blog method not allowed
|
|
142
|
+
blog_create POST /blog match
|
|
143
|
+
------------- ---------- ------- --------------------
|
|
144
|
+
|
|
145
|
+
Route 'blog_create' matches.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
It then shows the route in detail with the kwargs its view receives. Routes matching the
|
|
149
|
+
path but not the method, or hidden by an earlier route (`shadowed`), are listed; `-v 2`
|
|
150
|
+
lists every route. The command fails when no route serves the request (404 or 405).
|
|
151
|
+
`--format json` and `--urlconf` work as for `debug_router`.
|
|
152
|
+
|
|
153
|
+
## System checks
|
|
154
|
+
|
|
155
|
+
With `"kyro"` in `INSTALLED_APPS`, `manage.py check` (and `runserver`) warns about:
|
|
156
|
+
|
|
157
|
+
| Id | Problem |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `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`) |
|
|
160
|
+
| `kyro.W002` | a route name is shared with other routes, so `reverse()` only returns one of them |
|
|
161
|
+
| `kyro.W003` | routes declared with `@route` are not in the URLconf (`kyro.discover()` forgotten?) |
|
|
162
|
+
| `kyro.W004` | a parameter has a default but is not trailing, so it cannot be omitted |
|
|
163
|
+
|
|
164
|
+
Silence one with `SILENCED_SYSTEM_CHECKS = ["kyro.W003"]`.
|
|
165
|
+
|
|
166
|
+
## Settings
|
|
167
|
+
|
|
168
|
+
- `KYRO_DISCOVER` (default `["views"]`): app submodules imported by `kyro.discover()`.
|
|
169
|
+
|
|
170
|
+
`kyro.discover(["blog"])` imports the given modules (for a package, its `KYRO_DISCOVER`
|
|
171
|
+
submodules too) and returns only their routes, to mount them under a prefix:
|
|
172
|
+
`path("blog/", include(kyro.discover(["blog"])))`.
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv sync # create .venv and install dependencies
|
|
178
|
+
uv run pytest # run the test suite
|
|
179
|
+
uv run ruff check . # lint
|
|
180
|
+
uv run ruff format . # format
|
|
181
|
+
uv run python -m mypy # type check (strict)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Releasing
|
|
185
|
+
|
|
186
|
+
1. Set the version in `src/kyro/__init__.py` (`__version__`), and in `CHANGELOG.md` rename
|
|
187
|
+
`[Unreleased]` to `[x.y.z] - YYYY-MM-DD` and update the links at the bottom.
|
|
188
|
+
2. Optionally, try the release out on TestPyPI: *Actions → Release → Run workflow*.
|
|
189
|
+
3. Commit, tag and push: `git tag vx.y.z && git push origin main vx.y.z`. The `Release`
|
|
190
|
+
workflow publishes to TestPyPI, then PyPI.
|