Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
69a1602
build!: restart as a uv workspace for the v2 rewrite
jaykv Oct 13, 2026
dc35f38
feat!: async ODM core for msgspec, pydantic and attrs (0.2.0)
jaykv Oct 13, 2026
81a6fbe
style: format the code block in the package README
jaykv Oct 13, 2026
ea29457
feat(web): mongospecs-web 0.1.0, framework-neutral REST resources
jaykv Oct 13, 2026
4f3b2c1
feat(bench): benchmark and stress harness against real MongoDB
jaykv Oct 13, 2026
3c415da
perf(core): raw-BSON snapshots and batched cursor reads; e2e storefront
jaykv Oct 13, 2026
6dc837f
perf(web): native JSON serialisation and untracked reads; fix load ge…
jaykv Oct 13, 2026
724d20e
docs(bench): benchmark results, conclusions and decisions
jaykv Oct 13, 2026
d1544af
test(core): consumer typing fixtures checked with mypy
jaykv Oct 13, 2026
279bd6c
test(web): compare timestamps as instants, not strings
jaykv Oct 13, 2026
fb9d31c
test(web): compare timestamps as instants, not strings
jaykv Oct 13, 2026
4d26a76
Merge branch 'v2/web' into v2/bench
jaykv Oct 13, 2026
426c0b1
fix(core,web): defects found in review of the v2 pull requests
jaykv Oct 13, 2026
ea43cec
feat(web)!: harden the request path
jaykv Oct 13, 2026
e695763
docs,ci: review follow-ups
jaykv Oct 13, 2026
a78acb2
fix(core,web): defects from the second review of v2
jaykv Oct 13, 2026
976d41e
fix(web): enforce Policy(private=True) on fields of embedded models
jaykv Oct 13, 2026
0dde820
feat(core): StoreNone, OmitNone and SpecConfig(none=...)
jaykv Oct 13, 2026
49a32a9
bench: B9, CPU per call and commands per operation on the hot paths
jaykv Oct 13, 2026
ce91731
perf(core,web): cut work on the read, include, delete and routing paths
jaykv Oct 13, 2026
3ee709b
feat(web): Api(max_concurrency=...) and a 503 for pool timeouts
jaykv Oct 13, 2026
fb543de
bench: B9 also measures serialisation alone, through the model and fr…
jaykv Oct 13, 2026
9775738
docs,bench: production tuning guide, B9 before/after, review decisions
jaykv Oct 13, 2026
0748110
fix(core): serialise BSON-only values held in untyped fields
jaykv Oct 13, 2026
933df43
feat(web)!: PATCH merges into embedded objects
jaykv Oct 13, 2026
ecb42e6
feat(web): Put operation and Resource(merge_patch=False)
jaykv Oct 13, 2026
7a4012b
fix: nine defects found in the third review of v2
jaykv Oct 13, 2026
0c5388a
ci,build: run each CI job once per push; complete the extras
jaykv Oct 13, 2026
c5eba4d
docs: getting-started tutorial, runnable examples and four topic guides
jaykv Oct 13, 2026
c23ffbc
chore: changelog, contributing and security files; publish the docs
jaykv Oct 13, 2026
1dac7f6
feat(core): validate documents before they are written
jaykv Oct 13, 2026
92df625
build(docs): switch from mkdocs-material to zensical
jaykv Oct 13, 2026
7b75204
Merge pull request #9 from jaykv/v2/review-fixes
jaykv Oct 13, 2026
0bb35d3
Merge pull request #8 from jaykv/v2/bench
jaykv Oct 13, 2026
011f3e5
Merge pull request #7 from jaykv/v2/web
jaykv Oct 13, 2026
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
76 changes: 76 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
# Branches are tested through their pull request; a push trigger for them too ran every job twice.
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --all-packages --python 3.13
- run: make lint
- run: make types
- run: make build
- run: make docs

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["3.11", "3.12", "3.13", "3.14", "3.15"]
mongo: ["8.0"]
experimental: [false]
include:
- python: "3.11"
mongo: "7.0"
experimental: false
- python: "3.13"
mongo: "7.0"
experimental: false
- python: "3.15"
mongo: "7.0"
experimental: false
- python: "3.14t"
mongo: "8.0"
experimental: true
continue-on-error: ${{ matrix.experimental }}
env:
MONGO_VERSION: ${{ matrix.mongo }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Start MongoDB (replica set and sharded cluster)
run: make up
- run: uv sync --all-packages --python ${{ matrix.python }}
- run: uv run pytest
- name: Container logs
if: failure()
run: |
docker compose -f docker/compose.rs.yaml logs --tail 100
docker compose -f docker/compose.sharded.yaml logs --tail 100

tracing:
# End to end: Sentry SDK -> mongospecs-web -> MongoDB, received by a Sentry-compatible server.
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: docker compose -f docker/compose.rustrak.yaml up -d
- run: uv sync --all-packages --python 3.13
- name: Wait for rustrak
run: timeout 60 sh -c 'until curl -sf http://localhost:8787/health >/dev/null; do sleep 1; done'
- run: uv run mongospecs-bench trace
36 changes: 36 additions & 0 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Publishes the documentation site to GitHub Pages from main.
# Requires Settings > Pages > Source: "GitHub Actions".
name: Docs

on:
push:
branches: [main]
paths: ["docs/**", "examples/**", "zensical.toml", ".github/workflows/docs.yaml"]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
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
64 changes: 0 additions & 64 deletions .github/workflows/python-app.yaml

This file was deleted.

72 changes: 42 additions & 30 deletions .github/workflows/python-publish.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Upload Python Package
# Publishes whichever distribution the release tag names:
# mongospecs-v0.2.0 -> packages/mongospecs
# mongospecs-web-v0.1.0 -> packages/mongospecs-web
name: Publish

on:
release:
Expand All @@ -16,28 +11,45 @@ permissions:
contents: read

jobs:
deploy:
verify:
# Nothing is published from a commit that does not pass the checks.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: make up
- run: uv sync --all-packages --python 3.13
- run: make lint
- run: make types
- run: uv run pytest

publish:
needs: verify
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/mongospecs
# permissions:
# id-token: write
permissions:
id-token: write
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
# uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Pick the package from the tag
id: pick
run: |
tag="${GITHUB_REF_NAME}"
case "$tag" in
mongospecs-web-v*) echo "package=mongospecs-web" >> "$GITHUB_OUTPUT" ;;
mongospecs-v*) echo "package=mongospecs" >> "$GITHUB_OUTPUT" ;;
*) echo "unrecognised release tag: $tag" >&2; exit 1 ;;
esac
- name: The tag must be the version being published
run: |
package="${{ steps.pick.outputs.package }}"
declared="$(uv run --no-project python -c "import tomllib, sys; print(tomllib.load(open(sys.argv[1], 'rb'))['project']['version'])" "packages/$package/pyproject.toml")"
tagged="${GITHUB_REF_NAME#"$package"-v}"
if [ "$declared" != "$tagged" ]; then
echo "tag $GITHUB_REF_NAME says $tagged, but packages/$package/pyproject.toml says $declared" >&2
exit 1
fi
- run: uv build --package ${{ steps.pick.outputs.package }}
- uses: pypa/gh-action-pypi-publish@release/v1
16 changes: 14 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ venv.bak/
# Rope project settings
.ropeproject

