diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4b1ba53..e465756 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,10 +1,26 @@ name: CI +# Changes that touch only documentation do not start this pipeline; docs.yaml checks those with +# one job (it builds the site and runs the examples the pages contain). on: # Branches are tested through their pull request; a push trigger for them too ran every job twice. push: branches: [main] + paths-ignore: + - "**.md" + - "docs/**" + - "claudedocs/**" + - "mkdocs.yaml" + - "LICENSE" + - ".github/workflows/docs.yaml" pull_request: + paths-ignore: + - "**.md" + - "docs/**" + - "claudedocs/**" + - "mkdocs.yaml" + - "LICENSE" + - ".github/workflows/docs.yaml" permissions: contents: read diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 7f96787..64b0631 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,36 +1,52 @@ -# Publishes the documentation site to GitHub Pages from main. -# Requires Settings > Pages > Source: "GitHub Actions". +# Documentation: checked on pull requests that touch it, published to GitHub Pages from main. +# This is the only pipeline a docs-only change starts (see paths-ignore in ci.yaml). name: Docs on: + pull_request: + paths: + - "**.md" + - "docs/**" + - "examples/**" + - "mkdocs.yaml" + - ".github/workflows/docs.yaml" push: branches: [main] - paths: ["docs/**", "examples/**", "zensical.toml", ".github/workflows/docs.yaml"] + paths: + - "**.md" + - "docs/**" + - "examples/**" + - "mkdocs.yaml" + - ".github/workflows/docs.yaml" workflow_dispatch: permissions: contents: read - pages: write - id-token: write concurrency: - group: pages + group: docs-${{ github.ref }} cancel-in-progress: true jobs: + check: + # The pages are executable: build the site strictly and run the examples in it. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - run: docker compose -f docker/compose.rs.yaml up -d --wait + - run: uv sync --all-packages --group docs --python 3.13 + - run: make docs + - run: uv run pytest packages/mongospecs-web/tests/test_docs_examples.py examples + deploy: + if: github.event_name != 'pull_request' + needs: check runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + permissions: + contents: write steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - run: uv sync --group docs --python 3.13 - - run: uv run --group docs zensical build --clean - - uses: actions/configure-pages@v5 - - uses: actions/upload-pages-artifact@v3 - with: - path: site - - id: deployment - uses: actions/deploy-pages@v4 + - run: uv run --group docs mkdocs gh-deploy --strict --force diff --git a/.github/workflows/python-publish.yaml b/.github/workflows/python-publish.yaml index 63221b0..2f7ce6c 100644 --- a/.github/workflows/python-publish.yaml +++ b/.github/workflows/python-publish.yaml @@ -1,6 +1,7 @@ # Publishes whichever distribution the release tag names: # mongospecs-v0.2.0 -> packages/mongospecs -# mongospecs-web-v0.1.0 -> packages/mongospecs-web +# mongospecs-web-v0.2.0 -> packages/mongospecs-web +# The two packages carry the same version and are released together. name: Publish on: diff --git a/CHANGELOG.md b/CHANGELOG.md index f41585d..2135629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog -Two packages are released from this repository; each entry says which one it is about. Versions +Two packages are released from this repository, `mongospecs` and `mongospecs-web`. They always +carry the same version and are released together; each entry says which one it is about. Versions follow semantic versioning, and while a package is below 1.0 a minor release may break compatibility. Breaking changes are marked **Breaking**. @@ -29,9 +30,9 @@ A rewrite. See [Migrating from 0.1.x](docs/migration.md) for the API map. - A single-field update on a pydantic model runs the field's validators and not the model-level ones, which could only see the other fields' defaults. -## mongospecs-web 0.1.0 (2026-10-13) +## mongospecs-web 0.2.0 (2026-10-13) -First release. +First release. It starts at 0.2.0 to match the `mongospecs` release it goes with. - Resources described as data and mounted on plain ASGI, Starlette, FastAPI, Litestar or Quart. - Field `Policy` drives the create, patch, put, read, filter and sort surface; guards and row diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ea1ddc..fb93ee4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,10 @@ A plug only translates requests and responses. Prove it with the contract suite: ## Releasing -Each package is released by publishing a GitHub release whose tag names it: -`mongospecs-v0.2.0` or `mongospecs-web-v0.1.0`. The tag must match the version in that package's -`pyproject.toml`; the publish workflow checks this and runs the full suite before uploading. +The two packages always carry the same version and are released together: bump both +`pyproject.toml` files (and `mongospecs_web.__version__`), then publish `mongospecs` first, +because `mongospecs-web` depends on it. + +Each package is released by publishing a GitHub release whose tag names it: `mongospecs-v0.2.0`, +then `mongospecs-web-v0.2.0`. The tag must match the version in that package's `pyproject.toml`; +the publish workflow checks this and runs the full suite before uploading. diff --git a/claudedocs/v2-plan.md b/claudedocs/v2-plan.md index 14ec78e..370c3e3 100644 --- a/claudedocs/v2-plan.md +++ b/claudedocs/v2-plan.md @@ -893,6 +893,7 @@ Recorded per §7.1 rule 4. Each one names what changed and why; none invalidates | A39 | §4.8 | `Put` operation and `Resource(merge_patch=False)`. `PUT /{id}` replaces every field the client may update (sent value, or the default when left out; a required field must be sent) and leaves the rest as stored, in one atomic update; an embedded object holding protected values is replaced key by key the same way, one without them is replaced whole. It never creates. `Put` is not in the default `ops`. `merge_patch=False` makes `PATCH` replace embedded objects as before A37; nested keys the policy closes are still rejected | Requested by Jay as the opt-out from A37 and its limits (validators of the embedded model see one key at a time on a merge, a stored null needs the whole object, a backend without `supports_merge` gets a 422). The flag is on the resource, not the model: it describes an HTTP verb, model code is unaffected, and embedded models do not inherit settings (A35). Cost of opting out, accepted by whoever sets it: values the client cannot send are reset to defaults on replacement. `Put` is opt-in because guards are keyed by operation, so adding it to the defaults would expose an unguarded write on resources that guard `Patch` | | A40 | §4.4, §4.8 | Third review. Core: a single-field update on a pydantic model no longer fails a model-level validator that saw the other fields' defaults (the field's own validators still run); `insert_many` and `bulk_save` run document hooks; `page()` starts its count only after its arguments are validated. Web: request bodies are strict JSON (`NaN`, `Infinity` and over-deep nesting are a 400); `If-Match` on a model without a `Revision` is a 412; `If-None-Match` accepts lists, weak tags and `*`; the ASGI plug drops a request whose client disconnected mid-body; `Api(openapi=False)` is honoured by the FastAPI plug; guard and hook keys must name an operation; the concurrency limit is applied before authentication; a filter or sort key given twice is a 400 | Each was reproduced against the replica set first (`test_review_round3.py` in both packages). `insert_many` hooks deviate from Mongoose, where `insertMany` does not run `save` middleware: here the documents are in the caller's hands and a `before("insert")` hook that silently does not run loses data (a slug, a derived field). The hook calls are skipped when no hook is registered, so the bulk paths cost the same as before for models without hooks. Left as documented differences: trailing slash, `HEAD` and `OPTIONS` on framework plugs | | A41 | §4.2, §4.4 | Document writes are validated by default. `insert`, `save`, `insert_many` and `bulk_save` decode the document's own encoding with the backend's strict rules before sending it and raise `ValidationError` if it does not fit. `save` checks only when there is a change, and only the changed fields of a document loaded with a projection. Escape hatches: `validate=False` per call, `SpecConfig(validate_writes=False)` per model | Requested by Jay after the third review found that msgspec and attrs (no constructor or assignment checks) and pydantic (no assignment checks) let a wrongly typed value be stored, after which the document could not be loaded. Checking the encoded form means the test is exactly "can this be read back", is one code path for all three backends, and reuses the encoding the write needs anyway. Cost measured ad hoc at about 3 µs (msgspec) and 5 µs (pydantic) per seven-field document; the recorded B4 run predates it. The full suite (887 tests) passed with the check on, so no existing write was falsely refused | +| A42 | §5 | `mongospecs-web` is versioned 1-to-1 with `mongospecs`: its first release is 0.2.0, not 0.1.0, and the two are always released together. Supersedes A12 and the version rows in §5 and §7 | Decided by Jay on 2026-10-13, at release time | New node: diff --git a/packages/mongospecs-web/README.md b/packages/mongospecs-web/README.md index f3b80e9..8fa6fd1 100644 --- a/packages/mongospecs-web/README.md +++ b/packages/mongospecs-web/README.md @@ -3,7 +3,7 @@ REST resources for [mongospecs](https://github.com/jaykv/mongospecs) models, described once and mounted on the framework you use: plain ASGI, Starlette, FastAPI, Litestar or Quart. -> **Status: 0.1.0, alpha.** +> **Status: 0.2.0, alpha.** The version is always the same as the `mongospecs` release it goes with. ``` pip install "mongospecs-web[fastapi]" "mongospecs[msgspec]" diff --git a/packages/mongospecs-web/pyproject.toml b/packages/mongospecs-web/pyproject.toml index edd4f2a..886e9c9 100644 --- a/packages/mongospecs-web/pyproject.toml +++ b/packages/mongospecs-web/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mongospecs-web" -version = "0.1.0" +version = "0.2.0" description = "Framework-neutral REST resources for mongospecs models" readme = "README.md" license = { text = "MIT" } diff --git a/packages/mongospecs-web/src/mongospecs_web/__init__.py b/packages/mongospecs-web/src/mongospecs_web/__init__.py index d318f92..1eaba47 100644 --- a/packages/mongospecs-web/src/mongospecs_web/__init__.py +++ b/packages/mongospecs-web/src/mongospecs_web/__init__.py @@ -41,7 +41,7 @@ Response, ) -__version__ = "0.1.0" +__version__ = "0.2.0" __all__ = [ "Api", diff --git a/uv.lock b/uv.lock index 4c1ae74..f87fe21 100644 --- a/uv.lock +++ b/uv.lock @@ -809,7 +809,7 @@ requires-dist = [ [[package]] name = "mongospecs-web" -version = "0.1.0" +version = "0.2.0" source = { editable = "packages/mongospecs-web" } dependencies = [ { name = "mongospecs" },