Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7733791
Document the simulation contract library for ruff D
anth-volk Oct 13, 2026
d8b7c31
Document the FastAPI support library for ruff D
anth-volk Oct 13, 2026
0c3e22e
Document the simulation observability library for ruff D
anth-volk Oct 13, 2026
cb10b28
Document the Stage 12 persistence library for ruff D
anth-volk Oct 13, 2026
c90115a
Document the simulation entrypoint for ruff D
anth-volk Oct 13, 2026
4b762a1
Document the simulation gateway for ruff D
anth-volk Oct 13, 2026
25f905f
Document the simulation executor for ruff D
anth-volk Oct 13, 2026
1cdf364
Document the integration tests, repo scripts and package markers for …
anth-volk Oct 13, 2026
99694eb
Document the Stage 12 schema-DDL proof fixtures for ruff D
anth-volk Oct 13, 2026
0956db3
Enable ruff group 4 (docstrings)
anth-volk Oct 13, 2026
0cdc1dc
Drop redundant return None from documented no-op methods
anth-volk Oct 13, 2026
14ad6fb
Document published contract models and routes
anth-volk Oct 13, 2026
9d17ea0
Use the Public API's docstrings in the shared rules tool
anth-volk Oct 13, 2026
c55adf5
Register D in the ruff-lint rule
anth-volk Oct 13, 2026
c9f7349
Put the docstrings #767 added in the imperative mood
anth-volk Oct 13, 2026
b7a380e
Put the docstrings #743 and #744 added in the D style
anth-volk Oct 14, 2026
a4051e8
Put the docstrings #789 added in the D style
anth-volk Oct 14, 2026
00ae5e8
Document the shared object store fake #787 added
anth-volk Oct 14, 2026
3a07f82
Put the docstrings #795 added in the D style
anth-volk Oct 14, 2026
796902c
Put the docstrings #791 added in the D style
anth-volk Oct 14, 2026
68c2d54
Put the docstrings #758 added in the D style
anth-volk Oct 15, 2026
53d65e1
Put the docstrings #781 added in the D style
anth-volk Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/scripts/update_policyengine_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,6 @@ def verify_dispatch() -> None:
promotable is refused. ``dispatch_verified`` is true only when at least
one field was compared. Prints digests for the PR body.
"""

raw = os.environ.get("RELEASE_PAYLOAD", "").strip()
payload = json.loads(raw) if raw and raw != "null" else {}
installed = {}
Expand Down Expand Up @@ -379,6 +378,11 @@ def _parser() -> argparse.ArgumentParser:


def main() -> None:
"""Run one subcommand for update-policyengine-package.sh.