# mkdocs documentation
# zensical documentation
/site

# mypy
Expand All @@ -128,4 +128,16 @@ dmypy.json
# Pyre type checker
.pyre/

.vscode/
.vscode/

# uv
.venv-*/

# ruff
.ruff_cache/

# macOS
.DS_Store

# docs build
site/
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Changelog

Two packages are released from this repository; 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**.

## Unreleased

### mongospecs 0.2.0

A rewrite. See [Migrating from 0.1.x](docs/migration.md) for the API map.

- **Breaking:** async only, Python 3.11+, MongoDB 7.0+. The synchronous 0.1.x line stays on the
`0.1.x` branch and as `mongospecs<0.2`.
- **Breaking:** models are bound to an explicit `Registry`; there is no class-level client.
- **Breaking:** msgspec is no longer installed implicitly; choose an extra (`[msgspec]`,
`[pydantic]`, `[attrs]`).
- Change tracking by snapshot: `save()` sends only what changed.
- Typed, immutable queries with `Q`; `tracked`, `lean()` and `raw()` read modes; cursor and offset
pagination.
- Atomic update operators, real upsert, `bulk_write`, `bulk_save`, partial-failure `insert_many`.
- Relations: `Ref`, `Backref`, `include()`, cascade rules, `$graphLookup`, `$unionWith`.
- Document hooks and plugins; timestamps, soft delete and revisions by field marker.
- Transactions as a retried callback that restores in-memory documents when it aborts.
- Index planning with gated destructive actions; change streams; per-command observability events
with logging, OpenTelemetry and Sentry listeners.
- Document hooks run for `insert_many` and `bulk_save`.
- `insert`, `save`, `insert_many` and `bulk_save` validate the document before writing it, on
every backend. `validate=False` on a call or `SpecConfig(validate_writes=False)` on a model
turns the check off.
- 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

