Testing & contributing¶
Layout¶
A uv workspace: each module is its own
publishable package under packages/, all sharing one lockfile and dev environment. They install
into the single firm import namespace (PEP 420), so imports are always firm.queue, firm.cache,
etc. — regardless of which packages you installed.
firm/
├── pyproject.toml # virtual workspace root: members, dev tools, lint/type config
├── packages/
│ ├── firm-core/ src/firm/_core/ # shared: engine, dialects, poller, clock, config, process
│ ├── firm-queue/ src/firm/queue/ # firm.queue (jobs) + firm.contrib + migrations/
│ ├── firm-cache/ src/firm/cache/ # firm.cache (cache) + migrations/
│ ├── firm-channel/ src/firm/channel/ # firm.channel (pub/sub) + migrations/
│ ├── firm-audit/ src/firm/audit/ # firm.audit (audit log) + migrations/
│ ├── firm-ui/ src/firm/ui/ # firm.ui (dashboard; depends on all four)
│ └── firm/ # meta-package (no code; installs the four modules)
├── tests/{queue,cache,channel,audit,ui,contrib}/
└── docs/
Each module depends only on firm-core; firm-ui depends on all four; the firm meta-package pins
them together. See Split into a uv workspace for the rationale.
Setup¶
The gate¶
uv run pytest # tests
uv run ruff check && uv run ruff format --check # lint + format
uv run ty check packages scripts examples # types
All must pass. The codebase is fully type-annotated (checked with Astral's ty) and formatted
with ruff.
Running against Postgres and MySQL¶
By default the suite runs on SQLite. Point it at live databases and every database-touching test also runs against them (fresh schema per test):
# spin up servers (any Docker works)
docker run -d --name bb_pg -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=bb -p 5433:5432 postgres:15
docker run -d --name bb_mysql -e MARIADB_ROOT_PASSWORD=pw -e MARIADB_DATABASE=bb -p 3307:3306 mariadb:11
export FIRM_TEST_PG_URL="postgresql+psycopg://postgres:pw@localhost:5433/bb"
export FIRM_TEST_MYSQL_URL="mysql+pymysql://root:pw@localhost:3307/bb"
uv run pytest -p no:cacheprovider # sqlite + postgres + mysql
Test ids are suffixed with the backend (...[postgres], ...[mysql]); run one backend with
-k postgres.
Notes:
- Fork-mode tests (the forking supervisor) are SQLite-only — the fork model is independent of the SQL backend, so it's exercised once.
- Offline dialect-compile tests (
test_dialect_compile.py) assert the DDL andSKIP LOCKEDSQL render correctly for Postgres/MySQL with no live database.
Upstream test parity¶
firm is a port of the Rails Solid stack (solid_queue / solid_cache / solid_cable), but it is
its own project with its own suite — not a mirror of upstream. The porting rule is simple: every
upstream test case is handled as one of firm's own behavior tests, living beside the functionality
it exercises (a claim spec in test_claim.py, an eviction spec in test_expiry.py, and so on).
Where firm deliberately behaves differently from Rails, that divergence is written up in
comparison-to-rails.md rather than left as a red test.
Tests ported from upstream keep a short # upstream: <file>.rb :: <case> comment so the lineage is
discoverable when reconciling against a newer Solid release — but they are ordinary firm tests: they
assert firm's behavior and go green.
Conventions¶
- Tests are spec-style and grouped by behavior. New behavior gets a failing test first.
- Keep the public API small; internal machinery lives under
_core/. - Match the surrounding style — concise docstrings, explicit SQL via SQLAlchemy Core, no clever metaprogramming.
Releasing¶
Every package under packages/ is published to PyPI independently (they share the firm.*
namespace but carry their own versions). Publishing is fully automated through
.github/workflows/release.yml using
PyPI trusted publishing — there are no PyPI tokens
to manage. The workflow checks the changelog, builds, runs the test suite, twine checks,
uploads, and finishes by creating a GitHub Release for the tag — the changelog section for the
released version is the entire body (no auto-generated PR list: it isn't path-aware and would
pull in unrelated packages' PRs).
Changelogs¶
Each package keeps a packages/<name>/CHANGELOG.md in
Keep a Changelog format. PRs that make a user-visible
change add a line under the affected package's ## [Unreleased] section — a review convention,
not CI-enforced. At release time it is enforced: the workflow runs
scripts/check_changelog.py and refuses to publish a version that has no matching
## [<version>] section. The changelog ships in the sdist, and PyPI links it in each package's
sidebar via the Changelog project URL.
Releasing one package¶
To release one package (the common case):
- In one PR: bump
versioninpackages/<name>/pyproject.tomland retitle the package's## [Unreleased]section to## [<version>] - <YYYY-MM-DD>(leave a fresh, empty## [Unreleased]above it). If other packages should pick up the new version, adjust their~=pins in the same PR. - Tag the merged commit
<name>-v<version>and push the tag:
The workflow refuses to publish if the tag version doesn't match the package's
pyproject.toml, or if the changelog has no section for it.
Releasing everything¶
To release everything at once (e.g. a coordinated bump), tag v<version>. That builds all
packages and uploads whatever isn't already on PyPI — previously published files are skipped, so
the tag is safe even when some packages didn't change (their changelogs already carry the
section for their current, released version).
Versions follow Semantic Versioning: breaking changes bump
the major version. The firm meta-package's extras pin modules with ~=, so a meta-package
release is only needed when those pins change.
Meta-package status: the PyPI name
firmis held by a dormant, release-less project and a PEP 541 name-transfer request is pending. Until it's granted, the release workflow excludes the meta-package fromv*all-package tags (see therm dist/firm-[0-9]*line inrelease.yml), docs use the per-package install form (pip install firm-queue), and the extras form is provided by the interimfirm-stackmeta-package (packages/firm-stack/— keep its extras/pins in lockstep withpackages/firm/). Once the name is ours: remove thatrmline, publish the meta with afirm-v<ver>tag, and thefirm[queue]extras form starts working;firm-stackthen stays as a compatible alias.
Building the docs¶
The documentation site is built with Zensical (configured in
zensical.toml at the repo root):