Washington State law, regulation, and policy tracking service.
# Install Python dependencies (creates .venv, locks deps in uv.lock)
uv sync
# Install pre-commit hooks (runs ruff on commit)
uv run pre-commit install
# Load environment (production secrets, then dev/agent secrets)
export $(cat /etc/usa-wa/.env .env 2>/dev/null | xargs)Live service runs as systemd on port 8000 — never start uvicorn manually on that port.
Use port 8001 (= API_PORT + 1) for the dev server so the live service stays up:
uv run uvicorn usa_wa_api.api.main:app --host 0.0.0.0 --port 8001 --reload --log-config packages/usa-wa-api/src/usa_wa_api/log_config.jsonReachable at https://usa-wa.exe.xyz:8001/ via the exe.dev proxy.
A read-only /api/v1 over the published datasets (#184, flipped to the serving schema at
#313) — persons, organizations, roles, assignments (which are the tenure spans), sources,
coverage and provenance chains — plus the unversioned probes (/health, /ready,
/health/datasets, /health/serving) and the /datasets/* tree itself.
Read-only literally: since #313 the deployment registers no mutating route at all.
curl -s localhost:8001/api/v1/health/jobs | jq # last run per job slug (#178)
curl -s localhost:8001/api/v1/sources/wa_sos_filings/coverage | jq # declared vs verified (#180)
curl -s 'localhost:8001/api/v1/assignments?span_kind=chamber-house&as_of=2019-06-01' | jq
curl -s localhost:8001/openapi.json | jq '.paths | keys'Full route inventory, the pagination contract and the identifier form:
docs/API.md.
# Unit tier (#185) — no database at all; own coverage gate over packages/*/src/**
# (#198), so no flags needed. Add --no-cov for a faster, ungated inner loop
uv run pytest -m 'not db and not integration'
# Full suite (requires TEST_DATABASE_URL) — gated on the whole-tree floor
# (fail_under in pyproject.toml)
uv run pytest
# Single file (--no-cov: neither gate measures a slice)
uv run pytest --no-cov packages/usa-wa-api/tests/test_health.py
# Integration tests only (excluded by default)
uv run pytest -m integrationTEST_DATABASE_URL must be a dedicated test database, distinct from DATABASE_URL — the
test conftest enforces this, and teardown drops each declared schema CASCADE (not
Base.metadata.drop_all, which fails on the circular bill ↔ bill_version FKs).
Migrations need the owner role (DDL). In production, run the oneshot unit
(alembic upgrade head + scripts/grants.sql under DATABASE_URL_OWNER):
sudo systemctl start usa-wa-migrate. Ad-hoc alembic works when
DATABASE_URL_OWNER is set (alembic/env.py prefers it over DATABASE_URL):
uv run alembic upgrade head
uv run alembic revision --autogenerate -m "description"uv run ruff check .
uv run ruff format .Full command reference: docs/COMMANDS.md — it carries the
index of every operational CLI, grouped by the reference that documents each:
succession,
probes,
roster PDF.
Agent-facing docs (architecture, per-package module maps, deployment,
environment) are indexed under Detail Docs in AGENTS.md.
The systemd units live under deploy/ — the live API, a migrate
oneshot, and six timer-driven oneshots. (The PM sync sidecar and its three weekly
committee reconcilers were four more until usa-wa#314 retired the PM sync stack, and
the Postgres canonical tier's refreshes and invariant gates nine more until #412.)
Production secrets live in /etc/usa-wa/.env (managed manually on the VM, not in
the repo) — this file must exist before enabling any unit, or migrate (owner
DSN) and the services (app DSN) fail to start. The unit's ExecStartPre writes
the current git SHA to /run/usa-wa/build-id and exposes it as BUILD_ID.
To install on a fresh host, copy all units, then enable in this order at
provision time — migrate first, run synchronously by --now. Boot ordering is
already enforced by the units (the API declares
After=usa-wa-migrate.service, and migrate declares the reciprocal Before=;
the timer-driven oneshots below carry the same After=), so a reboot can't serve
against a not-yet-migrated schema. The --now here is for provision-time
synchrony: it runs migrate to completion before you enable the services in the
same session.
# Copy all units into systemd's path
sudo cp deploy/usa-wa*.{service,timer} /etc/systemd/system/
sudo systemctl daemon-reload
# 1. Migrate to head (owner role; RemainAfterExit oneshot — runs once now)
sudo systemctl enable --now usa-wa-migrate
# 2. Long-running service (app role)
sudo systemctl enable --now usa-wa
# Tail logs
sudo journalctl -u usa-wa -fThe deploy also ships timer-driven oneshots; a fresh host must enable their
timers explicitly — they are not pulled in by usa-wa.service. (The units
above already landed in /etc/systemd/system/ via the usa-wa* copy.)
Enable every timer the block enables. The nightly pipeline is the one that
matters most: its dbt tests and probes are the invariant gates, and they exit 1 and
email the operator (OnFailure=, #49) when the data drifts — skip it and nothing
fails, nothing alerts, and the absence looks identical to "no drift". The disk GC
(#394) is the same bargain one layer down: it exits 1
when free space runs out, which is how a nightly publish dies on ENOSPC instead
of publishing. The monthly roster re-check (#237) is the same bargain for a
document revised about twice a decade: it archives nothing and exits 4 when a new
edition is published — the alert is the only notice that one exists.
# Host hygiene (daily) — reclaim before the day's work, not after it fails
sudo systemctl enable --now usa-wa-disk-gc.timer # daily 05:45 UTC (#394)
# Dataset pipeline (daily) — the #302 publish chain
sudo systemctl enable --now usa-wa-pipeline.timer # daily 08:00 UTC (#311)
# Rollover measurement (daily, through 2027-02-28) — write-free; exit 4 = a WSL 2027-28 roster changed state
sudo systemctl enable --now usa-wa-wsl-availability-probe.timer # daily 09:20 UTC (#135)
# Backup (daily) — the database dump + the raw store to GCS, create-only; needs the bucket,
# the key and the role first (docs/RECOVERY.md § Install and first run)
sudo systemctl enable --now usa-wa-backup.timer # daily 10:17 UTC (#434)
# Sweep (weekly)
sudo systemctl enable --now usa-wa-integrity-sweep.timer # weekly Sun 08:00 UTC
# Edition re-check (monthly) — dry-run fetch; exit 4 = new roster edition → operator email
sudo systemctl enable --now usa-wa-roster-pdf-recheck.timer # monthly 1st 09:00 UTC (#237)
sudo systemctl list-timers 'usa-wa-*' # verify next-elapseWhat each one does is in docs/DEPLOYMENT.md § Services.
This block is pinned against deploy/*.timer by
scripts/tests/test_docs_timer_drift.py (#167) — a new timer fails the suite
until it is listed here with its own OnCalendar= cadence.