diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e465756..16d8859 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,26 +1,33 @@ 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). +# This pipeline runs only when something it tests changes: code, tests, dependencies, or the +# pipeline itself. Documentation is checked by docs.yaml with one job; changes to anything else +# (other workflows, repository files) start nothing here. 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" + paths: + - "packages/**" + - "bench/**" + - "examples/**" + - "docker/**" + - "pyproject.toml" + - "uv.lock" + - "Makefile" + - ".github/workflows/ci.yaml" + - "!**.md" # a README inside a package is documentation pull_request: - paths-ignore: - - "**.md" - - "docs/**" - - "claudedocs/**" - - "mkdocs.yaml" - - "LICENSE" - - ".github/workflows/docs.yaml" + paths: + - "packages/**" + - "bench/**" + - "examples/**" + - "docker/**" + - "pyproject.toml" + - "uv.lock" + - "Makefile" + - ".github/workflows/ci.yaml" + - "!**.md" # a README inside a package is documentation permissions: contents: read diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 64b0631..d0c630c 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,5 +1,5 @@ # 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). +# This is the only pipeline a docs-only change starts (ci.yaml runs for code and dependencies). name: Docs on: diff --git a/.github/workflows/python-publish.yaml b/.github/workflows/python-publish.yaml index 2f7ce6c..a527252 100644 --- a/.github/workflows/python-publish.yaml +++ b/.github/workflows/python-publish.yaml @@ -1,7 +1,7 @@ # Publishes whichever distribution the release tag names: # mongospecs-v0.2.0 -> packages/mongospecs # mongospecs-web-v0.2.0 -> packages/mongospecs-web -# The two packages carry the same version and are released together. +# The two packages are versioned and released independently. name: Publish on: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2135629..314834a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,7 @@ # Changelog -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 +Two packages are released from this repository, `mongospecs` and `mongospecs-web`, each with its +own version; 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**. ## mongospecs 0.2.0 (2026-10-13) @@ -32,7 +31,7 @@ A rewrite. See [Migrating from 0.1.x](docs/migration.md) for the API map. ## mongospecs-web 0.2.0 (2026-10-13) -First release. It starts at 0.2.0 to match the `mongospecs` release it goes with. +First release. It starts at 0.2.0 alongside `mongospecs` 0.2.0; later versions are independent. - 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 fb93ee4..3a80847 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,10 +46,11 @@ A plug only translates requests and responses. Prove it with the contract suite: ## Releasing -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. +The two packages are versioned and released independently (both happened to start v2 at 0.2.0). +Bump the version in the package's `pyproject.toml` (and `mongospecs_web.__version__` for the web +package). When a web release needs a newer `mongospecs`, raise its requirement and publish +`mongospecs` first. -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`; +Each package is released by publishing a GitHub release whose tag names it: `mongospecs-v0.2.0` +or `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 370c3e3..1c3541b 100644 --- a/claudedocs/v2-plan.md +++ b/claudedocs/v2-plan.md @@ -893,7 +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 | +| A42 | §5 | The first release of `mongospecs-web` is 0.2.0, not 0.1.0, to go out alongside `mongospecs` 0.2.0. After that the two packages are versioned and released independently. Supersedes A12 and the version rows in §5 and §7 | Decided by Jay on 2026-10-13, at release time. Jay first asked for 1-to-1 versions and then clarified that this was for the first release only | New node: diff --git a/packages/mongospecs-web/README.md b/packages/mongospecs-web/README.md index 8fa6fd1..0354f9f 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.2.0, alpha.** The version is always the same as the `mongospecs` release it goes with. +> **Status: 0.2.0, alpha.** ``` pip install "mongospecs-web[fastapi]" "mongospecs[msgspec]"