This file is for AI agents (and humans) changing dynamoxy itself. If you are using dynamoxy in an
application, read python/dynamoxy/AGENT_GUIDE.md (dynamoxy guide) instead.
dynamoxy is a typed, async DynamoDB ORM: a Rust core (crates/dynamoxy-core), a thin PyO3 binding
(crates/dynamoxy-py → dynamoxy._core) and the Python API (python/dynamoxy). Read
docs/architecture.md before any non-trivial change. It defines the layers,
the pipelines and the invariants you must preserve.
crates/dynamoxy-core/src/ Rust core (no Python)
client.rs (+ client/tests.rs) retries, timeouts, events, metrics, managed batches, waiters
backend/mod.rs the Backend port (one method per DynamoDB operation)
backend/aws/ AWS SDK adapter; errors.rs classifies every SDK error
backend/memory/ in-memory DynamoDB (items, query, batch, transact, partiql, admin)
expr/ expression language: parser, eval, update, projection, reserved words
value/ types/ AttributeValue, Number, item sizes; operation inputs/outputs
events.rs metrics.rs retry.rs config.rs error.rs
crates/dynamoxy-core/tests/ emulator.rs: parity scenarios (memory + MiniStack)
crates/dynamoxy-py/src/ PyO3 bindings: runtime.rs (Tokio + asyncio bridge), client.rs, convert.rs, errors.rs
python/dynamoxy/ Python API (models, schema, codecs, repository, transactions, query, ...)
python/dynamoxy/_core.pyi Type stubs of the native module - keep in sync with crates/dynamoxy-py
python/dynamoxy/AGENT_GUIDE.md Usage guide shipped in the wheel (`dynamoxy guide`)
tests/unit|integration|e2e pytest suites (integration runs on the memory backend AND MiniStack)
tests/models.py Shared test models (plain, single-table, pydantic)
examples/ Runnable examples (executed by tests/integration/test_examples.py)
docs/ Guides, reference (configuration/errors checked, events generated), ADRs
skills/dynamoxy/SKILL.md Skill for agents of applications using dynamoxy (copy into a project)
.claude/skills/ Skills for agents developing dynamoxy (this repository)
scripts/ check_consistency.py, generate_reference.py, release.py, release_notes.py, smoke_test.py
.github/workflows/ ci.yml, pr-title.yml, prepare-release.yml, tag-release.yml, release.yml
Requirements: Rust (stable, see rust-toolchain.toml) and uv. No Docker.
| Task | Command |
|---|---|
| Install/refresh everything (builds the extension) | uv sync |
| All checks (what CI runs) | make check |
| Python tests (unit, integration on memory and MiniStack, e2e) | uv run pytest |
| Only fast tests (no emulator) | uv run pytest -m "not e2e" |
| Rust tests | cargo test --workspace |
| Rust parity tests against the emulator | DYNAMOXY_TEST_ENDPOINT_URL=<url> cargo test -p dynamoxy-core --test emulator |
| Lint/format Python | uv run ruff check --fix python tests examples scripts && uv run ruff format python tests examples scripts |
| Type check Python | uv run pyrefly check |
| Lint/format Rust | cargo fmt --all && cargo clippy --workspace --all-targets -- -D warnings |
| Docs drift checks | uv run python scripts/check_consistency.py |
| Regenerate generated reference docs | uv run python scripts/generate_reference.py |
| Release wheels for all Linux targets | make wheels |
| Smoke-test an installed wheel | python -I scripts/smoke_test.py |
| Lint GitHub workflows | uvx --from actionlint-py actionlint (part of make lint) |
uv run ... rebuilds the native extension automatically when Rust sources change ([tool.uv] cache-keys). The pytest fixture starts MiniStack (a pip-installed AWS emulator, part of the test
dependency group) by itself unless DYNAMOXY_TEST_ENDPOINT_URL is set. Set CARGO_TARGET_DIR
outside the repository if the workspace is a slow mounted volume.
- Respect the layer split. Protocol, timing, I/O and the DynamoDB emulation → Rust core. Models, validation, codecs, repositories, guards, hooks, middleware, OpenTelemetry → Python. The binding crate contains conversions only.
- Never break the invariants in docs/architecture.md (fail fast before I/O, no values in expressions, unique guards consistent, versions checked, both backends equal, nothing dropped silently, fork safety, clean exit, stored formats stable, no payload logging).
- Every behaviour change ships with tests, docs and a changelog-worthy commit message. CI
enforces formatting, lint, types (pyrefly), clippy
-D warnings, tests on Python 3.12-3.15, andscripts/check_consistency.py. - Both backends. A backend operation is implemented in the AWS adapter and the memory
backend, with DynamoDB's semantics and error codes, and tested against both. Python
integration tests use the
client/client_factoryfixtures, so they run on both. - Errors are actionable. Validation errors say what to change; service errors keep the code, the request id and a retry classification; model-level failures map to specific errors.
- The public API is typed and documented. Google-style docstrings, PEP 695 generics,
from __future__ import annotations, noAnywhere a real type exists,@overrideon overrides. Rust public items need docs (missing_docs);unsafeis forbidden. - No required runtime dependencies in the Python package. pydantic and OpenTelemetry are extras, imported lazily.
- Stored formats are API (codecs, timestamps,
_type, guard keys, cursors): keep reading the old format, write an ADR, mention it in the changelog.
New or changed client option:
-
Client.__init__parameter with docstring and validation (or a dataclass field) - Rust
ClientConfigfield (serde,deny_unknown_fields) if the core needs it, andto_core() - tests for validation and the mapping
-
docs/reference/configuration.md(CI checks every parameter) and the guides -
python/dynamoxy/AGENT_GUIDE.mdcheat sheet if applications should know it
New field option or codec: attributes.py (specifier and overloads), schema.py (FieldSpec,
validation), codecs.py (codec, resolve_codec), pydantic marker if applicable, unit tests,
docs/reference/configuration.md, modeling guide, AGENT_GUIDE.
New event kind:
-
event_kinds!incrates/dynamoxy-core/src/events.rs(name, level, description), or for Python-only kindsEventKind+PYTHON_EVENT_KINDSinpython/dynamoxy/events.py -
EventKindinpython/dynamoxy/events.pyin the same position as in Rust - emit it, and test that it is emitted
-
uv run python scripts/generate_reference.py
New native API: Rust binding in crates/dynamoxy-py, python/dynamoxy/_core.pyi (CI compares
names and methods), Python wrapper, tests.
New backend operation: Backend trait method (backend/mod.rs), types in types/ops.rs, AWS
adapter (+ conversions, errors in one place), memory backend with DynamoDB's semantics and error
messages, Client method in client.rs (retries, events, metrics), binding, stub, Python wrapper,
tests in Rust (memory + tests/emulator.rs) and Python (tests/integration).
New repository or model behaviour: plan it in repository.py (_plan_* so transactions get it
too), map failures in explain_failure, test on both backends (tests/integration), update the
guides and AGENT_GUIDE.
Bug fix: reproduce with a failing test first (memory backend if possible, emulator if it is a protocol issue), fix, keep the test.
- Rust unit tests live next to the code (
#[tokio::test(start_paused = true)]for timing). Memory backend behaviour tests are inbackend/memory/tests.rs; each documents a DynamoDB rule. - Python integration tests take the
clientfixture: each scenario runs on the memory backend and on MiniStack. Faults (dynamoxy.testing.inject_fault) only exist in the memory backend: such tests build their own memory client (tests/integration/test_faults.py). - Assert on events, stats, counts and
dynamoxy.collect()records instead of sleeping. Use unique names; the fixtures give emulator clients a unique table prefix. - A test that fails only on MiniStack usually reveals real DynamoDB behaviour: check the DynamoDB documentation, model it in the memory backend, and only treat it as an emulator quirk (with a comment) when the documentation says otherwise.
- Examples are tests: if you change the API, update
examples/. - Never pipe test output through
head/tailwithoutset -o pipefail.
Update in the same change: the guide in docs/guides/, docs/reference/* (generated files via
the script), python/dynamoxy/AGENT_GUIDE.md and skills/dynamoxy/SKILL.md for user-facing
behaviour, docs/architecture.md for structural changes, a new ADR for decisions, docstrings
everywhere, and CHANGELOG.md notes under [Unreleased] when the commit message is not enough.
The README stays short and links to the docs.
- Conventional commits (
feat:,fix:,perf:,refactor:,docs:,test:,ci:,build:,chore:;feat!:orBREAKING CHANGE:for breaking changes). PR titles are checked, squash merges use them, and git-cliff builds the changelog from them. - One version for the workspace (
Cargo.toml[workspace.package]); the Python package takes it from the binding crate. While 0.x, breaking changes bump the minor version. - Release: run the "Prepare release" workflow, polish the generated CHANGELOG.md section in the PR
and squash-merge it. The tag is created and
release.ymlbuilds, tests and publishes. See.claude/skills/dynamoxy-release.
- Calling into Python from Rust threads (events are pulled by Python).
- Threads or Tokio runtimes outside
crates/dynamoxy-py/src/runtime.rs, or awaitables not created withruntime::future_into_py. - Holding a
parking_lotlock across.await. - Putting values into expression strings, or attribute names without placeholders.
- Long timeouts in tests;
time.sleepin async code; global mutable state in Python modules. - Logging item contents or key values (they may contain personal data).
- Weakening validation to make a test pass; skipping the memory backend; listing a class (rather
than functions) in
dataclass_transform(field_specifiers=...).