fastapi-router-versioning 0.1.1__tar.gz → 0.2.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.
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/pre-commit.yml +1 -1
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/prepare-release.yml +1 -1
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/publish.yml +1 -1
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/test-redistribute.yml +2 -2
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/test.yml +3 -3
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/PKG-INFO +56 -16
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/README.md +53 -14
- fastapi_router_versioning-0.2.0/RELEASE_NOTES.md +58 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/examples/openapi_hook_app.py +1 -1
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/examples/self_hosted_docs_app.py +1 -1
- fastapi_router_versioning-0.2.0/examples/validation_error_code_app.py +57 -0
- fastapi_router_versioning-0.2.0/examples/validation_error_code_custom_handler_app.py +70 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/examples/webhook_versioning_app.py +1 -1
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/fastapi_router_versioning/__init__.py +1 -1
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/fastapi_router_versioning/versioner.py +71 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/pyproject.toml +2 -1
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/tests/test_versioner.py +206 -1
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/uv.lock +163 -178
- fastapi_router_versioning-0.1.1/RELEASE_NOTES.md +0 -37
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/dependabot.yml +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/detect-conflicts.yml +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.gitignore +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.pre-commit-config.yaml +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.python-version +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/LICENSE +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/examples/calver_app.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/examples/download_static_assets.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/examples/multi_router_app.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/examples/semver_app.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/examples/semver_major_only_app.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/fastapi_router_versioning/py.typed +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/scripts/format.sh +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/scripts/lint.sh +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/scripts/prepare_release.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/scripts/test-cov-html.sh +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/scripts/test-cov.sh +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/scripts/test.sh +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/tests/__init__.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/tests/test_calver.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/tests/test_fastapi_integration.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/tests/test_prepare_release.py +0 -0
- {fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/tests/test_semver.py +0 -0
{fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/pre-commit.yml
RENAMED
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
run: echo "$GITHUB_CONTEXT"
|
|
22
22
|
|
|
23
23
|
- name: Checkout code
|
|
24
|
-
uses: actions/checkout@
|
|
24
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
25
25
|
with:
|
|
26
26
|
persist-credentials: false
|
|
27
27
|
# Full history required for --from-ref/--to-ref diff in prek
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
issues: write
|
|
32
32
|
pull-requests: write
|
|
33
33
|
steps:
|
|
34
|
-
- uses: actions/checkout@
|
|
34
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
35
35
|
with:
|
|
36
36
|
persist-credentials: true
|
|
37
37
|
- name: Set up Python
|
{fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/publish.yml
RENAMED
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
env:
|
|
21
21
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
|
22
22
|
run: echo "$GITHUB_CONTEXT"
|
|
23
|
-
- uses: actions/checkout@
|
|
23
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
24
24
|
with:
|
|
25
25
|
persist-credentials: false
|
|
26
26
|
- name: Set up Python
|
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
outputs:
|
|
21
21
|
src: ${{ steps.filter.outputs.src }}
|
|
22
22
|
steps:
|
|
23
|
-
- uses: actions/checkout@
|
|
23
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
24
24
|
with:
|
|
25
25
|
persist-credentials: false
|
|
26
26
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
env:
|
|
49
49
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
|
50
50
|
run: echo "$GITHUB_CONTEXT"
|
|
51
|
-
- uses: actions/checkout@
|
|
51
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
52
52
|
with:
|
|
53
53
|
persist-credentials: false
|
|
54
54
|
- name: Set up Python
|
{fastapi_router_versioning-0.1.1 → fastapi_router_versioning-0.2.0}/.github/workflows/test.yml
RENAMED
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
outputs:
|
|
26
26
|
src: ${{ steps.filter.outputs.src }}
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@
|
|
28
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
29
29
|
with:
|
|
30
30
|
persist-credentials: false
|
|
31
31
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
|
@@ -70,7 +70,7 @@ jobs:
|
|
|
70
70
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
|
71
71
|
run: echo "$GITHUB_CONTEXT"
|
|
72
72
|
|
|
73
|
-
- uses: actions/checkout@
|
|
73
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
74
74
|
with:
|
|
75
75
|
persist-credentials: false
|
|
76
76
|
|
|
@@ -122,7 +122,7 @@ jobs:
|
|
|
122
122
|
permissions:
|
|
123
123
|
contents: read
|
|
124
124
|
steps:
|
|
125
|
-
- uses: actions/checkout@
|
|
125
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
126
126
|
with:
|
|
127
127
|
persist-credentials: false
|
|
128
128
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-router-versioning
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Router-based API versioning for FastAPI, with per-version docs and a declarative route lifecycle.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mat81black/fastapi-router-versioning
|
|
6
6
|
Project-URL: Repository, https://github.com/mat81black/fastapi-router-versioning
|
|
7
7
|
Project-URL: Issues, https://github.com/mat81black/fastapi-router-versioning/issues
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
25
|
Classifier: Typing :: Typed
|
|
@@ -26,15 +27,15 @@ Requires-Python: >=3.10
|
|
|
26
27
|
Requires-Dist: fastapi!=0.137.0,!=0.137.1,>=0.120.0
|
|
27
28
|
Description-Content-Type: text/markdown
|
|
28
29
|
|
|
29
|
-
# FastAPI Router Versioning
|
|
30
|
+
# FastAPI Router Versioning
|
|
30
31
|
|
|
31
32
|
[](https://pypi.org/project/fastapi-router-versioning/)
|
|
32
33
|
[](LICENSE)
|
|
33
34
|
[](https://pypi.org/project/fastapi-router-versioning/)
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
Router-based API versioning for FastAPI.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
FastAPI has no built-in versioning mechanism. The common workaround — duplicating routers or managing prefixes manually — breaks down quickly as the number of versions grows. This package adds declarative versioning directly on routes, with isolated URL prefixes, per-version Swagger UI, and a full route lifecycle, without touching the existing application structure.
|
|
38
39
|
|
|
39
40
|
---
|
|
40
41
|
|
|
@@ -46,6 +47,7 @@ If you want to implement **FastAPI API versioning**, you can simply annotate you
|
|
|
46
47
|
- **Latest alias** — serve the newest version under a stable `/latest` prefix
|
|
47
48
|
- **Self-hosted docs** — point Swagger UI and ReDoc at your own assets for air-gapped environments
|
|
48
49
|
- **Reverse proxy aware** — doc URLs include the ASGI `root_path` at request time, so sub-app mounting works out of the box
|
|
50
|
+
- **Configurable validation error code** — return `400` (or any code) instead of `422` for request validation errors, with the OpenAPI schema updated automatically
|
|
49
51
|
- **Broad compatibility** — works with nested routers, WebSockets, `Depends`, and OpenAPI Callbacks
|
|
50
52
|
|
|
51
53
|
---
|
|
@@ -93,7 +95,7 @@ RouterVersioner(app=app, routers=router, version_format=VersionFormat.SEMVER).ve
|
|
|
93
95
|
# Mounts: GET /v1_0/items GET /v2_0/items
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
Each version
|
|
98
|
+
Each version gets its own Swagger UI at `/v1_0/docs`, `/v2_0/docs`, and so on.
|
|
97
99
|
|
|
98
100
|
---
|
|
99
101
|
|
|
@@ -117,9 +119,9 @@ RouterVersioner(app=app, routers=router, version_format=VersionFormat.CALVER).ve
|
|
|
117
119
|
# Mounts: GET /2025-01-01/items
|
|
118
120
|
```
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
Valid CalVer tokens: `"2025-01-01"`, `"v3"`, `"stable"`, etc.
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
**CalVer sorting:** versions are sorted lexicographically, so tokens must be comparable in the intended order. ISO dates (`"2025-01-01"`) and zero-padded numbers (`"v01"`, `"v02"`) work correctly. Non-padded strings like `"v1"`, `"v10"`, `"v2"` will not sort correctly and will cause routes to appear in the wrong versions.
|
|
123
125
|
|
|
124
126
|
---
|
|
125
127
|
|
|
@@ -168,6 +170,8 @@ Routes without `@api_version` fall back to `default_version` (default: `(1, 0)`
|
|
|
168
170
|
| `redoc_js_url` | `str \| None` | FastAPI CDN | Custom URL for the ReDoc JS bundle |
|
|
169
171
|
| `redoc_favicon_url` | `str \| None` | FastAPI favicon | Custom URL for the ReDoc favicon |
|
|
170
172
|
| `redoc_with_google_fonts` | `bool` | `True` | If `False`, ReDoc will not load Google Fonts |
|
|
173
|
+
| `validation_error_code` | `int` | `422` | HTTP status code returned for request validation errors; also replaces the `422` entry in the OpenAPI schema |
|
|
174
|
+
| `handle_validation_exceptions` | `bool` | `True` | If `False`, only the schema is updated — register your own `RequestValidationError` handler to control the response body |
|
|
171
175
|
|
|
172
176
|
Call `.versionize()` after constructing the object. It returns the list of active versions.
|
|
173
177
|
|
|
@@ -181,8 +185,8 @@ Call `.versionize()` after constructing the object. It returns the list of activ
|
|
|
181
185
|
|
|
182
186
|
| Parameter | Type | Required | Description |
|
|
183
187
|
|---|---|---|---|
|
|
184
|
-
| `version` | `tuple[int, int] \| str` | yes |
|
|
185
|
-
| `deprecate_in` | same \| `None` | no | Version
|
|
188
|
+
| `version` | `tuple[int, int] \| str` | yes | First version in which this route is active |
|
|
189
|
+
| `deprecate_in` | same \| `None` | no | Version in which this route is marked deprecated in the docs |
|
|
186
190
|
| `remove_in` | same \| `None` | no | Version from which this route is removed entirely |
|
|
187
191
|
|
|
188
192
|
All three parameters must match the `version_format` configured on `RouterVersioner`
|
|
@@ -278,7 +282,7 @@ RouterVersioner(
|
|
|
278
282
|
).versionize()
|
|
279
283
|
```
|
|
280
284
|
|
|
281
|
-
The hook receives `(schema: dict, version: VersionT)` and must return the
|
|
285
|
+
The hook receives `(schema: dict, version: VersionT)` and must return the modified dict.
|
|
282
286
|
|
|
283
287
|
### OpenAPI Callbacks and Webhooks
|
|
284
288
|
|
|
@@ -325,12 +329,11 @@ RouterVersioner(
|
|
|
325
329
|
# /v2_0/openapi.json → webhooks: /order-created (V2) ← /payment-failed removed
|
|
326
330
|
```
|
|
327
331
|
|
|
328
|
-
The same
|
|
329
|
-
a new webhook version only appears once a route version creates that API prefix.
|
|
332
|
+
The same `remove_in` lifecycle applies. A new webhook version only appears once a route version creates that API prefix.
|
|
330
333
|
|
|
331
334
|
### Multiple routers
|
|
332
335
|
|
|
333
|
-
Pass a list of routers to version routes
|
|
336
|
+
Pass a list of routers to version routes split across modules:
|
|
334
337
|
|
|
335
338
|
```python
|
|
336
339
|
RouterVersioner(
|
|
@@ -344,7 +347,7 @@ All routers are versioned together under the same prefix tree.
|
|
|
344
347
|
|
|
345
348
|
### Self-hosted docs (air-gapped environments)
|
|
346
349
|
|
|
347
|
-
By default, Swagger UI and ReDoc assets are loaded from the FastAPI CDN. In air-gapped or corporate environments, point them at
|
|
350
|
+
By default, Swagger UI and ReDoc assets are loaded from the FastAPI CDN. In air-gapped or corporate environments, point them at locally hosted assets:
|
|
348
351
|
|
|
349
352
|
```python
|
|
350
353
|
RouterVersioner(
|
|
@@ -360,7 +363,42 @@ RouterVersioner(
|
|
|
360
363
|
).versionize()
|
|
361
364
|
```
|
|
362
365
|
|
|
363
|
-
See [`examples/download_static_assets.py`](examples/download_static_assets.py) for a
|
|
366
|
+
See [`examples/download_static_assets.py`](examples/download_static_assets.py) for a script that downloads all required assets in one step, and [`examples/self_hosted_docs_app.py`](examples/self_hosted_docs_app.py) for a complete working example.
|
|
367
|
+
|
|
368
|
+
### Validation error code
|
|
369
|
+
|
|
370
|
+
By default FastAPI returns `422 Unprocessable Entity` for request validation errors.
|
|
371
|
+
Use `validation_error_code` to change the code returned both at runtime and in the OpenAPI schema:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
RouterVersioner(
|
|
375
|
+
app=app,
|
|
376
|
+
routers=router,
|
|
377
|
+
version_format=VersionFormat.SEMVER,
|
|
378
|
+
validation_error_code=400,
|
|
379
|
+
).versionize()
|
|
380
|
+
# Validation errors now return 400; the schema shows 400 instead of 422
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
To keep full control over the response body, set `handle_validation_exceptions=False` and register your own handler:
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from fastapi import Request
|
|
387
|
+
from fastapi.exceptions import RequestValidationError
|
|
388
|
+
from fastapi.responses import JSONResponse
|
|
389
|
+
|
|
390
|
+
@app.exception_handler(RequestValidationError)
|
|
391
|
+
async def my_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
392
|
+
return JSONResponse(status_code=400, content={"errors": exc.errors()})
|
|
393
|
+
|
|
394
|
+
RouterVersioner(
|
|
395
|
+
app=app,
|
|
396
|
+
routers=router,
|
|
397
|
+
version_format=VersionFormat.SEMVER,
|
|
398
|
+
validation_error_code=400,
|
|
399
|
+
handle_validation_exceptions=False, # schema updated; handler is yours
|
|
400
|
+
).versionize()
|
|
401
|
+
```
|
|
364
402
|
|
|
365
403
|
### Reverse proxy / sub-app mounting
|
|
366
404
|
|
|
@@ -402,6 +440,8 @@ Runnable examples are available in the [`examples/`](examples/) directory:
|
|
|
402
440
|
| [`webhook_versioning_app.py`](examples/webhook_versioning_app.py) | Per-version webhook definitions via `webhook_routers` |
|
|
403
441
|
| [`multi_router_app.py`](examples/multi_router_app.py) | Multiple routers versioned together |
|
|
404
442
|
| [`self_hosted_docs_app.py`](examples/self_hosted_docs_app.py) | Swagger UI and ReDoc from local static assets |
|
|
443
|
+
| [`validation_error_code_app.py`](examples/validation_error_code_app.py) | Return `400` instead of `422` for validation errors (automatic handler via `RouterVersioner`) |
|
|
444
|
+
| [`validation_error_code_custom_handler_app.py`](examples/validation_error_code_custom_handler_app.py) | Same, but with `handle_validation_exceptions=False` and a user-defined exception handler |
|
|
405
445
|
|
|
406
446
|
---
|
|
407
447
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# FastAPI Router Versioning
|
|
1
|
+
# FastAPI Router Versioning
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/fastapi-router-versioning/)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://pypi.org/project/fastapi-router-versioning/)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Router-based API versioning for FastAPI.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
FastAPI has no built-in versioning mechanism. The common workaround — duplicating routers or managing prefixes manually — breaks down quickly as the number of versions grows. This package adds declarative versioning directly on routes, with isolated URL prefixes, per-version Swagger UI, and a full route lifecycle, without touching the existing application structure.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -18,6 +18,7 @@ If you want to implement **FastAPI API versioning**, you can simply annotate you
|
|
|
18
18
|
- **Latest alias** — serve the newest version under a stable `/latest` prefix
|
|
19
19
|
- **Self-hosted docs** — point Swagger UI and ReDoc at your own assets for air-gapped environments
|
|
20
20
|
- **Reverse proxy aware** — doc URLs include the ASGI `root_path` at request time, so sub-app mounting works out of the box
|
|
21
|
+
- **Configurable validation error code** — return `400` (or any code) instead of `422` for request validation errors, with the OpenAPI schema updated automatically
|
|
21
22
|
- **Broad compatibility** — works with nested routers, WebSockets, `Depends`, and OpenAPI Callbacks
|
|
22
23
|
|
|
23
24
|
---
|
|
@@ -65,7 +66,7 @@ RouterVersioner(app=app, routers=router, version_format=VersionFormat.SEMVER).ve
|
|
|
65
66
|
# Mounts: GET /v1_0/items GET /v2_0/items
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
Each version
|
|
69
|
+
Each version gets its own Swagger UI at `/v1_0/docs`, `/v2_0/docs`, and so on.
|
|
69
70
|
|
|
70
71
|
---
|
|
71
72
|
|
|
@@ -89,9 +90,9 @@ RouterVersioner(app=app, routers=router, version_format=VersionFormat.CALVER).ve
|
|
|
89
90
|
# Mounts: GET /2025-01-01/items
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
Valid CalVer tokens: `"2025-01-01"`, `"v3"`, `"stable"`, etc.
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
**CalVer sorting:** versions are sorted lexicographically, so tokens must be comparable in the intended order. ISO dates (`"2025-01-01"`) and zero-padded numbers (`"v01"`, `"v02"`) work correctly. Non-padded strings like `"v1"`, `"v10"`, `"v2"` will not sort correctly and will cause routes to appear in the wrong versions.
|
|
95
96
|
|
|
96
97
|
---
|
|
97
98
|
|
|
@@ -140,6 +141,8 @@ Routes without `@api_version` fall back to `default_version` (default: `(1, 0)`
|
|
|
140
141
|
| `redoc_js_url` | `str \| None` | FastAPI CDN | Custom URL for the ReDoc JS bundle |
|
|
141
142
|
| `redoc_favicon_url` | `str \| None` | FastAPI favicon | Custom URL for the ReDoc favicon |
|
|
142
143
|
| `redoc_with_google_fonts` | `bool` | `True` | If `False`, ReDoc will not load Google Fonts |
|
|
144
|
+
| `validation_error_code` | `int` | `422` | HTTP status code returned for request validation errors; also replaces the `422` entry in the OpenAPI schema |
|
|
145
|
+
| `handle_validation_exceptions` | `bool` | `True` | If `False`, only the schema is updated — register your own `RequestValidationError` handler to control the response body |
|
|
143
146
|
|
|
144
147
|
Call `.versionize()` after constructing the object. It returns the list of active versions.
|
|
145
148
|
|
|
@@ -153,8 +156,8 @@ Call `.versionize()` after constructing the object. It returns the list of activ
|
|
|
153
156
|
|
|
154
157
|
| Parameter | Type | Required | Description |
|
|
155
158
|
|---|---|---|---|
|
|
156
|
-
| `version` | `tuple[int, int] \| str` | yes |
|
|
157
|
-
| `deprecate_in` | same \| `None` | no | Version
|
|
159
|
+
| `version` | `tuple[int, int] \| str` | yes | First version in which this route is active |
|
|
160
|
+
| `deprecate_in` | same \| `None` | no | Version in which this route is marked deprecated in the docs |
|
|
158
161
|
| `remove_in` | same \| `None` | no | Version from which this route is removed entirely |
|
|
159
162
|
|
|
160
163
|
All three parameters must match the `version_format` configured on `RouterVersioner`
|
|
@@ -250,7 +253,7 @@ RouterVersioner(
|
|
|
250
253
|
).versionize()
|
|
251
254
|
```
|
|
252
255
|
|
|
253
|
-
The hook receives `(schema: dict, version: VersionT)` and must return the
|
|
256
|
+
The hook receives `(schema: dict, version: VersionT)` and must return the modified dict.
|
|
254
257
|
|
|
255
258
|
### OpenAPI Callbacks and Webhooks
|
|
256
259
|
|
|
@@ -297,12 +300,11 @@ RouterVersioner(
|
|
|
297
300
|
# /v2_0/openapi.json → webhooks: /order-created (V2) ← /payment-failed removed
|
|
298
301
|
```
|
|
299
302
|
|
|
300
|
-
The same
|
|
301
|
-
a new webhook version only appears once a route version creates that API prefix.
|
|
303
|
+
The same `remove_in` lifecycle applies. A new webhook version only appears once a route version creates that API prefix.
|
|
302
304
|
|
|
303
305
|
### Multiple routers
|
|
304
306
|
|
|
305
|
-
Pass a list of routers to version routes
|
|
307
|
+
Pass a list of routers to version routes split across modules:
|
|
306
308
|
|
|
307
309
|
```python
|
|
308
310
|
RouterVersioner(
|
|
@@ -316,7 +318,7 @@ All routers are versioned together under the same prefix tree.
|
|
|
316
318
|
|
|
317
319
|
### Self-hosted docs (air-gapped environments)
|
|
318
320
|
|
|
319
|
-
By default, Swagger UI and ReDoc assets are loaded from the FastAPI CDN. In air-gapped or corporate environments, point them at
|
|
321
|
+
By default, Swagger UI and ReDoc assets are loaded from the FastAPI CDN. In air-gapped or corporate environments, point them at locally hosted assets:
|
|
320
322
|
|
|
321
323
|
```python
|
|
322
324
|
RouterVersioner(
|
|
@@ -332,7 +334,42 @@ RouterVersioner(
|
|
|
332
334
|
).versionize()
|
|
333
335
|
```
|
|
334
336
|
|
|
335
|
-
See [`examples/download_static_assets.py`](examples/download_static_assets.py) for a
|
|
337
|
+
See [`examples/download_static_assets.py`](examples/download_static_assets.py) for a script that downloads all required assets in one step, and [`examples/self_hosted_docs_app.py`](examples/self_hosted_docs_app.py) for a complete working example.
|
|
338
|
+
|
|
339
|
+
### Validation error code
|
|
340
|
+
|
|
341
|
+
By default FastAPI returns `422 Unprocessable Entity` for request validation errors.
|
|
342
|
+
Use `validation_error_code` to change the code returned both at runtime and in the OpenAPI schema:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
RouterVersioner(
|
|
346
|
+
app=app,
|
|
347
|
+
routers=router,
|
|
348
|
+
version_format=VersionFormat.SEMVER,
|
|
349
|
+
validation_error_code=400,
|
|
350
|
+
).versionize()
|
|
351
|
+
# Validation errors now return 400; the schema shows 400 instead of 422
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
To keep full control over the response body, set `handle_validation_exceptions=False` and register your own handler:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from fastapi import Request
|
|
358
|
+
from fastapi.exceptions import RequestValidationError
|
|
359
|
+
from fastapi.responses import JSONResponse
|
|
360
|
+
|
|
361
|
+
@app.exception_handler(RequestValidationError)
|
|
362
|
+
async def my_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
363
|
+
return JSONResponse(status_code=400, content={"errors": exc.errors()})
|
|
364
|
+
|
|
365
|
+
RouterVersioner(
|
|
366
|
+
app=app,
|
|
367
|
+
routers=router,
|
|
368
|
+
version_format=VersionFormat.SEMVER,
|
|
369
|
+
validation_error_code=400,
|
|
370
|
+
handle_validation_exceptions=False, # schema updated; handler is yours
|
|
371
|
+
).versionize()
|
|
372
|
+
```
|
|
336
373
|
|
|
337
374
|
### Reverse proxy / sub-app mounting
|
|
338
375
|
|
|
@@ -374,6 +411,8 @@ Runnable examples are available in the [`examples/`](examples/) directory:
|
|
|
374
411
|
| [`webhook_versioning_app.py`](examples/webhook_versioning_app.py) | Per-version webhook definitions via `webhook_routers` |
|
|
375
412
|
| [`multi_router_app.py`](examples/multi_router_app.py) | Multiple routers versioned together |
|
|
376
413
|
| [`self_hosted_docs_app.py`](examples/self_hosted_docs_app.py) | Swagger UI and ReDoc from local static assets |
|
|
414
|
+
| [`validation_error_code_app.py`](examples/validation_error_code_app.py) | Return `400` instead of `422` for validation errors (automatic handler via `RouterVersioner`) |
|
|
415
|
+
| [`validation_error_code_custom_handler_app.py`](examples/validation_error_code_custom_handler_app.py) | Same, but with `handle_validation_exceptions=False` and a user-defined exception handler |
|
|
377
416
|
|
|
378
417
|
---
|
|
379
418
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Release Notes
|
|
2
|
+
|
|
3
|
+
## Latest Changes
|
|
4
|
+
|
|
5
|
+
## 0.2.0 (2026-06-26)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* ✨ Add `validation_error_code` parameter to `RouterVersioner` to override the HTTP status code returned for request validation errors (default: `422`). The OpenAPI schema is updated automatically to reflect the custom code. PR [#16](https://github.com/mat81black/fastapi-router-versioning/pull/16) by [@mat81black](https://github.com/mat81black).
|
|
10
|
+
* ✨ Add `handle_validation_exceptions` parameter to `RouterVersioner`: when `False`, only the OpenAPI schema is updated, leaving the exception handler to the user. PR [#16](https://github.com/mat81black/fastapi-router-versioning/pull/16) by [@mat81black](https://github.com/mat81black).
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
* 🐛 Fix Python 3.14 classifier not actually added to `pyproject.toml` in 0.1.2. PR [#15](https://github.com/mat81black/fastapi-router-versioning/pull/15) by [@mat81black](https://github.com/mat81black).
|
|
15
|
+
|
|
16
|
+
## 0.1.2 (2026-06-25)
|
|
17
|
+
|
|
18
|
+
### Internal
|
|
19
|
+
|
|
20
|
+
* 👷 Add Python 3.14 classifier to `pyproject.toml` to align with the versions already covered by CI.
|
|
21
|
+
|
|
22
|
+
### Docs
|
|
23
|
+
|
|
24
|
+
* 📝 Update README and `pyproject.toml` to clarify package description and improve documentation consistency. PR [#12](https://github.com/mat81black/fastapi-router-versioning/pull/12) by [@mat81black](https://github.com/mat81black).
|
|
25
|
+
|
|
26
|
+
## 0.1.1 (2026-06-24)
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
* ✨ Add per-version OpenAPI schema caching: schemas are generated once and cached, reducing overhead on repeated requests. PR [#5](https://github.com/mat81black/fastapi-router-versioning/pull/5) by [@mat81black](https://github.com/mat81black).
|
|
31
|
+
* ✨ Track which routes belong to each version to invalidate the cache automatically when the route set changes. PR [#5](https://github.com/mat81black/fastapi-router-versioning/pull/5) by [@mat81black](https://github.com/mat81black).
|
|
32
|
+
|
|
33
|
+
### Fixes
|
|
34
|
+
|
|
35
|
+
* 🐛 Fix WebSocket routes not being handled correctly in `versioned_routers` when the cache was active. PR [#5](https://github.com/mat81black/fastapi-router-versioning/pull/5) by [@mat81black](https://github.com/mat81black).
|
|
36
|
+
* 🐛 Improve OpenAPI schema cache resilience: cache is now safely bypassed on unexpected errors instead of raising. PR [#5](https://github.com/mat81black/fastapi-router-versioning/pull/5) by [@mat81black](https://github.com/mat81black).
|
|
37
|
+
|
|
38
|
+
### Internal
|
|
39
|
+
|
|
40
|
+
* 👷 Add `prepare-release.yml` workflow and `scripts/prepare_release.py` for automated version bumping and release PR creation. PR [#6](https://github.com/mat81black/fastapi-router-versioning/pull/6) by [@mat81black](https://github.com/mat81black).
|
|
41
|
+
* 👷 Add `changes` job to `test.yml` and `test-redistribute.yml` to skip CI when no relevant files are modified. PR [#10](https://github.com/mat81black/fastapi-router-versioning/pull/10) by [@mat81black](https://github.com/mat81black).
|
|
42
|
+
|
|
43
|
+
## 0.1.0 (2025-06-24)
|
|
44
|
+
|
|
45
|
+
🚀 First official public release of **fastapi-router-versioning**.
|
|
46
|
+
|
|
47
|
+
Router-based API versioning for FastAPI, with declarative route lifecycle, per-version OpenAPI schemas, and isolated documentation — without altering the core application structure.
|
|
48
|
+
|
|
49
|
+
### Features
|
|
50
|
+
|
|
51
|
+
* ✨ SemVer & CalVer support: route versioning using `(major, minor)` tuples or lexicographically sortable arbitrary strings.
|
|
52
|
+
* ✨ Declarative route lifecycle: introduce, deprecate (`deprecate_in`), and remove (`remove_in`) routes across versions.
|
|
53
|
+
* ✨ Per-version documentation: isolated Swagger UI, ReDoc, and `openapi.json` for each active version.
|
|
54
|
+
* ✨ OpenAPI schema hook: modify the filtered schema per version via `openapi_hook`.
|
|
55
|
+
* ✨ Independent webhook versioning via `webhook_routers`, with propagation of per-route OpenAPI Callbacks.
|
|
56
|
+
* ✨ Latest-version alias: expose the highest active version under a stable configurable prefix.
|
|
57
|
+
* ✨ Self-hosted docs: full control over Swagger/ReDoc static assets and option to disable Google Fonts.
|
|
58
|
+
* ✨ Reverse proxy aware: resolves and injects the ASGI `root_path` at request time.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Demonstrate overriding the HTTP status code returned for request validation errors.
|
|
2
|
+
|
|
3
|
+
By default FastAPI returns 422 Unprocessable Entity for validation errors.
|
|
4
|
+
``validation_error_code=400`` makes RouterVersioner register an exception handler
|
|
5
|
+
and update the OpenAPI schema to reflect the custom code automatically.
|
|
6
|
+
|
|
7
|
+
Run:
|
|
8
|
+
python examples/validation_error_code_app.py
|
|
9
|
+
|
|
10
|
+
Then open http://127.0.0.1:8007/v1_0/docs and try ``POST /v1_0/items`` with body
|
|
11
|
+
``{"name": "widget", "quantity": "not-a-number"}`` to see the 400 response from
|
|
12
|
+
the server (not a Swagger client-side message).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter, FastAPI
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CreateItemRequest(BaseModel):
|
|
22
|
+
name: str
|
|
23
|
+
quantity: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
app = FastAPI(
|
|
27
|
+
title="Validation Error Code — Automatic Handler",
|
|
28
|
+
description=(
|
|
29
|
+
"Validation errors return **400 Bad Request** instead of 422.\n\n"
|
|
30
|
+
'Try `POST /v1_0/items` with body `{"name": "widget", "quantity": "not-a-number"}` '
|
|
31
|
+
"to see the 400 response from the server."
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
router = APIRouter()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post("/items")
|
|
39
|
+
@api_version((1, 0))
|
|
40
|
+
def create_item(body: CreateItemRequest) -> dict[str, str]:
|
|
41
|
+
"""Create an item. ``name`` must be a string, ``quantity`` must be an integer."""
|
|
42
|
+
return {"name": body.name, "quantity": str(body.quantity)}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
RouterVersioner(
|
|
46
|
+
app=app,
|
|
47
|
+
routers=router,
|
|
48
|
+
version_format=VersionFormat.SEMVER,
|
|
49
|
+
validation_error_code=400,
|
|
50
|
+
# handle_validation_exceptions=True is the default: no extra code needed.
|
|
51
|
+
).versionize()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
import uvicorn
|
|
56
|
+
|
|
57
|
+
uvicorn.run(app, host="127.0.0.1", port=8007)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Demonstrate using a custom validation exception handler alongside validation_error_code.
|
|
2
|
+
|
|
3
|
+
Setting ``handle_validation_exceptions=False`` tells RouterVersioner to update the
|
|
4
|
+
OpenAPI schema (so it shows 400 instead of 422) without registering any handler.
|
|
5
|
+
You then register your own handler for full control over the response body.
|
|
6
|
+
|
|
7
|
+
Run:
|
|
8
|
+
python examples/validation_error_code_custom_handler_app.py
|
|
9
|
+
|
|
10
|
+
Then open http://127.0.0.1:8008/v1_0/docs and try ``POST /v1_0/orders`` with body
|
|
11
|
+
``{"name": "widget", "quantity": "not-a-number"}`` to see the custom 400 response
|
|
12
|
+
with field-level error messages.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter, FastAPI, Request
|
|
16
|
+
from fastapi.exceptions import RequestValidationError
|
|
17
|
+
from fastapi.responses import JSONResponse
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CreateOrderRequest(BaseModel):
|
|
24
|
+
name: str
|
|
25
|
+
quantity: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
app = FastAPI(
|
|
29
|
+
title="Validation Error Code — Custom Handler",
|
|
30
|
+
description=(
|
|
31
|
+
"Validation errors return **400 Bad Request** with a custom response body.\n\n"
|
|
32
|
+
'Try `POST /v1_0/orders` with body `{"name": "widget", "quantity": "not-a-number"}` '
|
|
33
|
+
"to see the custom handler response."
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
router = APIRouter()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.post("/orders")
|
|
41
|
+
@api_version((1, 0))
|
|
42
|
+
def create_order(body: CreateOrderRequest) -> dict[str, str]:
|
|
43
|
+
"""Create an order. ``name`` must be a string, ``quantity`` must be an integer."""
|
|
44
|
+
return {"name": body.name, "quantity": str(body.quantity)}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.exception_handler(RequestValidationError)
|
|
48
|
+
async def my_validation_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
49
|
+
"""Returns field-level error messages under an 'errors' key."""
|
|
50
|
+
return JSONResponse(
|
|
51
|
+
status_code=400,
|
|
52
|
+
content={
|
|
53
|
+
"errors": [{"field": e["loc"][-1], "message": e["msg"]} for e in exc.errors()],
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
RouterVersioner(
|
|
59
|
+
app=app,
|
|
60
|
+
routers=router,
|
|
61
|
+
version_format=VersionFormat.SEMVER,
|
|
62
|
+
validation_error_code=400,
|
|
63
|
+
handle_validation_exceptions=False, # OpenAPI schema shows 400; handler is yours.
|
|
64
|
+
).versionize()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
import uvicorn
|
|
69
|
+
|
|
70
|
+
uvicorn.run(app, host="127.0.0.1", port=8008)
|