fastapi-router-versioning 0.2.3__tar.gz → 0.2.5__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.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/pre-commit.yml +4 -2
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/test-redistribute.yml +2 -1
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/test.yml +32 -8
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/PKG-INFO +6 -3
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/README.md +5 -2
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/RELEASE_NOTES.md +12 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/fastapi_router_versioning/__init__.py +1 -1
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/fastapi_router_versioning/versioner.py +39 -4
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/tests/test_versioner.py +108 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/dependabot.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/labeler.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/create-draft-release.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/detect-conflicts.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/guard-dependencies.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/labeler.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/latest-changes.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/prepare-release.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/publish.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/zizmor.yml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.gitignore +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.pre-commit-config.yaml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.python-version +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/LICENSE +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/calver_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/download_static_assets.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/mounted_subapps_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/multi_router_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/openapi_hook_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/self_hosted_docs_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/semver_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/semver_major_only_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/validation_error_code_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/validation_error_code_custom_handler_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/webhook_versioning_app.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/fastapi_router_versioning/py.typed +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/pyproject.toml +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/scripts/format.sh +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/scripts/lint.sh +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/scripts/prepare_release.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/scripts/test-cov-html.sh +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/scripts/test-cov.sh +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/scripts/test.sh +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/tests/__init__.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/tests/test_calver.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/tests/test_fastapi_integration.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/tests/test_prepare_release.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/tests/test_semver.py +0 -0
- {fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/uv.lock +0 -0
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/pre-commit.yml
RENAMED
|
@@ -32,6 +32,8 @@ jobs:
|
|
|
32
32
|
- name: Setup uv
|
|
33
33
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
34
34
|
with:
|
|
35
|
+
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
|
|
36
|
+
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
|
|
35
37
|
version: "0.11.18"
|
|
36
38
|
cache-dependency-glob: |
|
|
37
39
|
pyproject.toml
|
|
@@ -44,8 +46,8 @@ jobs:
|
|
|
44
46
|
# Only lint files changed in this PR, not the entire repo
|
|
45
47
|
run: uv run prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure
|
|
46
48
|
|
|
47
|
-
#
|
|
48
|
-
pre-commit-alls-green:
|
|
49
|
+
# https://github.com/marketplace/actions/alls-green#why
|
|
50
|
+
pre-commit-alls-green: # This job does nothing and is only used for the branch protection
|
|
49
51
|
if: always()
|
|
50
52
|
needs:
|
|
51
53
|
- pre-commit
|
|
@@ -12,6 +12,7 @@ jobs:
|
|
|
12
12
|
changes:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
permissions:
|
|
15
|
+
contents: read
|
|
15
16
|
pull-requests: read
|
|
16
17
|
timeout-minutes: 5
|
|
17
18
|
outputs:
|
|
@@ -74,7 +75,7 @@ jobs:
|
|
|
74
75
|
pip wheel --no-deps fastapi_router_versioning*.tar.gz
|
|
75
76
|
|
|
76
77
|
# https://github.com/marketplace/actions/alls-green#why
|
|
77
|
-
test-redistribute-alls-green:
|
|
78
|
+
test-redistribute-alls-green: # This job does nothing and is only used for the branch protection
|
|
78
79
|
if: always()
|
|
79
80
|
needs:
|
|
80
81
|
- test-redistribute
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/test.yml
RENAMED
|
@@ -6,6 +6,7 @@ on:
|
|
|
6
6
|
- main
|
|
7
7
|
pull_request:
|
|
8
8
|
schedule:
|
|
9
|
+
# cron every week on monday
|
|
9
10
|
- cron: "0 0 * * 1"
|
|
10
11
|
|
|
11
12
|
permissions: {}
|
|
@@ -16,15 +17,19 @@ env:
|
|
|
16
17
|
jobs:
|
|
17
18
|
changes:
|
|
18
19
|
runs-on: ubuntu-latest
|
|
20
|
+
# Required permissions
|
|
19
21
|
permissions:
|
|
22
|
+
contents: read
|
|
20
23
|
pull-requests: read
|
|
21
24
|
timeout-minutes: 5
|
|
25
|
+
# Set job outputs to values from filter step
|
|
22
26
|
outputs:
|
|
23
27
|
src: ${{ steps.filter.outputs.src }}
|
|
24
28
|
steps:
|
|
25
29
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
26
30
|
with:
|
|
27
31
|
persist-credentials: false
|
|
32
|
+
# For pull requests it's not necessary to checkout the code but for the main branch it is
|
|
28
33
|
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
|
29
34
|
id: filter
|
|
30
35
|
with:
|
|
@@ -79,14 +84,16 @@ jobs:
|
|
|
79
84
|
- name: Setup uv
|
|
80
85
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
81
86
|
with:
|
|
87
|
+
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
|
|
88
|
+
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
|
|
82
89
|
version: "0.11.18"
|
|
83
90
|
enable-cache: true
|
|
84
|
-
cache-suffix: ${{ matrix.
|
|
91
|
+
cache-suffix: ${{ matrix.python-version }}-fa${{ matrix.fastapi-version }}
|
|
85
92
|
cache-dependency-glob: |
|
|
86
93
|
pyproject.toml
|
|
87
94
|
uv.lock
|
|
88
95
|
|
|
89
|
-
- name: Install
|
|
96
|
+
- name: Install Dependencies
|
|
90
97
|
run: uv sync
|
|
91
98
|
|
|
92
99
|
- name: Override FastAPI version
|
|
@@ -97,17 +104,16 @@ jobs:
|
|
|
97
104
|
run: mkdir coverage
|
|
98
105
|
|
|
99
106
|
- name: Test
|
|
100
|
-
# GitHub Windows runners ship bash natively
|
|
101
107
|
run: uv run --no-sync bash scripts/test-cov.sh
|
|
102
108
|
env:
|
|
103
|
-
COVERAGE_FILE: coverage/.coverage
|
|
104
|
-
CONTEXT:
|
|
109
|
+
COVERAGE_FILE: coverage/.coverage.py${{ matrix.python-version }}-fa${{ matrix.fastapi-version }}
|
|
110
|
+
CONTEXT: py${{ matrix.python-version }}-fa${{ matrix.fastapi-version }}
|
|
105
111
|
|
|
106
112
|
- name: Store coverage files
|
|
107
113
|
if: matrix.coverage == 'coverage'
|
|
108
114
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
109
115
|
with:
|
|
110
|
-
name: coverage-${{
|
|
116
|
+
name: coverage-${{ matrix.python-version }}-${{ matrix.fastapi-version }}
|
|
111
117
|
path: coverage
|
|
112
118
|
include-hidden-files: true
|
|
113
119
|
|
|
@@ -119,6 +125,11 @@ jobs:
|
|
|
119
125
|
permissions:
|
|
120
126
|
contents: read
|
|
121
127
|
steps:
|
|
128
|
+
- name: Dump GitHub context
|
|
129
|
+
env:
|
|
130
|
+
GITHUB_CONTEXT: ${{ toJson(github) }}
|
|
131
|
+
run: echo "$GITHUB_CONTEXT"
|
|
132
|
+
|
|
122
133
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
123
134
|
with:
|
|
124
135
|
persist-credentials: false
|
|
@@ -126,6 +137,8 @@ jobs:
|
|
|
126
137
|
- name: Setup uv
|
|
127
138
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
128
139
|
with:
|
|
140
|
+
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
|
|
141
|
+
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
|
|
129
142
|
version: "0.11.18"
|
|
130
143
|
enable-cache: true
|
|
131
144
|
cache-dependency-glob: |
|
|
@@ -145,6 +158,7 @@ jobs:
|
|
|
145
158
|
- run: ls -la coverage
|
|
146
159
|
- run: uv run coverage combine coverage
|
|
147
160
|
- run: uv run coverage html --title "Coverage for ${{ github.sha }}"
|
|
161
|
+
- run: uv run coverage xml
|
|
148
162
|
|
|
149
163
|
- name: Store coverage HTML
|
|
150
164
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
@@ -153,18 +167,28 @@ jobs:
|
|
|
153
167
|
path: htmlcov
|
|
154
168
|
include-hidden-files: true
|
|
155
169
|
|
|
170
|
+
- name: Upload coverage to Codecov
|
|
171
|
+
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
|
172
|
+
with:
|
|
173
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
174
|
+
files: coverage.xml
|
|
175
|
+
fail_ci_if_error: true
|
|
176
|
+
|
|
156
177
|
- run: uv run coverage report --fail-under=100
|
|
157
178
|
|
|
158
179
|
# https://github.com/marketplace/actions/alls-green#why
|
|
159
|
-
test-alls-green:
|
|
180
|
+
test-alls-green: # This job does nothing and is only used for the branch protection
|
|
160
181
|
if: always()
|
|
161
182
|
needs:
|
|
162
183
|
- test
|
|
163
184
|
- coverage-combine
|
|
164
185
|
runs-on: ubuntu-latest
|
|
165
186
|
timeout-minutes: 5
|
|
166
|
-
permissions: {}
|
|
167
187
|
steps:
|
|
188
|
+
- name: Dump GitHub context
|
|
189
|
+
env:
|
|
190
|
+
GITHUB_CONTEXT: ${{ toJson(github) }}
|
|
191
|
+
run: echo "$GITHUB_CONTEXT"
|
|
168
192
|
- name: Decide whether the needed jobs succeeded or failed
|
|
169
193
|
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
|
|
170
194
|
with:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-router-versioning
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
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
|
|
@@ -32,7 +32,8 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
# FastAPI Router Versioning
|
|
33
33
|
|
|
34
34
|
[](https://github.com/mat81black/fastapi-router-versioning/actions)
|
|
35
|
-
[](https://codecov.io/github/mat81black/fastapi-router-versioning)
|
|
36
|
+
[](https://pypi.org/project/fastapi-router-versioning/)
|
|
36
37
|
[](https://pypi.org/project/fastapi-router-versioning/)
|
|
37
38
|
[](LICENSE)
|
|
38
39
|
|
|
@@ -182,7 +183,9 @@ A route without `@api_version` isn't excluded, it falls back to `default_version
|
|
|
182
183
|
| `validation_error_code` | `int` | `422` | Status code returned for request validation errors; also replaces the `422` entry everywhere it appears in the OpenAPI schema, root schema included |
|
|
183
184
|
| `handle_validation_exceptions` | `bool` | `True` | Set `False` to only patch the schema and register your own `RequestValidationError` handler |
|
|
184
185
|
|
|
185
|
-
`.versionize()` returns the list of versions it activated.
|
|
186
|
+
`.versionize()` returns the list of versions it activated. It can only be called once per
|
|
187
|
+
instance; a second call raises `RuntimeError`, since it mutates the live FastAPI app in a way
|
|
188
|
+
that can't be undone.
|
|
186
189
|
|
|
187
190
|
---
|
|
188
191
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# FastAPI Router Versioning
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mat81black/fastapi-router-versioning/actions)
|
|
4
|
-
[](https://codecov.io/github/mat81black/fastapi-router-versioning)
|
|
5
|
+
[](https://pypi.org/project/fastapi-router-versioning/)
|
|
5
6
|
[](https://pypi.org/project/fastapi-router-versioning/)
|
|
6
7
|
[](LICENSE)
|
|
7
8
|
|
|
@@ -151,7 +152,9 @@ A route without `@api_version` isn't excluded, it falls back to `default_version
|
|
|
151
152
|
| `validation_error_code` | `int` | `422` | Status code returned for request validation errors; also replaces the `422` entry everywhere it appears in the OpenAPI schema, root schema included |
|
|
152
153
|
| `handle_validation_exceptions` | `bool` | `True` | Set `False` to only patch the schema and register your own `RequestValidationError` handler |
|
|
153
154
|
|
|
154
|
-
`.versionize()` returns the list of versions it activated.
|
|
155
|
+
`.versionize()` returns the list of versions it activated. It can only be called once per
|
|
156
|
+
instance; a second call raises `RuntimeError`, since it mutates the live FastAPI app in a way
|
|
157
|
+
that can't be undone.
|
|
155
158
|
|
|
156
159
|
---
|
|
157
160
|
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Latest Changes
|
|
4
4
|
|
|
5
|
+
## 0.2.5 (2026-07-10)
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
* 🐛 Fix misleading self-collision error in RouterVersioner prefix claiming. PR [#36](https://github.com/mat81black/fastapi-router-versioning/pull/36) by [@mat81black](https://github.com/mat81black).
|
|
10
|
+
|
|
11
|
+
## 0.2.4 (2026-07-09)
|
|
12
|
+
|
|
13
|
+
### Internal
|
|
14
|
+
|
|
15
|
+
* 🔧 Integrate Codecov for coverage tracking and update README. PR [#34](https://github.com/mat81black/fastapi-router-versioning/pull/34) by [@mat81black](https://github.com/mat81black).
|
|
16
|
+
|
|
5
17
|
## 0.2.3 (2026-07-09)
|
|
6
18
|
|
|
7
19
|
### Docs
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import itertools
|
|
2
3
|
|
|
3
4
|
from collections import defaultdict
|
|
4
5
|
from collections.abc import Callable, Iterator
|
|
@@ -33,6 +34,12 @@ _ATTR_API_VERSION = "_api_version"
|
|
|
33
34
|
_ATTR_DEPRECATE_IN = "_deprecate_in_version"
|
|
34
35
|
_ATTR_REMOVE_IN = "_remove_in_version"
|
|
35
36
|
|
|
37
|
+
# Unique per-instance token for _claim_prefix, so it can tell apart a prefix collision
|
|
38
|
+
# caused by this same RouterVersioner instance (e.g. a degenerate prefix_format) from one
|
|
39
|
+
# caused by a different instance sharing the app. Not id(self): that can be reused after
|
|
40
|
+
# garbage collection for the common `RouterVersioner(...).versionize()` one-liner pattern.
|
|
41
|
+
_instance_counter = itertools.count()
|
|
42
|
+
|
|
36
43
|
|
|
37
44
|
class VersionFormat(str, Enum):
|
|
38
45
|
"""
|
|
@@ -194,6 +201,8 @@ class RouterVersioner:
|
|
|
194
201
|
|
|
195
202
|
self._validation_error_code = validation_error_code
|
|
196
203
|
self._handle_validation_exceptions = handle_validation_exceptions
|
|
204
|
+
self._versionized = False
|
|
205
|
+
self._instance_token = next(_instance_counter)
|
|
197
206
|
|
|
198
207
|
if self._validation_error_code != 422 and self._handle_validation_exceptions:
|
|
199
208
|
self._register_validation_exception_handler()
|
|
@@ -345,6 +354,24 @@ class RouterVersioner:
|
|
|
345
354
|
return None
|
|
346
355
|
|
|
347
356
|
def versionize(self) -> list[VersionT]:
|
|
357
|
+
"""
|
|
358
|
+
Reads the configured routers, groups their routes by version, and mounts one
|
|
359
|
+
versioned router per active version on the app.
|
|
360
|
+
|
|
361
|
+
May only be called once per instance: it mutates the live FastAPI app (including
|
|
362
|
+
routers, registering routes) in a way that cannot be undone, so a second call
|
|
363
|
+
would either duplicate routes or, if a version prefix was already claimed, raise.
|
|
364
|
+
Create a new RouterVersioner if you need to versionize again.
|
|
365
|
+
|
|
366
|
+
:return: The list of versions that were actually mounted.
|
|
367
|
+
"""
|
|
368
|
+
if self._versionized:
|
|
369
|
+
raise RuntimeError(
|
|
370
|
+
"versionize() was already called on this RouterVersioner instance. "
|
|
371
|
+
"Create a new RouterVersioner if you need to versionize a router again."
|
|
372
|
+
)
|
|
373
|
+
self._versionized = True
|
|
374
|
+
|
|
348
375
|
latest_version: VersionT | None = None
|
|
349
376
|
latest_routes: dict[tuple[str, str], Any] = {}
|
|
350
377
|
latest_webhooks: list[Any] = []
|
|
@@ -450,19 +477,27 @@ class RouterVersioner:
|
|
|
450
477
|
return {version: list(routes.values()) for version, routes in self._resolve_lifecycle(all_webhooks).items()}
|
|
451
478
|
|
|
452
479
|
def _claim_prefix(self, prefix: str) -> None:
|
|
453
|
-
claimed:
|
|
480
|
+
claimed: dict[str, int] | None = getattr(self._app.state, "_router_versioner_claimed_prefixes", None)
|
|
454
481
|
if claimed is None:
|
|
455
|
-
claimed =
|
|
482
|
+
claimed = {}
|
|
456
483
|
self._app.state._router_versioner_claimed_prefixes = claimed
|
|
457
484
|
|
|
458
|
-
|
|
485
|
+
owner = claimed.get(prefix)
|
|
486
|
+
if owner is not None:
|
|
487
|
+
if owner == self._instance_token:
|
|
488
|
+
raise RuntimeError(
|
|
489
|
+
f"Prefix '{prefix}' was already claimed by this same RouterVersioner instance. "
|
|
490
|
+
"This usually means prefix_format doesn't use {major}/{minor}/{version} and "
|
|
491
|
+
"produces the same prefix for multiple versions, or latest_prefix collides "
|
|
492
|
+
"with an already-active version prefix."
|
|
493
|
+
)
|
|
459
494
|
raise RuntimeError(
|
|
460
495
|
f"Prefix '{prefix}' is already used by another RouterVersioner attached to this app. "
|
|
461
496
|
"Two RouterVersioner instances sharing the same app must use distinct "
|
|
462
497
|
"prefix_format/latest_prefix values, otherwise their docs/openapi routes silently "
|
|
463
498
|
"shadow each other."
|
|
464
499
|
)
|
|
465
|
-
claimed.
|
|
500
|
+
claimed[prefix] = self._instance_token
|
|
466
501
|
|
|
467
502
|
def _resolve_webhooks_for_version(
|
|
468
503
|
self, version: VersionT, webhooks_by_version: dict[VersionT, list[Any]]
|
|
@@ -687,6 +687,29 @@ def test_default_version_mixed_with_explicitly_decorated_routes() -> None:
|
|
|
687
687
|
assert client.get("/v3_0/explicit").status_code == 200
|
|
688
688
|
|
|
689
689
|
|
|
690
|
+
def test_routers_as_list() -> None:
|
|
691
|
+
"""routers accepts a list of APIRouter, not just a single one (multi-router support)."""
|
|
692
|
+
app = FastAPI()
|
|
693
|
+
router1 = APIRouter()
|
|
694
|
+
router2 = APIRouter()
|
|
695
|
+
|
|
696
|
+
@router1.get("/a")
|
|
697
|
+
@api_version((1, 0))
|
|
698
|
+
def route_a() -> dict[str, str]:
|
|
699
|
+
return {"msg": "a"}
|
|
700
|
+
|
|
701
|
+
@router2.get("/b")
|
|
702
|
+
@api_version((1, 0))
|
|
703
|
+
def route_b() -> dict[str, str]:
|
|
704
|
+
return {"msg": "b"}
|
|
705
|
+
|
|
706
|
+
RouterVersioner(app=app, routers=[router1, router2], version_format=VersionFormat.SEMVER).versionize()
|
|
707
|
+
|
|
708
|
+
client = TestClient(app)
|
|
709
|
+
assert client.get("/v1_0/a").status_code == 200
|
|
710
|
+
assert client.get("/v1_0/b").status_code == 200
|
|
711
|
+
|
|
712
|
+
|
|
690
713
|
def test_openapi_callbacks_are_propagated_to_versioned_routes() -> None:
|
|
691
714
|
"""OpenAPI callbacks defined on a route are propagated to every versioned copy of that route."""
|
|
692
715
|
app = FastAPI()
|
|
@@ -815,6 +838,38 @@ def test_webhook_routers_none_falls_back_to_app_webhooks() -> None:
|
|
|
815
838
|
assert "/global-event" in schema_v1.get("webhooks", {})
|
|
816
839
|
|
|
817
840
|
|
|
841
|
+
def test_webhook_routers_as_list() -> None:
|
|
842
|
+
"""webhook_routers accepts a list of APIRouter, not just a single one."""
|
|
843
|
+
app = FastAPI()
|
|
844
|
+
router = APIRouter()
|
|
845
|
+
webhook_router1 = APIRouter()
|
|
846
|
+
webhook_router2 = APIRouter()
|
|
847
|
+
|
|
848
|
+
@router.get("/items")
|
|
849
|
+
@api_version((1, 0))
|
|
850
|
+
def get_items() -> dict[str, str]: ...
|
|
851
|
+
|
|
852
|
+
@webhook_router1.post("/hook-a")
|
|
853
|
+
@api_version((1, 0))
|
|
854
|
+
def hook_a(body: dict[str, str]) -> None: ...
|
|
855
|
+
|
|
856
|
+
@webhook_router2.post("/hook-b")
|
|
857
|
+
@api_version((1, 0))
|
|
858
|
+
def hook_b(body: dict[str, str]) -> None: ...
|
|
859
|
+
|
|
860
|
+
RouterVersioner(
|
|
861
|
+
app=app,
|
|
862
|
+
routers=router,
|
|
863
|
+
webhook_routers=[webhook_router1, webhook_router2],
|
|
864
|
+
version_format=VersionFormat.SEMVER,
|
|
865
|
+
).versionize()
|
|
866
|
+
|
|
867
|
+
client = TestClient(app)
|
|
868
|
+
schema = client.get("/v1_0/openapi.json").json()
|
|
869
|
+
assert "/hook-a" in schema.get("webhooks", {})
|
|
870
|
+
assert "/hook-b" in schema.get("webhooks", {})
|
|
871
|
+
|
|
872
|
+
|
|
818
873
|
def test_webhook_routers_no_webhook_before_first_version() -> None:
|
|
819
874
|
"""When all webhook versions are higher than the current route version, returns no webhooks."""
|
|
820
875
|
app = FastAPI()
|
|
@@ -1281,6 +1336,59 @@ def test_duplicate_version_prefix_across_versioners_raises() -> None:
|
|
|
1281
1336
|
RouterVersioner(app=app, routers=router2, version_format=VersionFormat.SEMVER).versionize()
|
|
1282
1337
|
|
|
1283
1338
|
|
|
1339
|
+
def test_degenerate_prefix_format_self_collision_raises() -> None:
|
|
1340
|
+
"""A prefix_format without {major}/{minor}/{version} placeholders makes every version
|
|
1341
|
+
resolve to the same prefix. This is a self-collision on a single instance, not a clash
|
|
1342
|
+
with another RouterVersioner, so the error message must say so explicitly."""
|
|
1343
|
+
app = FastAPI()
|
|
1344
|
+
router = APIRouter()
|
|
1345
|
+
|
|
1346
|
+
@router.get("/items")
|
|
1347
|
+
@api_version((1, 0))
|
|
1348
|
+
def get_items_v1() -> dict[str, str]: ...
|
|
1349
|
+
|
|
1350
|
+
@router.get("/items")
|
|
1351
|
+
@api_version((2, 0))
|
|
1352
|
+
def get_items_v2() -> dict[str, str]: ...
|
|
1353
|
+
|
|
1354
|
+
with pytest.raises(RuntimeError, match="already claimed by this same RouterVersioner instance"):
|
|
1355
|
+
RouterVersioner(app=app, routers=router, version_format=VersionFormat.SEMVER, prefix_format="/api").versionize()
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def test_latest_prefix_collides_with_own_version_prefix_raises() -> None:
|
|
1359
|
+
"""latest_prefix accidentally set to the same value as an actual version's own prefix is
|
|
1360
|
+
a self-collision on a single instance, not a clash with another RouterVersioner."""
|
|
1361
|
+
app = FastAPI()
|
|
1362
|
+
router = APIRouter()
|
|
1363
|
+
|
|
1364
|
+
@router.get("/items")
|
|
1365
|
+
@api_version((1, 0))
|
|
1366
|
+
def get_items() -> dict[str, str]: ...
|
|
1367
|
+
|
|
1368
|
+
with pytest.raises(RuntimeError, match="already claimed by this same RouterVersioner instance"):
|
|
1369
|
+
RouterVersioner(
|
|
1370
|
+
app=app, routers=router, version_format=VersionFormat.SEMVER, latest_prefix="/v1_0"
|
|
1371
|
+
).versionize()
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
def test_calling_versionize_twice_on_same_instance_raises() -> None:
|
|
1375
|
+
"""Calling .versionize() a second time on the same instance must raise a clear,
|
|
1376
|
+
self-explanatory error instead of the misleading '...used by another RouterVersioner'
|
|
1377
|
+
message that _claim_prefix would otherwise produce (it's the same instance, not another)."""
|
|
1378
|
+
app = FastAPI()
|
|
1379
|
+
router = APIRouter()
|
|
1380
|
+
|
|
1381
|
+
@router.get("/items")
|
|
1382
|
+
@api_version((1, 0))
|
|
1383
|
+
def get_items() -> dict[str, str]: ...
|
|
1384
|
+
|
|
1385
|
+
versioner = RouterVersioner(app=app, routers=router, version_format=VersionFormat.SEMVER)
|
|
1386
|
+
versioner.versionize()
|
|
1387
|
+
|
|
1388
|
+
with pytest.raises(RuntimeError, match="versionize\\(\\) was already called on this RouterVersioner instance"):
|
|
1389
|
+
versioner.versionize()
|
|
1390
|
+
|
|
1391
|
+
|
|
1284
1392
|
def test_duplicate_latest_prefix_across_versioners_raises() -> None:
|
|
1285
1393
|
"""Two RouterVersioners sharing the same latest_prefix on the same app must fail fast."""
|
|
1286
1394
|
app = FastAPI()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/labeler.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/.github/workflows/zizmor.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/mounted_subapps_app.py
RENAMED
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/multi_router_app.py
RENAMED
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/openapi_hook_app.py
RENAMED
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/examples/self_hosted_docs_app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/scripts/prepare_release.py
RENAMED
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/scripts/test-cov-html.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_router_versioning-0.2.3 → fastapi_router_versioning-0.2.5}/tests/test_prepare_release.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|