Results go to stdout for the shell script to capture; a failed check exits
non-zero through ``SystemExit`` with the reason as its message.
"""
args = _parser().parse_args()
if args.command == "current-version":
print(current_version(args.pyproject, args.package))
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ To add or change a rule, read `docs/engineering/skills/rules-registry.md`.
- `sim.quality.every-project-tested`: Every project and library with unit tests runs them in the PR test matrix. (doc-only, not checked: follow it yourself)
- `sim.quality.ruff-format-src`: Source code under projects/*/src and libs/*/src is formatted with ruff format. (blocking: ruff format)
- `sim.quality.ruff-format-tests`: Test code is formatted with ruff format too; CI currently checks only src directories. (doc-only, not checked: follow it yourself)
- `sim.quality.ruff-lint`: All Python code passes ruff check with every rule family selected in ruff.toml, with no ratchet; families are added one themed group at a time until the selection is ALL. (blocking: ruff check (ANN, E4, E7, E9, F, FLY, FURB, I, PIE, PYI, RET, RSE, TC, UP, W))
- `sim.quality.ruff-lint`: All Python code passes ruff check with every rule family selected in ruff.toml, with no ratchet; families are added one themed group at a time until the selection is ALL. (blocking: ruff check (ANN, D, E4, E7, E9, F, FLY, FURB, I, PIE, PYI, RET, RSE, TC, UP, W))
- `sim.quality.runtime-evaluated-models-resolve`: Every Pydantic model is runtime-evaluated for ruff: one of its bases, read through the imports of the model's own module, is listed in ruff.toml's runtime-evaluated-base-classes or is a runtime-evaluated class in that same module; and every Pydantic model resolves its annotations once its package is imported. (blocking: pytest `projects/policyengine-simulation-entry/tests/test_runtime_evaluated_annotations.py`, `projects/policyengine-simulation-executor/tests/test_runtime_evaluated_annotations.py`)
- `sim.quality.tests-pass`: Each project's unit tests pass in its own locked environment. (blocking: CI job `test`)
- `sim.quality.tests-ruff-exemptions`: Test files may use pytest's bare assert and need no docstrings, annotations or named constants for magic numbers (S101, D1, ANN, PLR2004); every other ruff rule applies to tests. (blocking: ruff per-file ignore of ANN, D1, PLR2004, S101 for `**/tests/**`)
Expand Down
1 change: 1 addition & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Made CLAUDE.md and the Copilot instructions thin pointers to AGENTS.md, required workflow logic longer than one line to live in tested and shellchecked scripts, and registered the tracking-issue rule
- Added a rules registry (rules/registry.toml) with a standard-library tool that validates it, checks every rule is enforced where it claims, and renders docs/generated/rules.md and the Rules block in AGENTS.md
changed:
- Enabled ruff's docstring rules (D) and documented every module, class and function outside tests; published contract models and routes now carry these descriptions in their JSON schema and OpenAPI, with no structural schema change
- Enabled ruff's annotation rules (ANN) and annotated every function outside tests; test files are exempt from ANN, missing docstrings, magic numbers and bare-assert rules
- Enabled ruff's typing-only import rules (TC) and moved annotation-only imports under TYPE_CHECKING, keeping runtime imports for Pydantic models and FastAPI endpoints
- Listed policyengine.py's Simulation, pydantic-settings' BaseSettings and the contract's SingleYearMacroOutput as runtime-evaluated bases for ruff, imported SingleYearMacroOutput from the contract in the executor's compat models instead of through a re-export, and added executor and entrypoint tests that fail when ruff, reading a model's bases through its module's imports, would not treat a Pydantic model as runtime-evaluated, or when a model's annotations do not resolve
Expand Down
8 changes: 4 additions & 4 deletions docs/generated/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ flowchart LR
| `sim.quality.every-project-tested` | Every project and library with unit tests runs them in the PR test matrix. | mechanical | doc-only | no | nothing | — | — |
| `sim.quality.ruff-format-src` | Source code under projects/*/src and libs/*/src is formatted with ruff format. | mechanical | enforced | yes | ruff format | `pr.yml:lint` | — |
| `sim.quality.ruff-format-tests` | Test code is formatted with ruff format too; CI currently checks only src directories. | mechanical | doc-only | no | nothing | — | — |
| `sim.quality.ruff-lint` | All Python code passes ruff check with every rule family selected in ruff.toml, with no ratchet; families are added one themed group at a time until the selection is ALL. | mechanical | enforced | yes | ruff check (ANN, E4, E7, E9, F, FLY, FURB, I, PIE, PYI, RET, RSE, TC, UP, W) | `pr.yml:lint` | — |
| `sim.quality.ruff-lint` | All Python code passes ruff check with every rule family selected in ruff.toml, with no ratchet; families are added one themed group at a time until the selection is ALL. | mechanical | enforced | yes | ruff check (ANN, D, E4, E7, E9, F, FLY, FURB, I, PIE, PYI, RET, RSE, TC, UP, W) | `pr.yml:lint` | — |
| `sim.quality.runtime-evaluated-models-resolve` | Every Pydantic model is runtime-evaluated for ruff: one of its bases, read through the imports of the model's own module, is listed in ruff.toml's runtime-evaluated-base-classes or is a runtime-evaluated class in that same module; and every Pydantic model resolves its annotations once its package is imported. | mechanical | enforced | yes | pytest `projects/policyengine-simulation-entry/tests/test_runtime_evaluated_annotations.py`, `projects/policyengine-simulation-executor/tests/test_runtime_evaluated_annotations.py` | `pr.yml:test` | yes |
| `sim.quality.tests-pass` | Each project's unit tests pass in its own locked environment. | mechanical | enforced | yes | CI job `test` | `pr.yml:test` | — |
| `sim.quality.tests-ruff-exemptions` | Test files may use pytest's bare assert and need no docstrings, annotations or named constants for magic numbers (S101, D1, ANN, PLR2004); every other ruff rule applies to tests. | mechanical | enforced | yes | ruff per-file ignore of ANN, D1, PLR2004, S101 for `**/tests/**` | `pr.yml:lint` | — |
Expand All @@ -263,7 +263,7 @@ flowchart LR
j_CI_pr_yml_lint["CI pr.yml:lint"]
r_sim_quality_ruff_format_tests("sim.quality.ruff-format-tests (doc-only)")
r_sim_quality_ruff_lint("sim.quality.ruff-lint (enforced)")
e_ruff_check__ANN__E4__E7__E9__F__FLY__FURB__I__PIE__PYI__RET__RSE__TC__UP__W_["ruff check (ANN, E4, E7, E9, F, FLY, FURB, I, PIE, PYI, RET, RSE, TC, UP, W)"]
e_ruff_check__ANN__D__E4__E7__E9__F__FLY__FURB__I__PIE__PYI__RET__RSE__TC__UP__W_["ruff check (ANN, D, E4, E7, E9, F, FLY, FURB, I, PIE, PYI, RET, RSE, TC, UP, W)"]
r_sim_quality_runtime_evaluated_models_resolve("sim.quality.runtime-evaluated-models-resolve (enforced)")
e_pytest_projects_policyengine_simulation_entry_tests_test_runtime_evaluated_annotations_py__projects_policyengine_simulation_executor_tests_test_runtime_evaluated_annotations_py["pytest projects/policyengine-simulation-entry/tests/test_runtime_evaluated_annotations.py, projects/policyengine-simulation-executor/tests/test_runtime_evaluated_annotations.py"]
j_CI_pr_yml_test["CI pr.yml:test"]
Expand All @@ -275,8 +275,8 @@ flowchart LR
r_sim_quality_ruff_format_src --> e_ruff_format
e_ruff_format --> j_CI_pr_yml_lint
r_sim_quality_ruff_format_tests -.-> gap_unenforced
r_sim_quality_ruff_lint --> e_ruff_check__ANN__E4__E7__E9__F__FLY__FURB__I__PIE__PYI__RET__RSE__TC__UP__W_
e_ruff_check__ANN__E4__E7__E9__F__FLY__FURB__I__PIE__PYI__RET__RSE__TC__UP__W_ --> j_CI_pr_yml_lint
r_sim_quality_ruff_lint --> e_ruff_check__ANN__D__E4__E7__E9__F__FLY__FURB__I__PIE__PYI__RET__RSE__TC__UP__W_
e_ruff_check__ANN__D__E4__E7__E9__F__FLY__FURB__I__PIE__PYI__RET__RSE__TC__UP__W_ --> j_CI_pr_yml_lint
r_sim_quality_runtime_evaluated_models_resolve --> e_pytest_projects_policyengine_simulation_entry_tests_test_runtime_evaluated_annotations_py__projects_policyengine_simulation_executor_tests_test_runtime_evaluated_annotations_py
e_pytest_projects_policyengine_simulation_entry_tests_test_runtime_evaluated_annotations_py__projects_policyengine_simulation_executor_tests_test_runtime_evaluated_annotations_py --> j_CI_pr_yml_test
r_sim_quality_tests_pass --> e_CI_job_test
Expand Down
2 changes: 2 additions & 0 deletions libs/policyengine-fastapi/fixtures/ping/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

@pytest.fixture
def health_registry() -> HealthRegistry:
"""Provide an empty registry; it reports healthy until a test registers a system."""
return HealthRegistry()


@pytest.fixture
def client(health_registry: HealthRegistry) -> TestClient:
"""Return a client for a bare app serving ping routes from ``health_registry``."""
api = FastAPI()
ping.include_all_routers(api, health_registry)
return TestClient(api)
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""
Re-usable elements that support FastAPI and SQLModel indipendent of this specific app.
"""
"""Reusable FastAPI and SQLModel building blocks that are independent of any one app."""

