Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python-publish.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
7 changes: 3 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion claudedocs/v2-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion packages/mongospecs-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand Down
Loading