From 5a2b145fdee9f4028e28c55aafb7885c7fbd23d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=B0=D1=87=D0=B5=D0=B2=20=D0=A4=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=80?= Date: Wed, 29 Jul 2026 21:33:53 +0500 Subject: [PATCH 1/5] chore: add the context7 manifest --- context7.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 context7.json diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..0d1392c --- /dev/null +++ b/context7.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "projectTitle": "fastapi-easy-versioning", + "description": "Versioned APIs for FastAPI: one FastAPI sub-application per version, automatic inheritance of marked endpoints from older versions into newer ones, and a regenerated OpenAPI schema per version.", + "folders": ["docs/en"], + "excludeFolders": ["docs/en/reference"], + "rules": [ + "Requires Python >= 3.10 and fastapi >=0.95.0,!=0.137.0,!=0.137.1; fastapi is the only runtime dependency.", + "A version is a FastAPI sub-application created with an api_version extra and mounted under an aggregating app: `app.mount('/v1', FastAPI(api_version=1))`. The value must be an int (0 is valid, bool is not); a sub-app without it takes no part in versioning.", + "Add VersioningMiddleware to the application that directly mounts the version sub-apps, never to the sub-apps themselves: `app.add_middleware(VersioningMiddleware)`. Several instances can coexist, one per aggregating app, so independent APIs version separately.", + "Versioning is opt-in: only endpoints carrying `Depends(versioning())` are inherited. Attach it per endpoint, via add_api_route, or on a whole APIRouter with `APIRouter(dependencies=[Depends(versioning())])`.", + "`versioning()` with no `until` means 'available through the latest version', not 'this version only'; `versioning(until=N)` caps availability at version N inclusive, and the smallest declared `until` wins when a route carries several markers.", + "Inject `Annotated[VersionInfo, Depends(versioning())]` into an endpoint to read the resolved `origin` and `until`; it works for WebSocket endpoints too.", + "Inheritance is built once, on the middleware's first ASGI event. Routes or versions added after that need an explicit `rebuild_versioning(app)`, which is idempotent.", + "Redefining an endpoint on the same path in a newer version shadows the inherited one, at runtime and in the OpenAPI schema; pass `rebuild_openapi=False` to keep inherited endpoints out of each version's /docs." + ] +} From c42cc1217402a3b84652622c1370db50e57c10a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=B0=D1=87=D0=B5=D0=B2=20=D0=A4=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=80?= Date: Wed, 29 Jul 2026 21:34:10 +0500 Subject: [PATCH 2/5] chore: fill package keywords and project urls --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 97f96e1..aac2a2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "fastapi>=0.95.0,!=0.137.0,!=0.137.1", ] license = { file = "LICENSE" } +keywords = ["fastapi", "versioning", "api-versioning", "openapi", "asgi"] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: FastAPI", @@ -28,8 +29,10 @@ classifiers = [ [project.urls] Repository = "https://github.com/feodor-ra/fastapi-easy-versioning" -Homepage = "https://github.com/feodor-ra/fastapi-easy-versioning" +Homepage = "https://feodor-ra.github.io/fastapi-easy-versioning/" Documentation = "https://feodor-ra.github.io/fastapi-easy-versioning/" +Issues = "https://github.com/feodor-ra/fastapi-easy-versioning/issues" +Changelog = "https://feodor-ra.github.io/fastapi-easy-versioning/changelog/" [build-system] requires = ["uv_build>=0.8.3,<0.9.0"] From c9b705a5bad81064bf11e651463d8a9ae14518d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=B0=D1=87=D0=B5=D0=B2=20=D0=A4=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=80?= Date: Wed, 29 Jul 2026 21:38:43 +0500 Subject: [PATCH 3/5] ci: deploy docs via GitHub Pages Actions --- .github/workflows/docs.yml | 56 ++++++++++++++++++++++++++++ .github/workflows/mkdocs-publish.yml | 34 ----------------- 2 files changed, 56 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/mkdocs-publish.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..7b71cf1 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,56 @@ +name: docs + +on: + push: + branches: ["master"] + paths: + - docs/** + - overrides/** + - mkdocs.yml + - pyproject.toml + - uv.lock + - src/** + - .github/workflows/docs.yml + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code Repository + uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: uv.lock + - name: Set up Python + run: uv python install + - name: Create venv, install dependencies + run: uv sync --group=docs --frozen + - name: Build documentation + run: uv run mkdocs build --strict + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/mkdocs-publish.yml b/.github/workflows/mkdocs-publish.yml deleted file mode 100644 index 62b8f15..0000000 --- a/.github/workflows/mkdocs-publish.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: mkdocs publish -on: - push: - branches: - - master -permissions: - contents: write -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout Code Repository - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true - cache-dependency-glob: uv.lock - - name: Set up Python - run: uv python install - - name: Create venv, install dependencies - run: | - uv sync --group=docs --frozen - source .venv/bin/activate - echo "$VIRTUAL_ENV/bin" >> $GITHUB_PATH - echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV - - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - - uses: actions/cache@v4 - with: - key: mkdocs-material-${{ env.cache_id }} - path: .cache - restore-keys: | - mkdocs-material- - - run: mkdocs gh-deploy --force From 538772e7f74df5e2fb9e923b926a120fe34d7121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=B0=D1=87=D0=B5=D0=B2=20=D0=A4=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=80?= Date: Wed, 29 Jul 2026 21:38:53 +0500 Subject: [PATCH 4/5] ci: skip lint and tests for the Russian README --- .github/workflows/lint.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b46f7ef..f04f56b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,10 +2,10 @@ name: Lints on: pull_request: branches: ["master"] - paths-ignore: ["docs/**", "README.md", "LICENSE"] + paths-ignore: ["docs/**", "README.md", "README.ru.md", "LICENSE"] push: branches: ["master"] - paths-ignore: ["docs/**", "README.md", "LICENSE"] + paths-ignore: ["docs/**", "README.md", "README.ru.md", "LICENSE"] jobs: lint: runs-on: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 51cc014..a5aebf2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,10 +5,10 @@ concurrency: on: pull_request: branches: ["master"] - paths-ignore: ["docs/**", "README.md", "LICENSE"] + paths-ignore: ["docs/**", "README.md", "README.ru.md", "LICENSE"] push: branches: ["master"] - paths-ignore: ["docs/**", "README.md", "LICENSE"] + paths-ignore: ["docs/**", "README.md", "README.ru.md", "LICENSE"] jobs: run: name: Python ${{ matrix.python-version }} From 3153021a9b1e0f59209d2c6fefe6136dc51f2693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=B0=D1=87=D0=B5=D0=B2=20=D0=A4=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=80?= Date: Wed, 29 Jul 2026 21:40:00 +0500 Subject: [PATCH 5/5] docs: update CLAUDE.md for the new docs layout --- CLAUDE.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index efb55c8..e2ff012 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ uvx --python=3.14 --from="fastapi[standard]" --with="fastapi-easy-versioning" \ - **Commits**: Conventional Commits. Do NOT add a `Co-Authored-By: Claude` trailer. - **Release flow**: bumping the version in `pyproject.toml` (static `version = "..."`) + committing does NOT publish. PyPI publish (`release.yml`) fires ONLY when a GitHub Release is `published`, via `uv build && uv publish` over OIDC trusted publishing (no token secret). Version bumps are dedicated `chore: bump to X` commits. -- **CI**: lint (`ruff check` + `ty check src`) and tests (matrix 3.10–3.14) run on PR/push to master, ignoring `docs/**`, `README.md`, `LICENSE`. A `fastapi-compat` job additionally runs the suite against fastapi `0.95.*` (minimum), `0.136.*` (last flat-router release) and latest via `uv run --frozen --with … -m pytest --no-cov`. Docs auto-deploy to GitHub Pages on EVERY push to master. +- **CI**: lint (`ruff check` + `ty check src`) and tests (matrix 3.10–3.14) run on PR/push to master, ignoring `docs/**`, `README.md`, `README.ru.md`, `LICENSE`. A `fastapi-compat` job additionally runs the suite against fastapi `0.95.*` (minimum), `0.136.*` (last flat-router release) and latest via `uv run --frozen --with … -m pytest --no-cov`. Docs (`docs.yml`) build with `mkdocs build --strict` and deploy to GitHub Pages through the Pages Actions artifact (NOT `mkdocs gh-deploy`/the `gh-pages` branch — the Pages source is set to "GitHub Actions"), triggered by pushes to master touching `docs/**`, `overrides/**`, `mkdocs.yml`, `pyproject.toml`, `uv.lock`, `src/**` (docstrings feed the reference) or the workflow itself. - **Dependabot** (`.github/dependabot.yml`): one grouped weekly PR bumping ONLY dev dependencies. `dependency-type: development` scopes updates to the PEP 735 `[dependency-groups]`; `[project.dependencies]` (fastapi) are classified production and deliberately excluded so their intentional lower bounds stay put. It won't touch pre-commit hook revs — bump those with `pre-commit autoupdate`. - **pre-commit**: hooks include ruff-check, ruff-format, `uv-lock`, `ty`. Hook revs are kept in sync with the `pyproject` dev pins via `pre-commit autoupdate` (e.g. ruff `v0.15.20` ↔ `ruff~=0.15.20`). `ruff format --check` is enforced ONLY via pre-commit, not CI. - **Type checker is `ty`** (Astral, preview — pinned `ty~=0.0.56`), NOT mypy. There is no `strict = true` analog; ty uses its default rule set and `[tool.ty.terminal] error-on-warning` defaults to `true` (warnings fail too). `[tool.ty.environment] python-version = "3.10"`. `tests` are excluded via `[tool.ty.src] exclude`. ty resolves third-party imports (fastapi) straight from `pyproject.toml` — the pre-commit hook takes NO `additional_dependencies` (unlike the old mypy hook), and checks the whole project (minus `tests`), while CI narrows to `ty check src`. @@ -73,5 +73,11 @@ uvx --python=3.14 --from="fastapi[standard]" --with="fastapi-easy-versioning" \ - **Ruff is maximal**: `lint.select = ['ALL']` with `preview = true` (docstring `D*` rules and a few others are the main opt-outs; per-file relaxations for `examples/*` and `tests/*`). Expect new code to trip many rules, and note `fix = true` auto-applies safe fixes on `ruff check`. - **Tests** import via the src-layout path `src.fastapi_easy_versioning`, so run from repo root. Async via `anyio` — each test file needs module-level `pytestmark = [pytest.mark.anyio]` plus the session-scoped `anyio_backend` fixture (`"asyncio"`). Shared fixtures (`app`, `client`, `middleware_setup`, `v1`, `v2`, `websocket_request`) are in `tests/conftest.py`. WebSocket tests use the `websocket_request` raw-ASGI fixture — neither httpx2 nor httpx has a native WebSocket client, and starlette's `TestClient` would require the `httpx` package (absent from the `test` group). - **WebSocket opt-in on fastapi 0.95**: WS routes there have no route-level `dependencies=` kwarg — mark them via `Depends(versioning())` in the endpoint signature. -- **Docs are bilingual (RU + EN)** built with mkdocs-material + `mkdocs-static-i18n`. `docs/ru/*` is the source of truth; `docs/en/*` is a maintained first-class translation (no auto-translated banner). Mirror the same 5 pages (index, dependency, middleware, examples/simple, examples/multiple) in both, and update BOTH `nav:` lists in `mkdocs.yml` when adding a page. -- **Drift risk**: the quick-start example is copied near-verbatim across `README.md`, `docs/en/index.md`, `docs/ru/index.md`; the `docs/*/examples/*.md` code blocks are near-verbatim copies of `examples/*.py` (docs omit the in-file `# To run…` comment and use a clone-prefixed run command). Any public-API change must be propagated to all copies. +- **Docs are bilingual (EN + RU)** built with mkdocs-material + `mkdocs-static-i18n` + `mkdocstrings`. `docs/ru/*` is the source of truth; `docs/en/*` is a maintained first-class translation (no auto-translated banner) and is the **default** locale (English at `/`, Russian at `/ru/`). Mirror the same 10 pages in both trees: `index`, `quickstart`, `guide/{dependency,middleware,recipes,limitations}`, `examples/{simple,multiple}`, `reference/{dependency,middleware}`, `changelog`. There is now ONE `nav:` in `mkdocs.yml` plus `nav_translations` for ru — add a page to the single nav and translate its section title there. +- **Reference pages are generated** by mkdocstrings from the docstrings of the 6 public names (`::: fastapi_easy_versioning.X`), so a docstring is user-facing documentation: keep it Google-style and cross-link with `[`name`][fastapi_easy_versioning.name]`. Because the same identifiers are rendered in both locales, `autorefs.resolve_closest: true` is required — without it `--strict` aborts on "Multiple primary URLs". +- **`mkdocs build --strict` must stay clean** — it is what CI runs, and it catches broken internal links and unresolved mkdocstrings identifiers. Run it after moving or renaming any page. +- **Mermaid diagrams** use `classDef available/missing` + the `:::` operator (not per-node `style` lines). The classes carry an explicit `color:` — mermaid routes text properties of a class into the label's inline style, and without it Material paints the labels in the theme foreground, which is unreadable on the pastel fills in the slate scheme. The `pymdownx.superfences` custom fence for mermaid must stay in `mkdocs.yml`. +- **`check-yaml` runs with `--unsafe`** — mkdocs.yml carries `!!python/name:` tags the hook cannot construct. +- **`overrides/main.html`** exists solely to inject the Context7 chat widget into Material's `scripts` block (`extra_javascript` cannot carry the required `data-library` attribute). `context7.json` at the repo root drives Context7's indexing of `docs/en` — keep its `rules` in sync with the public API. +- **Drift risk**: the quick-start example is copied near-verbatim across `README.md`, `README.ru.md`, `docs/en/index.md`, `docs/ru/index.md`, `docs/*/quickstart.md`; the `docs/*/examples/*.md` code blocks are near-verbatim copies of `examples/*.py` (docs omit the in-file `# To run…` comment and use a clone-prefixed run command). Any public-API change must be propagated to all copies. Both READMEs use ABSOLUTE links — `README.md` is the PyPI long description, where relative links 404. +- **Release notes**: each release gets an entry in `docs/{en,ru}/changelog.md` (Keep a Changelog format) alongside the `chore: bump to X` commit.