from .observability import (
ObservabilityConfig as ObservabilityConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Bearer-token authentication dependencies for FastAPI routes."""

from .jwt_decoder import JWTDecoder

__all__ = ["JWTDecoder"]
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""FastAPI dependency that verifies RS256 bearer JWTs against an issuer's JWKS."""

import logging

import jwt
Expand All @@ -11,12 +13,19 @@


class JWTDecoder:
"""
Parse and verify the JWT, verifying the correct issuer and audience and returning
the parsed token with all fields.
"""Callable FastAPI dependency that verifies a bearer JWT and returns its claims.

Tokens must be RS256-signed by a key from the issuer's JWKS and carry the
configured issuer and audience. A missing or invalid token raises HTTP 403,
or yields None when ``auto_error`` is False.
"""

def __init__(self, issuer: str, audience: str, auto_error: bool = True) -> None:
"""Store the expected claims and create a JWKS client for the issuer.

Keys are fetched from ``{issuer}.well-known/jwks.json``, so ``issuer``
must end with a slash.
"""
self.issuer = issuer
self.audience = audience
self.auto_error = auto_error
Expand All @@ -29,6 +38,11 @@ def __call__(
HTTPBearer(auto_error=False)
),
) -> dict[str, str] | None:
"""Return the verified token's claims.

A missing or invalid token raises HTTP 403, or returns None when
``auto_error`` is False.
"""
try:
return self._validate(token)
except HTTPException as err:
Expand Down
14 changes: 9 additions & 5 deletions libs/policyengine-fastapi/src/policyengine_fastapi/database.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""SQLModel engine and per-request session dependency helpers."""

from collections.abc import Callable, Generator
from typing import Any

Expand All @@ -13,8 +15,10 @@


def create_sqlite_engine(filename: str | None = None) -> Engine:
"""
For desktop and testing, use sqlite to back sqlmodel
"""Create a SQLite engine for desktop use and tests.

Without ``filename`` the database is in memory; StaticPool keeps that single
in-memory connection shared across threads.
"""
sqlite_url = f"sqlite:///{filename}" if filename else "sqlite:///:memory:"
connect_args = {"check_same_thread": False}
Expand All @@ -25,9 +29,9 @@ def create_sqlite_engine(filename: str | None = None) -> Engine:


def create_session_dep(engine: Engine) -> SessionGeneratorFactory:
"""
given an SQLModelEngine create a session dependency for use in
routers.
"""Return a FastAPI dependency that yields one SQLModel session per request.

The session is bound to ``engine`` and closed when the request finishes.
"""

def session_dep() -> Generator[Session]:
Expand Down
17 changes: 14 additions & 3 deletions libs/policyengine-fastapi/src/policyengine_fastapi/exit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
atexit does not work with uvicorn/fastapi for whatever reason and fastapi does not provide
a middleware-style way to register lifecycle events so I'm writing this
"""Process-wide registry of shutdown callbacks run at the end of the app lifespan.

atexit handlers do not run reliably under uvicorn/FastAPI, and FastAPI has no
middleware-style way to register lifecycle events, so callbacks are registered
here and run from the app's lifespan instead.
"""

import logging
Expand All @@ -14,10 +16,18 @@


class AppExit:
"""Collect callbacks to run once, in registration order, when the app shuts down."""

def __init__(self) -> None:
"""Start with no registered callbacks."""
self.callbacks: list[Callable[[], None]] = []

def __call__(self, *args: object, **kwargs: object) -> Callable[[T], T]:
"""Return a decorator that registers a function to run with these args at exit.

The decorated function is returned unchanged.
"""

def accept(func: T) -> T:
self.callbacks.append(lambda: func(*args, **kwargs))
return func
Expand All @@ -32,6 +42,7 @@ def _exit(self) -> None:

@asynccontextmanager
async def lifespan(self) -> AsyncIterator[None]:
"""Run every registered callback once the lifespan body exits without error."""
yield
self._exit()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
"""Health models and the registry behind the ``/ping/alive`` probe."""

from collections.abc import Callable

from pydantic import BaseModel


class ProbeStatus(BaseModel):
"""Result of one health probe, with an optional explanatory message."""

name: str
healthy: bool
message: str | None = None


class SystemStatus(BaseModel):
"""Health of one system; it is healthy only if every probe in ``detail`` is."""

name: str
healthy: bool
detail: list[ProbeStatus]


class HealthStatus(BaseModel):
"""Health of the service; it is healthy only if every system is."""

healthy: bool
systems: list[SystemStatus]

Expand All @@ -24,11 +32,15 @@ class HealthStatus(BaseModel):


class HealthSystemReporter:
"""Named group of health probes reported together as one system."""

def __init__(self, name: str, probes: dict[str, HealthProbe]) -> None:
"""Hold the system name and its probes, keyed by probe name."""
self.name = name
self.probes = probes

def report(self) -> SystemStatus:
"""Run every probe; the system is healthy only if all of its probes are."""
detail = [self.probes[key]() for key in self.probes.keys()]
first_unhealthy = next((d for d in detail if d.healthy is not True), None)
return SystemStatus(
Expand All @@ -37,21 +49,24 @@ def report(self) -> SystemStatus:


class HealthRegistry:
"""
Mechanism for collecting and reporting sevice health.
"""Registry of system reporters that together make up the service's health.

NOTE: any error will result in the service being restarted
so only report real errors.
"""

systems: dict[str, HealthSystemReporter]

def __init__(self) -> None:
"""Start with no systems, which reports as healthy."""
self.systems = {}

def register(self, reporter: HealthSystemReporter) -> None:
"""Add a system reporter, replacing any registered under the same name."""
self.systems[reporter.name] = reporter

def report(self) -> HealthStatus:
"""Report every system; the service is healthy only if all systems are."""
system_status = [self.systems[key].report() for key in self.systems.keys()]
first_unhealthy = next(
(s for s in system_status if s.healthy is not True), None
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""
Shared observability contracts and utilities for the PolicyEngine simulation API services.
"""Shared observability contracts and utilities for the PolicyEngine simulation API services.

This stays in `policyengine-fastapi` for now because only one active service
needs it and the main value today is shared contract stability, not a separate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Settings for the shared observability emitters."""

from __future__ import annotations

from dataclasses import dataclass, field
Expand All @@ -8,7 +10,6 @@

def parse_header_value_pairs(raw: str | None) -> dict[str, str]:
"""Parse OTLP headers from a comma or newline separated key=value string."""

if raw is None:
return {}

Expand All @@ -33,6 +34,12 @@ def parse_header_value_pairs(raw: str | None) -> dict[str, str]:

@dataclass(frozen=True)
class ObservabilityConfig:
"""Immutable observability settings.

Covers whether emission is on, where OTLP telemetry and tracer artifacts go,
and when tracer output is captured. The defaults describe disabled emission.
"""

enabled: bool = False
shadow_mode: bool = True
service_name: str = "policyengine-observability"
Expand All @@ -46,4 +53,5 @@ class ObservabilityConfig:

@classmethod
def disabled(cls, service_name: str = "policyengine-observability") -> Self:
"""Return a config with emission off, setting only the service name."""
return cls(enabled=False, service_name=service_name)
Loading
Loading