# AGENTS.python.md — Python Agent Rules Applies to: `backend-compliance/`, `ai-compliance-sdk/` (Python path), `compliance-tts-service/`, `document-crawler/`, `dsms-gateway/` (Python services) --- ## NON-NEGOTIABLE: Pre-Push Checklist **BEFORE every `git push`, run ALL of the following from the service directory. A single failure blocks the push.** ```bash # 1. Fast lint (Ruff — catches syntax errors, unused imports, style violations) ruff check . # 2. Auto-fix safe issues, then re-check ruff check --fix . && ruff check . # 3. Type checking (mypy strict on new modules, standard on legacy) mypy . --ignore-missing-imports --no-error-summary # 4. Unit tests only (fast, no external deps) pytest tests/unit/ -x -q --no-header # 5. Verify the service starts (catches import errors, missing env vars with defaults) python -c "import app" 2>/dev/null || python -c "import main" 2>/dev/null || true ``` **One-liner pre-push gate (run from service root):** ```bash ruff check . && mypy . --ignore-missing-imports --no-error-summary && pytest tests/ -x -q --no-header ``` ### Why each check matters | Check | Catches | Time | |-------|---------|------| | `ruff check` | Syntax errors, unused imports, undefined names | <2s | | `mypy` | Type mismatches, wrong argument types | 5-15s | | `pytest -x` | Logic errors, regressions | 10-60s | | import check | Missing packages, circular imports | <1s | --- ## Code Style (Ruff) Config lives in `pyproject.toml`. Do **not** add per-file `# noqa` suppressions without a comment explaining why. ```toml [tool.ruff] line-length = 100 target-version = "py311" [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "TCH"] ignore = ["E501"] # line length handled by formatter [tool.ruff.lint.per-file-ignores] "tests/*" = ["S101"] # assert is fine in tests ``` **Blocked patterns:** - `from module import *` — always name imports explicitly - Bare `except:` — use `except Exception as e:` at minimum - `print()` in production code — use `logger` - Mutable default arguments: `def f(x=[])` → `def f(x=None)` --- ## Type Annotations All new functions **must** have complete type annotations. Use `from __future__ import annotations` for forward references. ```python # Required async def get_tenant(tenant_id: str, db: AsyncSession) -> TenantModel | None: ... # Required for complex types from typing import Sequence def list_risks(filters: dict[str, str]) -> Sequence[RiskModel]: ... ``` **Mypy rules:** - `--disallow-untyped-defs` on new files - `--strict` on new modules (not legacy) - Never use `type: ignore` without a comment --- ## FastAPI-Specific Rules ```python # Handlers stay thin — delegate to service layer @router.get("/risks/{risk_id}", response_model=RiskResponse) async def get_risk(risk_id: UUID, service: RiskService = Depends(get_risk_service)): return await service.get(risk_id) # ≤5 lines per handler # Always use response_model — never return raw dicts from endpoints # Always validate input with Pydantic — no manual dict parsing # Use HTTPException with specific status codes, never bare 500 ``` --- ## Testing Requirements ``` tests/ ├── unit/ # Pure logic tests, no DB/HTTP (run on every push) ├── integration/ # Requires running services (run in CI only) └── contracts/ # OpenAPI snapshot tests (run on API changes) ``` **Unit test requirements:** - Every new function → at least one happy-path test - Every bug fix → regression test that would have caught it - Mock all I/O: DB calls, HTTP calls, filesystem reads ```bash # Run unit tests only (fast, for pre-push) pytest tests/unit/ -x -q # Run with coverage (for CI) pytest tests/ --cov=. --cov-report=term-missing --cov-fail-under=70 ``` --- ## Dependency Management ```bash # Check new package license before adding pip show | grep -E "License|Home-page" # After adding to requirements.txt — verify no GPL/AGPL pip-licenses --fail-on="GPL;AGPL" 2>/dev/null || echo "Check licenses manually" ``` **Never add:** - GPL/AGPL licensed packages - Packages with known CVEs (`pip audit`) - Packages that only exist for dev (`pytest`, `ruff`) to production requirements --- ## Common Pitfalls That Break CI | Pitfall | Prevention | |---------|------------| | `const x = ...` inside dict literal (wrong language!) | Run ruff before push | | Pydantic v1 syntax in v2 project | Use `model_config`, not `class Config` | | Sync function called inside async without `run_in_executor` | mypy + async linter | | Missing `await` on coroutine | mypy catches this | | `datetime.utcnow()` (deprecated) | Use `datetime.now(timezone.utc)` | | Bare `except:` swallowing errors silently | ruff B001/E722 catches this | | Unused imports left in committed code | ruff F401 catches this |