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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
48 changes: 32 additions & 16 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion .github/workflows/python-publish.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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**.

Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions claudedocs/v2-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

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.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]"
Expand Down
2 changes: 1 addition & 1 deletion packages/mongospecs-web/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
2 changes: 1 addition & 1 deletion packages/mongospecs-web/src/mongospecs_web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
Response,
)

__version__ = "0.1.0"
__version__ = "0.2.0"

__all__ = [
"Api",
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading