Skip to main content

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

This installs the runtime dependencies and the dev dependency group (pytest, bandit, and friends) from uv.lock.
3

Verify the CLI works

Everyday commands

Run everything through uv 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 like ruff, 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.
To reproduce a specific finding from CI, use 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. No libs/<x> module may import from libs/<y>. If two adapters need to coordinate, that coordination belongs in src/.
  • src/ is the orchestration layer: multi-step workflows, side effects, and Modal @app.function / @modal.fastapi_endpoint decorators. Adapter modules in libs/ must stay callable in isolation — no orchestration inside libs/.
  • cli/ is Typer-only: parse arguments → preflight → call into src/ → render output. No business logic in cli/.
If you add a new top-level package, update [tool.setuptools.packages.find] in the matching pyproject.toml (root, or cli/pyproject.toml for CLI packages) — otherwise the package is not importable after install.

Where new code goes

  1. External SDK call? New file in libs/<service>/. Wrap one SDK only, no cross-lib imports.
  2. Multi-step flow or Modal endpoint? src/<service>/. If the module defines Modal endpoints, add its import to _ENDPOINT_MODULES in src/app.py so the decorators register.
  3. User-facing command? cli/<group>/ as a Typer subapp that calls into src/. Wire it into cli/main.py via app.add_typer(...).
  4. Standalone data product? data-gen/<product>/. Self-contained and independent of other data products.
  5. Webhook handler? webhooks/<name>.py. Each handler is an independent Modal app — do not register it in src/app.py.

Testing

  • Tests mirror the source layout: tests/cli/, tests/libs/, tests/src/, and tests/integration/. Put new tests in the directory that mirrors the code under test.
  • Tests that hit live external APIs carry the integration marker, defined in pyproject.toml. The default addopts filter (-m 'not integration') keeps them out of ordinary runs, so uv run pytest never needs credentials.
  • Plain assert statements 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 reads pyproject.toml’s per-file-ignores, so a green local ruff check can still land a red CI.

Property-based tests

Alongside the example-based suite, some modules carry property tests written with Hypothesis. These live in test_<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.
Profiles are registered in 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.
Two things to get right when writing one:
  • If the domain is finite and enumerable, check it exhaustively with a loop instead of st.sampled_from. max_examples caps 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 call hypothesis.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.py does 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 with uv shebangs where practical. Scripts that need Infisical secrets must show the full flag form in their usage text (--projectId, --token, --env) rather than assuming infisical init has run:
  • Docstrings explain why, not what. Document decisions and gotchas inline, next to the code they affect.
  • No summary or investigation .md files. Live documentation belongs in code (docstrings, per-module READMEs); the docs site under docs/ is the only place for prose documentation.

Docs site

Documentation pages live in docs/. 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.
Last modified on August 22, 2026