Phase 0 milestone 0.1: repo scaffold + Docker Compose

Scaffolding for the YPMS in-house build. Mirrors OWC's structure so
future OWC framework code can be vendored in with minimal fiddling
(milestone 0.2).

Includes:

- pyproject.toml — Python 3.12, FastAPI, SQLAlchemy 2.x, Alembic,
  Postgres, pydantic-settings, structlog, cryptography. Dev extras:
  pytest, ruff, mypy, bandit, pip-audit.
- Dockerfile — multi-stage builder + slim runtime.
- docker-compose.yml — app + Postgres (on 5433 to avoid clashing with
  OWC on 5432) + Adminer (on 8081).
- alembic.ini + migrations/env.py + baseline 0001_baseline.py (empty
  by design; every future change is additive and immutable once merged).
- src/ypms/main.py — FastAPI factory with /healthz and /readyz.
- src/ypms/settings.py — pydantic-settings config (DB URL, JWT,
  column encryption key, log level, env, CORS).
- src/ypms/logging.py — structlog + sanitize_for_log() for the
  sensitive-fields-never-in-logs rule from ARCHITECTURE.md §10.1.
- src/ypms/db.py — SQLAlchemy Base, engine, session factory, get_db.
- src/ypms/middleware.py — RequestIdMiddleware; X-Request-ID header
  echoed both ways.
- src/ypms/framework/ and src/ypms/security/ — empty packages, to be
  populated in milestones 0.2 and 0.3-0.4.
- tests/test_health.py — 3 smoke tests (healthz OK, header present,
  header honoured on incoming).
- README.md — quickstart.
- CONTRIBUTING.md — hard rules mirrored from OWC.
- .github/workflows/ci.yml — lint (ruff), types (mypy), tests + 80%
  coverage floor, SAST (bandit), SCA (pip-audit), gitleaks.

Green locally: ruff clean, mypy clean, pytest 3 passed.

Advances YPMS-POAM-040 (architecture doc + Phase 0 kickoff).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris TonjesandClaude Opus 4.7 committed 2026-09-30 16:15:45 -04:00
1 parent 8712e93258
commit 393631ad69
23 files changed
+851

No files matched your search

View File
Whitespace-only changes.
+35
View File
@@ -0,0 +1,35 @@
"""Shared pytest fixtures.
Uses SQLite for speed (see ARCHITECTURE.md §4.8). Any test that needs
Postgres-specific behaviour lives in a Postgres-only fixture path (added when a
migration or query needs it).
"""
from __future__ import annotations
import os
from collections.abc import Iterator
import pytest
from fastapi.testclient import TestClient
@pytest.fixture(scope="session", autouse=True)
def _test_env() -> Iterator[None]:
"""Set minimum env vars required for `Settings()` to construct in tests."""
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("JWT_SECRET", "test-secret")
os.environ.setdefault(
"COLUMN_ENCRYPTION_KEY", "test-col-enc-key-32bytes-______________"
)
os.environ.setdefault("ENV", "test")
yield
@pytest.fixture
def client() -> Iterator[TestClient]:
from ypms.main import create_app
app = create_app()
with TestClient(app) as c:
yield c
+30
View File
@@ -0,0 +1,30 @@
"""Milestone 0.1 smoke tests.
Proves:
- `/healthz` returns 200 without any dependency access.
- Every response carries an `X-Request-ID` header (RequestIdMiddleware wired).
- The header is echoed back when the caller supplies one.
"""
from __future__ import annotations
from fastapi.testclient import TestClient
def test_healthz_returns_ok(client: TestClient) -> None:
r = client.get("/healthz")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_response_carries_request_id(client: TestClient) -> None:
r = client.get("/healthz")
assert "x-request-id" in {k.lower() for k in r.headers}
req_id = r.headers["X-Request-ID"]
assert req_id
assert len(req_id) >= 8
def test_incoming_request_id_is_honoured(client: TestClient) -> None:
r = client.get("/healthz", headers={"X-Request-ID": "test-req-abcd"})
assert r.headers["X-Request-ID"] == "test-req-abcd"