# 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 <package> | 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 |