First release.

- 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
scope for authorization; RFC 9457 errors; ETag and `If-Match`; OpenAPI 3.1.
- `PATCH` merges into embedded objects (RFC 7396); `Resource(merge_patch=False)` makes it replace
them. `PUT` is available as the `Put` operation.

Changes made after the first review builds, for anyone who tried those:

- **Breaking:** `?total=true` is opt-in per resource (`page=Cursor(total=True)`).
- **Breaking:** `PATCH` on an embedded object is partial. Send `null` for keys to clear, or set
`merge_patch=False`.
- **Breaking:** `If-Match` on a model without a `Revision` field is answered with 412.
- **Breaking:** a filter or sort key given twice is a 400, as are `NaN` and `Infinity` in a body.
- **Breaking:** a guard or hook registered for an operation the resource does not have raises
`ValueError`.
51 changes: 51 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Contributing

## Setting up

You need [uv](https://docs.astral.sh/uv/) and Docker.

```
make install # uv sync for the whole workspace
make up # MongoDB in Docker: a replica set on :27117 and a sharded cluster on :27118
make check # ruff, mypy --strict, and the whole test suite
make down # stop MongoDB and delete its data
```

Tests run against real MongoDB; nothing is mocked. `make test-fast` leaves out the tests that
need the sharded cluster. To use another server, set `MONGOSPECS_TEST_URI` (a replica set) and
`MONGOSPECS_TEST_SHARDED_URI`.

## Where things are

| Path | What |
|---|---|
| `packages/mongospecs` | the ODM |
| `packages/mongospecs-web` | the REST layer and its framework plugs |
| `bench` | benchmarks, stress scenarios and the storefront reference application; never published |
| `examples` | the files shown in the tutorial; they are tests too |
| `docs` | the documentation site (`make docs` builds it) |
| `claudedocs/v2-plan.md` | the design of 0.2 and the record of every decision that departed from it |

## What a change needs

- **A test that fails without it.** For a bug, reproduce it first. Tests for behaviour that
differs by model library are parametrised over `msgspec`, `pydantic` and `attrs`.
- **Docs for anything a user can see.** `python` blocks in `docs/guide` and `docs/web` are
executed in order, per page, against MongoDB; `py` blocks are shown but not run. Prefer `python`.
- **A changelog entry** in `CHANGELOG.md`, marked **Breaking** when it is.
- **`make check` passing.**

Keep changes to one concern. If a change departs from the design in `claudedocs/v2-plan.md`, add
an amendment row there saying what and why.

## Plugs for other frameworks

A plug only translates requests and responses. Prove it with the contract suite:
`mongospecs_web.testing.run_contract` must produce the same transcript as the reference plug. See
[Plugs](docs/web/plugs.md).

## 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.
Loading
Loading