fastapi-router-versioning 1.1.0__tar.gz → 1.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.
Files changed (137) hide show
  1. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/latest-changes.yml +5 -1
  2. fastapi_router_versioning-1.2.0/.github/workflows/build-docs.yml +97 -0
  3. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/bump-pre-commit-hooks.yml +1 -1
  4. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/create-draft-release.yml +2 -2
  5. fastapi_router_versioning-1.2.0/.github/workflows/deploy-docs.yml +58 -0
  6. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/pre-commit.yml +1 -1
  7. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/prepare-release.yml +2 -2
  8. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/publish.yml +1 -1
  9. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/test.yml +5 -4
  10. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/zizmor.yml +1 -1
  11. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.gitignore +4 -0
  12. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.pre-commit-config.yaml +9 -1
  13. fastapi_router_versioning-1.2.0/PKG-INFO +137 -0
  14. fastapi_router_versioning-1.2.0/README.md +105 -0
  15. fastapi_router_versioning-1.2.0/docs/advanced/callback-hook.md +18 -0
  16. fastapi_router_versioning-1.2.0/docs/advanced/callbacks-and-webhooks.md +33 -0
  17. fastapi_router_versioning-1.2.0/docs/advanced/custom-url-format.md +25 -0
  18. fastapi_router_versioning-1.2.0/docs/advanced/deprecation-headers.md +36 -0
  19. fastapi_router_versioning-1.2.0/docs/advanced/index.md +19 -0
  20. fastapi_router_versioning-1.2.0/docs/advanced/latest-alias.md +17 -0
  21. fastapi_router_versioning-1.2.0/docs/advanced/multiple-routers.md +28 -0
  22. fastapi_router_versioning-1.2.0/docs/advanced/openapi-hook.md +13 -0
  23. fastapi_router_versioning-1.2.0/docs/advanced/reverse-proxy-and-sub-apps.md +17 -0
  24. fastapi_router_versioning-1.2.0/docs/advanced/self-hosted-assets.md +15 -0
  25. fastapi_router_versioning-1.2.0/docs/advanced/validation-error-status-code.md +22 -0
  26. fastapi_router_versioning-1.2.0/docs/advanced/version-discovery.md +49 -0
  27. fastapi_router_versioning-1.2.0/docs/assets/images/apple-touch-icon.png +0 -0
  28. fastapi_router_versioning-1.2.0/docs/assets/images/favicon.ico +0 -0
  29. fastapi_router_versioning-1.2.0/docs/assets/images/favicon.png +0 -0
  30. fastapi_router_versioning-1.2.0/docs/assets/images/favicon.svg +1 -0
  31. fastapi_router_versioning-1.2.0/docs/assets/images/logo-header.svg +1 -0
  32. fastapi_router_versioning-1.2.0/docs/assets/images/logo.svg +1 -0
  33. fastapi_router_versioning-1.2.0/docs/assets/images/wordmark.png +0 -0
  34. fastapi_router_versioning-1.2.0/docs/assets/screenshots/latest-alias.jpg +0 -0
  35. fastapi_router_versioning-1.2.0/docs/assets/screenshots/quickstart-calver.jpg +0 -0
  36. fastapi_router_versioning-1.2.0/docs/assets/screenshots/quickstart-semver-v1.jpg +0 -0
  37. fastapi_router_versioning-1.2.0/docs/assets/screenshots/quickstart-semver-v2.jpg +0 -0
  38. fastapi_router_versioning-1.2.0/docs/assets/screenshots/redoc-v2.jpg +0 -0
  39. fastapi_router_versioning-1.2.0/docs/assets/screenshots/swagger-v1.jpg +0 -0
  40. fastapi_router_versioning-1.2.0/docs/assets/screenshots/swagger-v2.jpg +0 -0
  41. fastapi_router_versioning-1.2.0/docs/assets/screenshots/url-format-latest.jpg +0 -0
  42. fastapi_router_versioning-1.2.0/docs/assets/screenshots/url-format-v1.jpg +0 -0
  43. fastapi_router_versioning-1.2.0/docs/assets/screenshots/versions-dashboard.jpg +0 -0
  44. fastapi_router_versioning-1.2.0/docs/assets/screenshots/webhooks-v1.jpg +0 -0
  45. fastapi_router_versioning-1.2.0/docs/assets/screenshots/webhooks-v2.jpg +0 -0
  46. fastapi_router_versioning-1.2.0/docs/examples.md +131 -0
  47. fastapi_router_versioning-1.2.0/docs/how-it-works.md +62 -0
  48. fastapi_router_versioning-1.2.0/docs/index.md +68 -0
  49. fastapi_router_versioning-1.2.0/docs/lifecycle.md +61 -0
  50. fastapi_router_versioning-1.2.0/docs/quickstart.md +44 -0
  51. fastapi_router_versioning-1.2.0/docs/reference.md +18 -0
  52. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0/docs}/release-notes.md +18 -0
  53. fastapi_router_versioning-1.2.0/docs/stylesheets/extra.css +31 -0
  54. fastapi_router_versioning-1.2.0/docs_src/advanced/callback_hook.py +31 -0
  55. fastapi_router_versioning-1.2.0/docs_src/advanced/callbacks.py +20 -0
  56. fastapi_router_versioning-1.2.0/docs_src/advanced/custom_url_format.py +30 -0
  57. fastapi_router_versioning-1.2.0/docs_src/advanced/deprecation_headers.py +38 -0
  58. fastapi_router_versioning-1.2.0/docs_src/advanced/latest_alias.py +28 -0
  59. fastapi_router_versioning-1.2.0/docs_src/advanced/multiple_routers.py +28 -0
  60. fastapi_router_versioning-1.2.0/docs_src/advanced/openapi_hook.py +37 -0
  61. fastapi_router_versioning-1.2.0/docs_src/advanced/reverse_proxy.py +28 -0
  62. fastapi_router_versioning-1.2.0/docs_src/advanced/self_hosted_assets.py +33 -0
  63. fastapi_router_versioning-1.2.0/docs_src/advanced/validation_error_status_code.py +48 -0
  64. fastapi_router_versioning-1.2.0/docs_src/advanced/version_discovery.py +28 -0
  65. fastapi_router_versioning-1.2.0/docs_src/advanced/webhooks.py +54 -0
  66. fastapi_router_versioning-1.2.0/docs_src/lifecycle/legacy_route.py +16 -0
  67. fastapi_router_versioning-1.2.0/docs_src/quickstart/calver.py +19 -0
  68. fastapi_router_versioning-1.2.0/docs_src/quickstart/semver.py +25 -0
  69. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/__init__.py +1 -1
  70. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_versions.py +31 -24
  71. fastapi_router_versioning-1.2.0/fastapi_router_versioning/versioner.py +286 -0
  72. fastapi_router_versioning-1.2.0/mkdocs.yml +129 -0
  73. fastapi_router_versioning-1.2.0/overrides/main.html +7 -0
  74. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/pyproject.toml +25 -3
  75. fastapi_router_versioning-1.2.0/scripts/build_icons.py +83 -0
  76. fastapi_router_versioning-1.2.0/scripts/docs-build.sh +6 -0
  77. fastapi_router_versioning-1.2.0/scripts/docs.sh +6 -0
  78. fastapi_router_versioning-1.2.0/scripts/docs_screenshots.py +144 -0
  79. fastapi_router_versioning-1.2.0/scripts/generate_readme.py +174 -0
  80. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/scripts/lint.sh +1 -1
  81. fastapi_router_versioning-1.2.0/tests/test_docs_src.py +55 -0
  82. fastapi_router_versioning-1.2.0/tests/test_docs_src_behavior.py +178 -0
  83. fastapi_router_versioning-1.2.0/tests/test_public_signature_docs.py +58 -0
  84. fastapi_router_versioning-1.2.0/tests/test_readme.py +10 -0
  85. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/uv.lock +688 -107
  86. fastapi_router_versioning-1.1.0/PKG-INFO +0 -622
  87. fastapi_router_versioning-1.1.0/README.md +0 -591
  88. fastapi_router_versioning-1.1.0/fastapi_router_versioning/versioner.py +0 -261
  89. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/dependabot.yml +0 -0
  90. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/detect-conflicts.yml +0 -0
  91. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/guard-dependencies.yml +0 -0
  92. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.github/workflows/test-redistribute.yml +0 -0
  93. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/.python-version +0 -0
  94. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/LICENSE +0 -0
  95. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/calver_app.py +0 -0
  96. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/dashboard_assets/versions_dashboard.css +0 -0
  97. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/dashboard_assets/versions_dashboard.html +0 -0
  98. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/deprecation_headers_app.py +0 -0
  99. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/download_static_assets.py +0 -0
  100. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/mounted_subapps_app.py +0 -0
  101. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/multi_router_app.py +0 -0
  102. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/openapi_hook_app.py +0 -0
  103. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/self_hosted_docs_app.py +0 -0
  104. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/semver_app.py +0 -0
  105. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/semver_major_only_app.py +0 -0
  106. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/validation_override_integration_app.py +0 -0
  107. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/versions_dashboard_app.py +0 -0
  108. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/versions_dashboard_hook_app.py +0 -0
  109. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/examples/webhook_versioning_app.py +0 -0
  110. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_builder.py +0 -0
  111. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_compat.py +0 -0
  112. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_dashboard.py +0 -0
  113. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_deprecation.py +0 -0
  114. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_docs.py +0 -0
  115. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_lifecycle.py +0 -0
  116. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_registry.py +0 -0
  117. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/_scheme.py +0 -0
  118. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/fastapi_router_versioning/py.typed +0 -0
  119. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/scripts/format.sh +0 -0
  120. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/scripts/prepare_release.py +0 -0
  121. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/scripts/test-cov-html.sh +0 -0
  122. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/scripts/test-cov.sh +0 -0
  123. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/scripts/test.sh +0 -0
  124. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/__init__.py +0 -0
  125. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_calver.py +0 -0
  126. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_examples.py +0 -0
  127. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_fastapi_integration.py +0 -0
  128. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_prepare_release.py +0 -0
  129. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_semver.py +0 -0
  130. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_versioner.py +0 -0
  131. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_versioner_deprecation_headers.py +0 -0
  132. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_versioner_docs.py +0 -0
  133. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_versioner_errors.py +0 -0
  134. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_versioner_lifecycle.py +0 -0
  135. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_versioner_versions_dashboard.py +0 -0
  136. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_versioner_versions_route.py +0 -0
  137. {fastapi_router_versioning-1.1.0 → fastapi_router_versioning-1.2.0}/tests/test_versioner_webhooks.py +0 -0
