Setup
The project requires Python 3.13 (>=3.13,<3.14) and uses uv as the only supported package manager — never pip, pip3, or python3 -m pip. uv resolves everything against the committed uv.lock, which guarantees a deterministic, reproducible environment across machines and CI; bare pip bypasses that lock file and causes environment drift.
1
Clone the repository
2
Install dependencies
dev dependency group (pytest, bandit, and friends) from uv.lock.3
Verify the CLI works
Everyday commands
Run everything throughuv run so it executes inside the project virtual environment.
uv run pytest uses --import-mode=importlib and excludes integration tests by default — both are configured in pyproject.toml under [tool.pytest.ini_options] via addopts = "--import-mode=importlib -m 'not integration' -p hypothesispytest".Linting
All linters and formatters run through trunk, never as bare binaries. Tools likeruff, bandit, yamllint, shellcheck, prettier, and actionlint live inside trunk’s sandbox — invoking them directly either fails with command not found or picks up the wrong configuration.
trunk check --filter=<TOOL> <PATH> with the tool name from the CI output.
Layer rules
The repository enforces a strict layered architecture:libs/<service>/wraps one external SDK or API with idiomatic Python types and functions. Nolibs/<x>module may import fromlibs/<y>. If two adapters need to coordinate, that coordination belongs insrc/.src/is the orchestration layer: multi-step workflows, side effects, and Modal@app.function/@modal.fastapi_endpointdecorators. Adapter modules inlibs/must stay callable in isolation — no orchestration insidelibs/.cli/is Typer-only: parse arguments → preflight → call intosrc/→ render output. No business logic incli/.
Where new code goes
- External SDK call? New file in
libs/<service>/. Wrap one SDK only, no cross-lib imports. - Multi-step flow or Modal endpoint?
src/<service>/. If the module defines Modal endpoints, add its import to_ENDPOINT_MODULESinsrc/app.pyso the decorators register. - User-facing command?
cli/<group>/as a Typer subapp that calls intosrc/. Wire it intocli/main.pyviaapp.add_typer(...). - Standalone data product?
data-gen/<product>/. Self-contained and independent of other data products. - Webhook handler?
webhooks/<name>.py. Each handler is an independent Modal app — do not register it insrc/app.py.
Testing
- Tests mirror the source layout:
tests/cli/,tests/libs/,tests/src/, andtests/integration/. Put new tests in the directory that mirrors the code under test. - Tests that hit live external APIs carry the
integrationmarker, defined inpyproject.toml. The defaultaddoptsfilter (-m 'not integration') keeps them out of ordinary runs, souv run pytestnever needs credentials. - Plain
assertstatements are allowed in tests, but each test file must carry its own# ruff: noqa: S101, ...header — trunk invokes ruff with its own config and never readspyproject.toml’sper-file-ignores, so a green localruff checkcan still land a red CI.
Property-based tests
Alongside the example-based suite, some modules carry property tests written with Hypothesis. These live intest_<module>_properties.py next to the example tests they complement, and they do not replace them: an example test pins a specific known input and output, while a property states a rule that must hold for every input and lets Hypothesis search for a counterexample.
tests/conftest.py and chosen with HYPOTHESIS_PROFILE (dev by default). CI uses the ci profile, which seeds generation from a hash of the test function so a failure reproduces exactly rather than looking like a flaky test.
Reach for a property when the claim you want to make is universal — “never raises on any input”, “idempotent”, “round-trips through serialization”, “output is always lowercase”. Reach for an example when you want to pin one specific behavior, or to document a bug that must not come back.
- If the domain is finite and enumerable, check it exhaustively with a loop instead of
st.sampled_from.max_examplescaps how many values Hypothesis draws, and because the CI profile uses a fixed seed, it draws the same subset on every run — leaving the rest permanently untested. Hypothesis is most valuable on unbounded domains such as arbitrary text. - Make sure the property is not vacuous. If its interesting branch only runs when a parse succeeds, bare
st.text()will almost never reach it. Generate realistic inputs, and callhypothesis.event()so the branch split shows up under--hypothesis-show-statistics.
Conventions
-
Temporary files go in
tmp/only. The directory is gitignored. Never write scratch output to the repo root or next to source code. -
Anchor script file I/O on the script’s own directory, not the CWD.
uv run path/to/script.pydoes not change the working directory, so relative paths resolve from wherever the command was invoked and can silently write files to the wrong place: -
Scripts under
scripts/are directly executable withuvshebangs where practical. Scripts that need Infisical secrets must show the full flag form in their usage text (--projectId,--token,--env) rather than assuminginfisical inithas run: - Docstrings explain why, not what. Document decisions and gotchas inline, next to the code they affect.
-
No summary or investigation
.mdfiles. Live documentation belongs in code (docstrings, per-module READMEs); the docs site underdocs/is the only place for prose documentation.
Docs site
Documentation pages live indocs/. Local preview requires Node 24 (pinned in docs/.node-version): install the docs CLI with npm i -g mint, then run mint dev from inside docs/. Every page needs title and description frontmatter — the description becomes the page’s llms.txt entry.