Local development

Requirements

  • uv (Python 3.12 is installed by uv if missing)

  • GNU make

  • Read access to the private repositories pinned in pyproject.toml: pybFlaskUtils, EventLoggerPySDK and MessagingEmail

  • For SAM targets: the AWS SAM CLI. For make run-local: Docker as well.

Quick start

make install   # .venv from uv.lock, plus pre-commit hooks (ruff format, lint, complexity)
make check     # lint (incl. complexity), typecheck, name-clash guard, tests

Every command CI runs is a make target, so a green make check locally means a green CI job, apart from sam-validate and the lockfile check (uv lock --check).

Make targets

Usage: make <target> [ENV=dev|prod] [AWS_PROFILE=...]

  help                           Show this help
  install                        Create .venv with runtime and dev dependencies (uv sync) and install the git hooks
  lock                           Re-resolve uv.lock after editing pyproject.toml
  upgrade                        Upgrade every dependency to the newest allowed version
  hooks                          Install the pre-commit git hooks (skipped in CI and outside a git checkout)
  format                         Format code and apply safe lint autofixes (rewrites files)
  lint                           Check formatting, lint rules and complexity (changes nothing)
  typecheck                      Type-check src with mypy
  test                           Run the test suite
  test-cov                       Run the test suite with a coverage report (coverage.xml + terminal)
  check-names                    Fail if a src/ top-level package name is shadowed by an installed distribution
  check                          Run lint, typecheck, the name-clash guard and tests
  docs-help                      Refresh the make target list included in the docs
  clean                          Remove build output, caches and coverage files
  sam-validate                   Validate template.yaml (sam validate --lint)
  sam-build                      Build all functions into .aws-sam/
  run-local                      Serve the API locally on :3000 (sam local start-api, needs Docker)
  deploy                         Build and deploy the stack for ENV (dev or prod) using samconfig.toml
  build-ApiFunction              (sam) Build ApiFunction
  build-WorkerFunction           (sam) Build WorkerFunction

ENV (dev or prod) selects the samconfig.toml environment for SAM targets, and AWS_PROFILE is passed to SAM as --profile.

Complexity limits

make lint fails when a function in src/, tests/ or scripts/ is too complex. Two measures are checked, both configured in pyproject.toml:

Measure Limit Tool and what it counts

Cyclomatic complexity

10

ruff C901 ([tool.ruff.lint.mccabe]): the number of independent paths, one per branch or loop

Cognitive complexity

15

complexipy ([tool.complexipy]): branches and loops weighted by how deeply they are nested, so flat code scores low even with many cases

To see the scores, run uv run complexipy --sort desc. When a function goes over, flatten it (early returns, a lookup table instead of an if/elif chain, small helpers) rather than raising the limit.

Docstrings

make lint also fails when a module, class or public function in src/ or scripts/ has no docstring (ruff D1; tests, magic methods and __init__ are exempt). Write what the code is responsible for and what callers need to know, such as what it raises or returns when something is missing, rather than restating the name.

Dependencies

  • Add or change a dependency in pyproject.toml, then run make lock and commit uv.lock.

  • make upgrade re-resolves everything to the newest allowed versions.

  • The private libraries are pinned to git tags in [tool.uv.sources].

Tests

make test        # pytest
make test-cov    # with coverage (terminal + coverage.xml)
uv run --frozen pytest tests/integration/test_handlers.py   # a subset

Tests need no AWS access or network. tests/conftest.py sets fake AWS credentials and AWS_SECRETS_MANAGER=False before anything is imported, moto’s mock_aws provides DynamoDB, SNS and Kinesis, and HTTP calls to internal APIs are mocked with requests-mock.

Running the API locally

make run-local AWS_PROFILE=dev

This builds the functions and serves ApiFunction on http://127.0.0.1:3000 with sam local start-api, using the staging settings with triggers off. It reads common/crm and the _staging tables of the staging account through AWS_PROFILE, so treat it as staging.

  • The first request takes a while: SAM pulls the Lambda image and starts a container.

  • SAM logs pyb-api-gateway-authorizer not found on each request. The authorizer lives in another stack, so it is skipped locally and requests go straight to the app.

  • Run make sam-build again after code changes.

Scripts

scripts/ holds one-off operational scripts (rule migration, history listing, test events). Tokens and keys come from environment variables, for example:

EVENTLOGGER_SESSION_TOKEN=... uv run python scripts/list_rules_history.py