@@ -1,7 +1,11 @@
1
1
  auto-labels:
2
2
  docs:
3
3
  - README.md
4
- - release-notes.md
4
+ - docs/**
5
+ - docs_src/**
6
+ - examples/**
7
+ - mkdocs.yml
8
+ - overrides/**
5
9
  internal:
6
10
  - .github/**
7
11
  - scripts/**
@@ -0,0 +1,97 @@
1
+ name: Build Docs
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ changes:
13
+ runs-on: ubuntu-latest
14
+ # Required permissions
15
+ permissions:
16
+ pull-requests: read
17
+ timeout-minutes: 5
18
+ # Set job outputs to values from filter step
19
+ outputs:
20
+ docs: ${{ steps.filter.outputs.docs }}
21
+ steps:
22
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 #sync-pin:checkout-6
23
+ with:
24
+ persist-credentials: false
25
+ # For pull requests it's not necessary to checkout the code but for the main branch it is
26
+ - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.2 #sync-pin:paths-filter-2
27
+ id: filter
28
+ with:
29
+ filters: |
30
+ docs:
31
+ - .github/workflows/build-docs.yml
32
+ - .github/workflows/deploy-docs.yml
33
+ - docs/**
34
+ - docs_src/**
35
+ - examples/**
36
+ - fastapi_router_versioning/**
37
+ - mkdocs.yml
38
+ - overrides/**
39
+ - scripts/docs-build.sh
40
+ - pyproject.toml
41
+ - uv.lock
42
+
43
+ build-docs:
44
+ needs:
45
+ - changes
46
+ if: needs.changes.outputs.docs == 'true'
47
+ runs-on: ubuntu-latest
48
+ timeout-minutes: 10
49
+ steps:
50
+ - name: Dump GitHub context
51
+ env:
52
+ GITHUB_CONTEXT: ${{ toJson(github) }}
53
+ run: echo "$GITHUB_CONTEXT"
54
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 #sync-pin:checkout-7
55
+ with:
56
+ persist-credentials: false
57
+
58
+ - name: Set up Python
59
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 #sync-pin:setup-python-3
60
+ with:
61
+ python-version-file: .python-version
62
+
63
+ - name: Setup uv
64
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv-4
65
+ with:
66
+ # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
67
+ # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
68
+ version: "0.11.18"
69
+ # Zensical's own docs advise against caching its build output on CI for now.
70
+ enable-cache: false
71
+
72
+ - name: Install Dependencies
73
+ run: uv sync --locked --no-dev --group docs
74
+
75
+ - name: Build docs
76
+ run: uv run --no-sync bash scripts/docs-build.sh
77
+
78
+ # The site is deployed by Deploy Docs, which runs once this workflow has finished
79
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 #sync-pin:upload-artifact-3
80
+ with:
81
+ name: docs-site
82
+ path: ./site
83
+
84
+ # https://github.com/marketplace/actions/alls-green#why
85
+ docs-alls-green: # This job does nothing and is only used for the branch protection
86
+ if: always()
87
+ needs:
88
+ - changes
89
+ - build-docs
90
+ runs-on: ubuntu-latest
91
+ timeout-minutes: 5
92
+ steps:
93
+ - name: Decide whether the needed jobs succeeded or failed
94
+ uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.2.2 #sync-pin:alls-green-2
95
+ with:
96
+ jobs: ${{ toJSON(needs) }}
97
+ allowed-skips: build-docs
@@ -25,7 +25,7 @@ jobs:
25
25
  with:
26
26
  python-version-file: ".python-version"
27
27
  - name: Setup uv
28
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v9.0.0 #sync-pin:setup-uv
28
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv
29
29
  with:
30
30
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
31
31
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -16,7 +16,7 @@ jobs:
16
16
  contents: write
17
17
  env:
18
18
  PREPARE_RELEASE_VERSION_FILE: fastapi_router_versioning/__init__.py
19
- PREPARE_RELEASE_RELEASE_NOTES_FILE: release-notes.md
19
+ PREPARE_RELEASE_RELEASE_NOTES_FILE: docs/release-notes.md
20
20
  steps:
21
21
  - name: Dump GitHub context
22
22
  env:
@@ -31,7 +31,7 @@ jobs:
31
31
  with:
32
32
  python-version-file: ".python-version"
33
33
  - name: Install uv
34
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v9.0.0 #sync-pin:setup-uv
34
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv
35
35
  with:
36
36
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
37
37
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -0,0 +1,58 @@
1
+ name: Deploy Docs
2
+
3
+ on:
4
+ workflow_run: # zizmor: ignore[dangerous-triggers] -- only a successful push to main of this repository is deployed; pull request builds never are
5
+ workflows:
6
+ - Build Docs
7
+ types:
8
+ - completed
9
+
10
+ permissions: {}
11
+
12
+ jobs:
13
+ deploy-docs:
14
+ if: >-
15
+ github.event.workflow_run.conclusion == 'success'
16
+ && github.event.workflow_run.head_branch == 'main'
17
+ && github.event.workflow_run.head_repository.full_name == github.repository
18
+ && github.event.workflow_run.event == 'push'
19
+ runs-on: ubuntu-latest
20
+ permissions:
21
+ actions: read # download the artifact of the Build Docs run
22
+ pages: write
23
+ id-token: write
24
+ environment:
25
+ name: github-pages
26
+ url: ${{ steps.deployment.outputs.page_url }}
27
+ concurrency:
28
+ group: pages
29
+ cancel-in-progress: false
30
+ timeout-minutes: 10
31
+ steps:
32
+ - name: Dump GitHub context
33
+ env:
34
+ GITHUB_CONTEXT: ${{ toJson(github) }}
35
+ run: echo "$GITHUB_CONTEXT"
36
+ - name: Clean site
37
+ run: |
38
+ rm -rf ./site
39
+ mkdir ./site
40
+ # Build Docs is skipped when nothing that goes into the site changed: it uploads no artifact,
41
+ # the download below finds nothing, and the steps that need ./site/ are skipped
42
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 #sync-pin:download-artifact-2
43
+ with:
44
+ path: ./site/
45
+ pattern: docs-site
46
+ merge-multiple: true
47
+ github-token: ${{ secrets.GITHUB_TOKEN }}
48
+ run-id: ${{ github.event.workflow_run.id }}
49
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 #sync-pin:upload-pages-artifact
50
+ # hashFiles returns an empty string if there are no files
51
+ if: hashFiles('./site/*')
52
+ with:
53
+ path: site
54
+ - name: Deploy to GitHub Pages
55
+ # hashFiles returns an empty string if there are no files
56
+ if: hashFiles('./site/*')
57
+ id: deployment
58
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 #sync-pin:deploy-pages
@@ -30,7 +30,7 @@ jobs:
30
30
  python-version-file: ".python-version"
31
31
 
32
32
  - name: Setup uv
33
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v9.0.0 #sync-pin:setup-uv
33
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv
34
34
  with:
35
35
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
36
36
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -28,7 +28,7 @@ jobs:
28
28
  pull-requests: write
29
29
  env:
30
30
  PREPARE_RELEASE_VERSION_FILE: fastapi_router_versioning/__init__.py
31
- PREPARE_RELEASE_RELEASE_NOTES_FILE: release-notes.md
31
+ PREPARE_RELEASE_RELEASE_NOTES_FILE: docs/release-notes.md
32
32
  steps:
33
33
  - name: Dump GitHub context
34
34
  env:
@@ -42,7 +42,7 @@ jobs:
42
42
  with:
43
43
  python-version-file: ".python-version"
44
44
  - name: Install uv
45
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v9.0.0 #sync-pin:setup-uv
45
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv
46
46
  with:
47
47
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
48
48
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -28,7 +28,7 @@ jobs:
28
28
  with:
29
29
  python-version-file: ".python-version"
30
30
  - name: Install uv
31
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v9.0.0 #sync-pin:setup-uv
31
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv
32
32
  with:
33
33
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
34
34
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -35,6 +35,7 @@ jobs:
35
35
  filters: |
36
36
  src:
37
37
  - .github/workflows/test.yml
38
+ - docs_src/**
38
39
  - examples/**
39
40
  - fastapi_router_versioning/**
40
41
  - scripts/**
@@ -80,7 +81,7 @@ jobs:
80
81
  python-version: ${{ matrix.python-version }}
81
82
 
82
83
  - name: Setup uv
83
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v9.0.0 #sync-pin:setup-uv
84
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv
84
85
  with:
85
86
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
86
87
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -168,7 +169,7 @@ jobs:
168
169
 
169
170
  - name: Setup uv
170
171
  if: steps.changed-tests.outputs.found == 'true'
171
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v9.0.0 #sync-pin:setup-uv-2
172
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv-2
172
173
  with:
173
174
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
174
175
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -210,7 +211,7 @@ jobs:
210
211
  persist-credentials: false
211
212
 
212
213
  - name: Setup uv
213
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v9.0.0 #sync-pin:setup-uv-3
214
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v9.0.0 #sync-pin:setup-uv-3
214
215
  with:
215
216
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
216
217
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -243,7 +244,7 @@ jobs:
243
244
  include-hidden-files: true
244
245
 
245
246
  - name: Upload coverage to Codecov
246
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 #sync-pin:codecov-action
247
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.0.0 #sync-pin:codecov-action
247
248
  with:
248
249
  token: ${{ secrets.CODECOV_TOKEN }}
249
250
  files: coverage.xml
@@ -22,4 +22,4 @@ jobs:
22
22
  with:
23
23
  persist-credentials: false
24
24
  - name: Run zizmor
25
- uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 #sync-pin:zizmor-action
25
+ uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.2 #sync-pin:zizmor-action
@@ -10,6 +10,10 @@ __pycache__/
10
10
  build/
11
11
  dist/
12
12
 
13
+ # Docs
14
+ site/
15
+ .cache/
16
+
13
17
  # Coverage
14
18
  .coverage
15
19
  coverage/
@@ -15,7 +15,7 @@ repos:
15
15
  - id: trailing-whitespace
16
16
 
17
17
  - repo: https://github.com/crate-ci/typos
18
- rev: 8a48f81b6c64dcfea44b3633223084c4be58ac5f # frozen: v1.49.0
18
+ rev: 53821664afd79bb88fcdd22fdc2f49d8d3f6cac9 # frozen: v1
19
19
  hooks:
20
20
  - id: typos
21
21
  args: [ --force-exclude ]
@@ -50,6 +50,14 @@ repos:
50
50
  language: unsupported
51
51
  pass_filenames: false
52
52
 
53
+ - id: generate-readme
54
+ name: generate README.md from the docs
55
+ entry: uv run python scripts/generate_readme.py
56
+ language: unsupported
57
+ files: ^(docs/index\.md|docs/quickstart\.md|docs_src/quickstart/semver\.py|scripts/generate_readme\.py)$
58
+ require_serial: true
59
+ pass_filenames: false
60
+
53
61
  - id: zizmor
54
62
  name: zizmor
55
63
  language: python
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: fastapi-router-versioning
3
+ Version: 1.2.0
4
+ Summary: Router-based API versioning for FastAPI, with per-version docs and a declarative route lifecycle.
5
+ Project-URL: Homepage, https://mat81black.github.io/fastapi-router-versioning/
6
+ Project-URL: Repository, https://github.com/mat81black/fastapi-router-versioning
7
+ Project-URL: Issues, https://github.com/mat81black/fastapi-router-versioning/issues
8
+ Project-URL: Documentation, https://mat81black.github.io/fastapi-router-versioning/
9
+ Project-URL: Changelog, https://mat81black.github.io/fastapi-router-versioning/release-notes/
10
+ Author-email: Matteo Nieddu <mat81black@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: annotated-doc>=0.0.2
30
+ Requires-Dist: fastapi!=0.137.0,!=0.137.1,>=0.120.0
31
+ Description-Content-Type: text/markdown
32
+
33
+ <p align="center">
34
+ <img src="https://raw.githubusercontent.com/mat81black/fastapi-router-versioning/main/docs/assets/images/logo.svg" alt="FastAPI Router Versioning logo" width="128" height="128">
35
+ </p>
36
+
37
+ <h1 align="center">
38
+ <img src="https://raw.githubusercontent.com/mat81black/fastapi-router-versioning/main/docs/assets/images/wordmark.png" alt="FastAPI Router Versioning" width="480">
39
+ </h1>
40
+
41
+ <p align="center">
42
+ <a href="https://github.com/mat81black/fastapi-router-versioning/actions"><img src="https://github.com/mat81black/fastapi-router-versioning/workflows/Test/badge.svg" alt="Build Status"></a>
43
+ <a href="https://codecov.io/github/mat81black/fastapi-router-versioning"><img src="https://codecov.io/github/mat81black/fastapi-router-versioning/graph/badge.svg?token=4WQ63Q7ESY" alt="codecov"></a>
44
+ <a href="https://pypi.org/project/fastapi-router-versioning/"><img src="https://img.shields.io/pypi/v/fastapi-router-versioning?color=%2334D058&label=pypi%20package" alt="pypi package"></a>
45
+ <a href="https://pypi.org/project/fastapi-router-versioning/"><img src="https://img.shields.io/pypi/pyversions/fastapi-router-versioning.svg?color=%2334D058" alt="Supported Python versions"></a>
46
+ <a href="https://github.com/mat81black/fastapi-router-versioning/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT"></a>
47
+ </p>
48
+
49
+ Running multiple API versions side by side usually means duplicating routers, hand-rolling prefixes, or branching on request paths. `RouterVersioner` does it declaratively instead: annotate each route with the version it belongs to, and it generates the URL prefixes, the per-version OpenAPI schema, and the docs, without moving anything else in your app.
50
+
51
+ Documentation: https://mat81black.github.io/fastapi-router-versioning/
52
+
53
+ ---
54
+
55
+ ## Features
56
+
57
+ - **SemVer and CalVer**: version routes with `(major, minor)` tuples, or with arbitrary sortable strings
58
+ - **Per-version docs**: isolated Swagger UI, ReDoc, and `openapi.json` for every active version
59
+ - **Declarative lifecycle**: mark a route's introduction, deprecation, and removal with one decorator
60
+ - **Deprecation headers**: opt-in `Deprecation`, `Sunset` and `Link` response headers on routes in their deprecation window
61
+ - **Latest alias**: expose the newest version under a fixed `/latest` prefix clients can pin to
62
+ - **Self-hosted docs assets**: point Swagger UI and ReDoc at your own JS/CSS for air-gapped deployments
63
+ - **Reverse proxy and sub-app aware**: doc URLs pick up the ASGI `root_path` at request time
64
+ - **Route composition support**: works across nested routers, WebSockets, `Depends`, and OpenAPI Callbacks
65
+
66
+ ---
67
+
68
+ ## Requirements
69
+
70
+ - Python ≥ 3.10
71
+ - FastAPI ≥ 0.120.0, except `0.137.0` and `0.137.1`
72
+
73
+ > **Why two FastAPI versions are excluded**
74
+ >
75
+ > `0.137.0` and `0.137.1` shipped a routing internals rewrite before `iter_route_contexts()` landed in `0.137.2`, which this package relies on. Supporting that narrow gap would have meant a third compatibility code path instead of the two the package actually needs.
76
+
77
+ ---
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ pip install fastapi-router-versioning
83
+ # or
84
+ uv add fastapi-router-versioning
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Quick start
90
+
91
+ `RouterVersioner` is not a request-time dependency; there's no `Depends()` involved. It's a
92
+ one-time setup step: you build it, then call `.versionize()` once, and it reads the routers
93
+ you gave it and mounts one copy per version. Attach `@api_version` to every route **before**
94
+ calling `.versionize()`, since that call is what reads the router and wires everything up;
95
+ routes added to the router afterward are never picked up.
96
+
97
+ ```python
98
+ from fastapi import APIRouter, FastAPI
99
+ from fastapi_router_versioning import (
100
+ RouterVersioner, VersionFormat, api_version,
101
+ )
102
+
103
+ app = FastAPI()
104
+ router = APIRouter()
105
+
106
+
107
+ @router.get("/items")
108
+ @api_version((1, 0))
109
+ def get_items_v1():
110
+ return {"version": "1.0", "items": ["a", "b"]}
111
+
112
+
113
+ @router.get("/items")
114
+ @api_version((2, 0))
115
+ def get_items_v2():
116
+ return {"version": "2.0", "items": ["a", "b", "c"]}
117
+
118
+
119
+ RouterVersioner(
120
+ app=app, routers=router,
121
+ version_format=VersionFormat.SEMVER
122
+ ).versionize()
123
+ ```
124
+
125
+ CalVer, the route lifecycle, the parameter reference and the advanced options are in the [documentation](https://mat81black.github.io/fastapi-router-versioning/).
126
+
127
+ ---
128
+
129
+ ## Release Notes
130
+
131
+ [RELEASE_NOTES](https://github.com/mat81black/fastapi-router-versioning/blob/main/docs/release-notes.md)
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ [MIT](https://github.com/mat81black/fastapi-router-versioning/blob/main/LICENSE)
@@ -0,0 +1,105 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mat81black/fastapi-router-versioning/main/docs/assets/images/logo.svg" alt="FastAPI Router Versioning logo" width="128" height="128">
3
+ </p>
4
+
5
+ <h1 align="center">
6
+ <img src="https://raw.githubusercontent.com/mat81black/fastapi-router-versioning/main/docs/assets/images/wordmark.png" alt="FastAPI Router Versioning" width="480">
7
+ </h1>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/mat81black/fastapi-router-versioning/actions"><img src="https://github.com/mat81black/fastapi-router-versioning/workflows/Test/badge.svg" alt="Build Status"></a>
11
+ <a href="https://codecov.io/github/mat81black/fastapi-router-versioning"><img src="https://codecov.io/github/mat81black/fastapi-router-versioning/graph/badge.svg?token=4WQ63Q7ESY" alt="codecov"></a>
12
+ <a href="https://pypi.org/project/fastapi-router-versioning/"><img src="https://img.shields.io/pypi/v/fastapi-router-versioning?color=%2334D058&label=pypi%20package" alt="pypi package"></a>
13
+ <a href="https://pypi.org/project/fastapi-router-versioning/"><img src="https://img.shields.io/pypi/pyversions/fastapi-router-versioning.svg?color=%2334D058" alt="Supported Python versions"></a>
14
+ <a href="https://github.com/mat81black/fastapi-router-versioning/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT"></a>
15
+ </p>
16
+
17
+ Running multiple API versions side by side usually means duplicating routers, hand-rolling prefixes, or branching on request paths. `RouterVersioner` does it declaratively instead: annotate each route with the version it belongs to, and it generates the URL prefixes, the per-version OpenAPI schema, and the docs, without moving anything else in your app.
18
+
19
+ Documentation: https://mat81black.github.io/fastapi-router-versioning/
20
+
21
+ ---
22
+
23
+ ## Features
24
+
25
+ - **SemVer and CalVer**: version routes with `(major, minor)` tuples, or with arbitrary sortable strings
26
+ - **Per-version docs**: isolated Swagger UI, ReDoc, and `openapi.json` for every active version
27
+ - **Declarative lifecycle**: mark a route's introduction, deprecation, and removal with one decorator
28
+ - **Deprecation headers**: opt-in `Deprecation`, `Sunset` and `Link` response headers on routes in their deprecation window
29
+ - **Latest alias**: expose the newest version under a fixed `/latest` prefix clients can pin to
30
+ - **Self-hosted docs assets**: point Swagger UI and ReDoc at your own JS/CSS for air-gapped deployments
31
+ - **Reverse proxy and sub-app aware**: doc URLs pick up the ASGI `root_path` at request time
32
+ - **Route composition support**: works across nested routers, WebSockets, `Depends`, and OpenAPI Callbacks
33
+
34
+ ---
35
+
36
+ ## Requirements
37
+
38
+ - Python ≥ 3.10
39
+ - FastAPI ≥ 0.120.0, except `0.137.0` and `0.137.1`
40
+
41
+ > **Why two FastAPI versions are excluded**
42
+ >
43
+ > `0.137.0` and `0.137.1` shipped a routing internals rewrite before `iter_route_contexts()` landed in `0.137.2`, which this package relies on. Supporting that narrow gap would have meant a third compatibility code path instead of the two the package actually needs.
44
+
45
+ ---
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install fastapi-router-versioning
51
+ # or
52
+ uv add fastapi-router-versioning
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Quick start
58
+
59
+ `RouterVersioner` is not a request-time dependency; there's no `Depends()` involved. It's a
60
+ one-time setup step: you build it, then call `.versionize()` once, and it reads the routers
61
+ you gave it and mounts one copy per version. Attach `@api_version` to every route **before**
62
+ calling `.versionize()`, since that call is what reads the router and wires everything up;
63
+ routes added to the router afterward are never picked up.
64
+
65
+ ```python
66
+ from fastapi import APIRouter, FastAPI
67
+ from fastapi_router_versioning import (
68
+ RouterVersioner, VersionFormat, api_version,
69
+ )
70
+
71
+ app = FastAPI()
72
+ router = APIRouter()
73
+
74
+
75
+ @router.get("/items")
76
+ @api_version((1, 0))
77
+ def get_items_v1():
78
+ return {"version": "1.0", "items": ["a", "b"]}
79
+
80
+
81
+ @router.get("/items")
82
+ @api_version((2, 0))
83
+ def get_items_v2():
84
+ return {"version": "2.0", "items": ["a", "b", "c"]}
85
+
86
+
87
+ RouterVersioner(
88
+ app=app, routers=router,
89
+ version_format=VersionFormat.SEMVER
90
+ ).versionize()
91
+ ```
92
+
93
+ CalVer, the route lifecycle, the parameter reference and the advanced options are in the [documentation](https://mat81black.github.io/fastapi-router-versioning/).
94
+
95
+ ---
96
+
97
+ ## Release Notes
98
+
99
+ [RELEASE_NOTES](https://github.com/mat81black/fastapi-router-versioning/blob/main/docs/release-notes.md)
100
+
101
+ ---
102
+
103
+ ## License
104
+
105
+ [MIT](https://github.com/mat81black/fastapi-router-versioning/blob/main/LICENSE)
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Run a function once for each versioned router before it is mounted.
3
+ ---
4
+
5
+ # Callback hook
6
+
7
+ `callback` runs for every versioned router, the `latest_prefix` alias included, right before
8
+ `RouterVersioner` includes it in the app, handy for logging every mount point or wiring metrics:
9
+
10
+ ```python
11
+ --8<-- "docs_src/advanced/callback_hook.py:snippet"
12
+ ```
13
+
14
+ If `versionize()` raises, don't catch the exception and retry. Errors found while the versions
15
+ are built, such as a callback that throws or two versions that resolve to the same prefix, are
16
+ raised before anything is added to the app, but the routers are then included one after another,
17
+ and this package makes no attempt to undo a partial inclusion. `versionize()` normally runs at
18
+ startup, so the app fails to start; fix the underlying issue and start it again.
@@ -0,0 +1,33 @@
1
+ ---
2
+ description: Version OpenAPI webhooks and carry route callbacks into every version.
3
+ ---
4
+
5
+ # OpenAPI callbacks and webhooks
6
+
7
+ Route-level **Callbacks** need no special handling: a `callbacks=[...]` argument on a route
8
+ is carried over to every versioned copy of it automatically:
9
+
10
+ ```python
11
+ --8<-- "docs_src/advanced/callbacks.py:snippet"
12
+ ```
13
+
14
+ **Webhooks** (`app.webhooks`) are visible in every version's schema by default. Pass
15
+ `webhook_routers` to version them the same way as regular routes, with `@api_version` on
16
+ each definition:
17
+
18
+ ```python
19
+ --8<-- "docs_src/advanced/webhooks.py:snippet"
20
+ ```
21
+
22
+ In [`webhook_versioning_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/webhook_versioning_app.py), which follows this pattern, v1.0 lists two webhooks and v2.0 only `order-created`, with its v2 definition (the example's second webhook is `payment-received` rather than `payment-failed`):
23
+
24
+ === "v1.0"
25
+
26
+ ![Swagger UI of version 1.0, with a Webhooks section listing order-created and payment-received](../assets/screenshots/webhooks-v1.jpg){ loading=lazy }
27
+
28
+ === "v2.0"
29
+
30
+ ![Swagger UI of version 2.0, with a Webhooks section listing only order-created](../assets/screenshots/webhooks-v2.jpg){ loading=lazy }
31
+
32
+ A webhook version only becomes visible once a route version reaches that same prefix, since
33
+ both follow the same `remove_in` lifecycle.
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: Change how a version appears in URL prefixes and in the Swagger and ReDoc titles.
3
+ ---
4
+
5
+ # Custom URL format
6
+
7
+ `prefix_format` and `semantic_version_format` control how a version renders in URLs and in
8
+ doc titles, independently of how it's expressed in `@api_version`. A common use is dropping
9
+ the minor number from the URL while still tracking it internally:
10
+
11
+ ```python
12
+ --8<-- "docs_src/advanced/custom_url_format.py:snippet"
13
+ ```
14
+
15
+ The routes are mounted under `/v1`, `/v2` and `/latest`, and the Swagger titles read `v1` and `v2` instead of `v1.0` and `v2.0`. Here are the docs at `/v1/docs` and at `/latest/docs`, which serves the newest version:
16
+
17
+ === "/v1/docs"
18
+
19
+ ![Swagger UI at /v1/docs, titled v1 and listing GET /v1/items](../assets/screenshots/url-format-v1.jpg){ loading=lazy }
20
+
21
+ === "/latest/docs"
22
+
23
+ ![Swagger UI at /latest/docs, titled v2 and listing GET /latest/items](../assets/screenshots/url-format-latest.jpg){ loading=lazy }
24
+
25
+ Routes are still decorated with `(major, minor)` tuples; only their URL and label change.