-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
376 lines (354 loc) · 16.7 KB
/
Copy pathpyproject.toml
File metadata and controls
376 lines (354 loc) · 16.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
[project]
name = "iris-service"
version = "0.1.0"
description = "Iris customer service — Python mirror of iris-service Java (FastAPI + Pydantic v2 + SQLAlchemy 2.x async)"
readme = "README.md"
authors = [
{ name = "Benoît Besson", email = "benoit.besson@gmail.com" }
]
requires-python = ">=3.14"
license = { text = "MIT" }
keywords = ["fastapi", "demo", "observability", "kafka", "redis", "opentelemetry"]
# Runtime dependencies — pinned exactly to mirror Java project's
# `pin every upstream reference` rule (no floating versions).
dependencies = [
# Web framework — mirror of Spring Boot Web MVC
"fastapi==0.136.1", # bumped 2026-04-25 — pulls starlette>=0.46.0
# Explicit starlette pin > 0.49.1 to dodge CVE-2025-54121 + CVE-2025-62727
# (both tickets surfaced by pip-audit in CI ; fastapi's lower bound 0.46.0
# would otherwise leave us on a vulnerable starlette).
"starlette>=0.49.1",
"uvicorn[standard]==0.34.0",
"gunicorn==23.0.0",
# DTO + validation — mirror of Jackson + Bean Validation
"pydantic[email]>=2.11",
"pydantic-settings>=2.7",
# ORM + migrations — mirror of Spring Data JPA + Flyway
"sqlalchemy[asyncio]==2.0.36",
"asyncpg==0.31.0", # 0.31.0 ships cp314 wheels ; 0.30.0 didn't (forced source build → cc missing in CI slim image)
"alembic==1.14.0",
# Auth + JWT — mirror of Spring Security + jjwt
# Kafka — mirror of Spring Kafka
"aiokafka==0.13.0", # 0.13.0 ships cp314 wheels ; 0.12.0 didn't (forced source build → cc missing in CI slim image)
# Redis — mirror of Spring Data Redis
"redis[hiredis]==5.2.1",
# Observability — mirror of Micrometer + OpenTelemetry
"opentelemetry-api==1.29.0",
"opentelemetry-sdk==1.29.0",
"opentelemetry-exporter-otlp==1.29.0",
"opentelemetry-instrumentation-fastapi==0.50b0",
"opentelemetry-instrumentation-sqlalchemy==0.50b0",
"opentelemetry-instrumentation-aiokafka==0.50b0",
"opentelemetry-instrumentation-redis==0.50b0",
"prometheus-client==0.21.1",
"starlette-prometheus==0.10.0",
# Logging — structured JSON
"structlog==24.4.0",
# Rate limiting — mirror of bucket4j
"slowapi==0.1.9",
# Resilience patterns (retry, backoff) — mirror of resilience4j retry
"tenacity==9.1.4",
# HTTP client for outbound calls to JSONPlaceholder, Ollama, etc.
"httpx==0.28.1",
"apscheduler>=3.11.2",
"pyjwt>=2.12.1",
"bcrypt>=4.0",
# Model Context Protocol — official Anthropic SDK. Bundles FastMCP
# (Pythonic decorator API) + streamable-http transport that mounts as a
# Starlette ASGI sub-app onto our FastAPI server (see ADR-0062 for the
# decision to expose @tool per-method on the service layer). The [cli]
# extra adds `uv run mcp` for inspector / dev-mode workflows.
"mcp[cli]>=1.27.0",
# In-process TTL cache — mirrors Spring's Caffeine. Used by
# MCP get_metrics tool (5s TTL) so an LLM that re-asks the same metric
# while reasoning doesn't re-walk the prometheus REGISTRY twice.
"cachetools>=5.5",
# ONNX Runtime — runtime serving for the Customer Churn model
# (Phase C of shared ADR-0061). The training-side `torch` + `onnx`
# stack stays in the optional `ml` extra ; the runtime container
# ONLY needs `onnxruntime` (~30 MB) to serve predictions. Adding
# it here means the runtime container is fully self-contained
# (no opt-in install required for inference).
"onnxruntime>=1.21,<2",
# numpy — required by the inference path's feature vector shaping.
# `onnxruntime` already pulls a compatible version, but we declare
# it explicitly so a future runtime trim doesn't accidentally drop
# the only numpy import. Same pin range as the `ml` extra below.
"numpy>=1.26,<3",
"onnxscript[ml]>=0.7.0",
]
[project.optional-dependencies]
dev = [
# Test framework — mirror of JUnit 5 + Mockito
# Bumped 2026-04-25 to 9.0.3 — fixes CVE-2025-71176 (TOCTOU race in
# tmp_path fixture). pip-audit caught it.
"pytest==9.0.3",
"pytest-asyncio==1.3.0", # bumped 2026-04-25 — 0.25 capped pytest at <9 (blocked CVE fix)
"pytest-cov==6.0.0",
"pytest-mock==3.14.0",
"httpx==0.28.1",
"respx==0.22.0",
"freezegun==1.5.1",
"polyfactory==2.18.1",
# Property-based testing — generate adversarial inputs, find edge cases
# the example-based tests overlook. Per ADR-0007 §4 (selected paths only :
# Customer DTOs, JWT round-trip, RecentBuffer LIFO invariant).
"hypothesis>=6.122",
# Lint + type — mirror of Checkstyle + SpotBugs + PMD
"ruff>=0.13",
"mypy>=1.18",
# Architecture tests — mirror of ArchUnit
"import-linter==2.0",
# CVE scanning — mirror of OWASP Dependency-Check (Java) + npm-audit (UI).
# Per ADR-0007 §9. Wired in CI via .gitlab-ci/quality.yml :: pip-audit.
"pip-audit>=2.7.3",
# Microbenchmarks — mirror of JMH (Java). Per ADR-0007 §13. Hot paths :
# JWT encode/decode, bcrypt hash/verify, repository search. JSON output
# via --benchmark-json for CI delta tracking. Run via :
# pytest tests/benchmarks --benchmark-only
"pytest-benchmark>=5.1.0",
# Mutation testing — adds N variants of each function (e.g. swap < for >,
# invert booleans, replace literals with 0/1) and confirms tests catch
# them. Surviving mutants = tests have a hole. Heavy : 30-60 min on
# full suite, so we only target security-critical modules
# (auth/jwt + auth/passwords) via .mutmut.toml whitelist.
"mutmut>=3.5.0",
# Container tests — mirror of Testcontainers
"testcontainers[postgres,redis,kafka]==4.9.0",
]
ml = [
# ML training stack — opt-in extra. Not installed in the runtime
# serving container ; only for the training pipeline + tests/ml/.
# Per shared ADR-0061 §"Phase A scope".
#
# PyTorch chosen over scikit-learn even for an 8-feature MLP because
# the project is portfolio-themed and PyTorch is the modern lingua
# franca (2026-04-27 user decision : "PyTorch me paraît plus standard").
"torch>=2.5",
# ONNX format + runtime — cross-language inference per shared ADR-0060.
# `onnx` for the export side ; `onnxruntime` for the round-trip
# validation tests + Python serving (Phase C).
"onnx>=1.17",
"onnxruntime>=1.21,<2",
# MLflow for tracking + registry — observability-first per shared
# ADR-0062. The training script writes runs to file:// by default
# ; setting MLFLOW_TRACKING_URI=http://localhost:5000 redirects to
# the standalone tracking server.
"mlflow>=2.16",
# scikit-learn — used for evaluation metrics (roc_auc_score, etc.)
# + StandardScaler in the feature pipeline. NOT for training (that's
# PyTorch).
"scikit-learn>=1.5",
# Numerical stack — pinned to recent stable.
"numpy>=1.26",
"pandas>=2.2",
# Synthetic data for v1 training set per shared ADR-0061 §"Training
# data — synthetic for v1". Random seed 42 (deterministic).
"Faker>=30",
]
[project.scripts]
iris-service = "iris_service.app:run"
[build-system]
requires = ["uv_build>=0.11.2,<0.12.0"]
build-backend = "uv_build"
# ── Tooling configuration ─────────────────────────────────────────────────────
[tool.ruff]
line-length = 120
target-version = "py314"
# Exclude generated / scratch dirs (mutmut creates ./mutants with mutated
# source copies that would trigger thousands of false positives).
extend-exclude = ["mutants", ".mutmut-cache"]
[tool.ruff.lint]
select = [
"E", "W", # pycodestyle errors + warnings
"F", # pyflakes (unused imports, etc.)
"I", # isort (import order)
"B", # flake8-bugbear (likely bugs)
"C90", # mccabe complexity (= cyclomatic)
"N", # pep8-naming
"UP", # pyupgrade (modern Python idioms)
"S", # flake8-bandit (security)
"RUF", # ruff-specific rules
]
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.lint.per-file-ignores]
# Tests : `assert` is the standard pytest assertion mechanism. S101 is the
# bandit warning about asserts being stripped in `python -O` mode — does not
# apply to tests (we never run tests with -O). Same logic for hardcoded test
# data (S106 hardcoded password, S107 hardcoded password default).
"tests/**/*.py" = ["S101", "S105", "S106", "S107"]
[tool.ruff.lint.isort]
# Pin the project's first-party module so import grouping is stable.
# `iris_service` is the only namespace ; `bin/` (shell scripts) is no
# longer a Python module after the 2026-04-27 ML phase A move that
# relocated `bin/ml/` → `src/iris_service/ml/` for tooling-friendliness.
known-first-party = ["iris_service"]
[tool.mypy]
python_version = "3.14"
strict = true
warn_unused_ignores = true
warn_return_any = true
disallow_untyped_defs = true
plugins = ["pydantic.mypy"]
# aiokafka + testcontainers : no py.typed markers yet (tracked upstream).
# Skip stub checking for these libraries — we still type-check our own
# code that uses them, but without import-time errors.
[[tool.mypy.overrides]]
module = ["aiokafka", "aiokafka.*", "testcontainers.*", "apscheduler.*"]
ignore_missing_imports = true
# ML training stack — heavy deps in the [ml] extra. Stubs are partial /
# inconsistent (torch has py.typed but its dynamic API is hard for mypy ;
# scikit-learn has third-party stubs ; mlflow has no stubs). Treat all as
# untyped — strict mode still applies to OUR code (training script
# annotations check), just not to the library contracts.
[[tool.mypy.overrides]]
module = [
"torch", "torch.*",
"numpy", "numpy.*",
"pandas", "pandas.*",
"sklearn", "sklearn.*",
"mlflow", "mlflow.*",
"onnx", "onnx.*",
"onnxruntime", "onnxruntime.*",
"faker", "faker.*",
]
ignore_missing_imports = true
# iris_service.ml.* — the ML training package mixes pandas dynamic
# DataFrame access (`df["col"]` returns object), torch's nn.Module
# subclass-with-Any, and dict-typed config from pyproject. Strict mypy
# flags every interaction (assignment, __sub__, range()) as "object
# doesn't match X". The code is annotated where it matters ; suppressing
# the dict-access friction class lets us keep strict on the rest of the
# codebase. Re-tighten when pandas-stubs ships first-class support
# (tracked in pandas-stubs#XXXX).
[[tool.mypy.overrides]]
module = ["iris_service.ml.*"]
disable_error_code = [
"misc", # nn.Module subclass-with-Any
"assignment", # pandas object → int re-assignment
"operator", # datetime - object overload mismatch
"call-overload", # range(object) variant matching
"unused-ignore", # leftover ignores after override resolves them
"no-any-return", # tomllib dict access typed as Any
"type-arg", # DataLoader[T] generic args (PyTorch typing is partial)
"union-attr", # mlflow.active_run() Optional[ActiveRun].info — checked at runtime
]
# mcp (Anthropic MCP SDK) ships py.typed but its FastMCP.tool() decorator
# is typed as ``Callable[[AnyFunction], AnyFunction]`` (= ``Any`` propagation).
# Strict mypy flags every decorated wrapper inside :func:`register_tools` as
# ``untyped-decorator`` ; suppressing that one error code per file lets us keep
# strict mode for ALL real business logic in mcp/ (the wrappers are pure
# dispatch — type errors surface in the WRAPPED functions, which stay typed).
[[tool.mypy.overrides]]
module = ["iris_service.mcp.tools"]
disable_error_code = ["untyped-decorator"]
[tool.pytest.ini_options]
# cov-fail-under=90 — ADR-0007 §3 industrial Python posture :
# - 65 → 80 (Étape 9) : greenlet+thread hooks + 16 new tests.
# - 80 → 90 (ADR-0007) : property-based tests via hypothesis +
# targeted coverage uplift on passwords malformed, redis_client lifecycle,
# cleanup scheduler, customer_event publish error paths, customer/router
# PUT/PATCH/DELETE 4xx branches, enrichment_router factories,
# kafka_client _header + 503 path + stop_kafka idempotence.
# `-m "not integration"` is the DEFAULT — keeps `pytest` fast (no Docker).
# Run integration suite explicitly via : pytest -m integration
# Default `pytest` excludes : integration (need testcontainers) + benchmarks
# (need pytest-benchmark + slow). Run via the dedicated CI jobs :
# pytest tests/integration -m integration
# pytest tests/benchmarks --benchmark-only -m benchmarks
addopts = "-ra --strict-markers -m 'not integration and not benchmarks' --cov=iris_service --cov-report=term-missing --cov-report=html --cov-report=xml --cov-fail-under=90"
testpaths = ["tests"]
asyncio_mode = "auto"
markers = [
"integration: integration tests requiring external services",
"benchmarks: pytest-benchmark microbenchmarks of hot paths",
"slow: tests that take > 1s",
]
[tool.coverage.run]
source = ["src"]
omit = [
"*/migrations/*",
"*/conftest.py",
# iris_service/ml/* — split per file :
#
# The TRAINING side (model.py, seed_demo_data.py, train_churn.py,
# feature_engineering.py) requires the opt-in [ml] extra (torch +
# mlflow + sklearn ~ 500 MB) ; the default unit-tests CI job does
# NOT install it, so coverage on those files would always be 0 %.
# They keep the omit ; tests run separately when --extra ml is set.
#
# The RUNTIME side (inference.py, router.py, dtos.py, risk_band.py,
# predictor_singleton.py) shipped in Phase C is now part of the
# default deps (onnxruntime is ~30 MB and lives in the runtime
# container). These files are tested by tests/ml/test_inference*,
# test_risk_band*, test_router_churn* WITHOUT the [ml] extra, so
# they DO contribute to the global coverage gate.
"*/iris_service/ml/feature_engineering.py",
"*/iris_service/ml/model.py",
"*/iris_service/ml/seed_demo_data.py",
"*/iris_service/ml/train_churn.py",
]
# greenlet : SQLAlchemy async uses greenlets internally to bridge async ↔
# sync. Without this hook coverage misses every line invoked via
# session.execute / session.flush — which makes router bodies (heavy
# session usage) appear uncovered even though tests pass.
concurrency = ["greenlet", "thread"]
# Mutation testing — opt-in tooling for security-critical paths.
# mutmut adds N variants of each function (swap < for >, invert booleans,
# replace literals) and confirms tests catch them. Surviving mutants =
# tests have a hole.
#
# Status (2026-04-25) : installed but NOT in CI yet. mutmut 3.x has a
# known issue copying its src tree (walks parent filesystem, hits
# unreadable files like /.VolumeIcon.icns on macOS). Tracking upstream :
# https://github.com/boxed/mutmut/issues
# Once the upstream fix lands, wire a manual GitLab CI job that runs
# `uv run mutmut run` against auth/jwt + auth/passwords (target : crypto
# only ; full-suite mutmut is 30-60 min, targeted is 2-5 min).
#
# Manual run today : copy auth/jwt.py + auth/passwords.py to a tmp dir,
# edit `paths_to_mutate` here, then `uv run mutmut run`. Document any
# surviving mutant as a test-gap ticket in TASKS.md.
[tool.mutmut]
# List form required by mutmut 3.x — the legacy string form was silently
# ignored when read from pyproject.toml [tool.mutmut], so the loader fell
# back to `guess_paths_to_mutate()` which walked the project root +
# followed symlinks into /sys/devices/platform/psci/of_node
# (FileNotFoundError on first scheduled mutmut-auth-monthly run 2026-05-14).
paths_to_mutate = ["src/iris_service/auth"]
tests_dir = "tests/unit/auth"
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
[dependency-groups]
dev = [
"aiosqlite==0.20.0",
"asgi-lifespan>=2.1.0",
"fakeredis>=2.35.1",
"mkdocs>=1.6.1",
"mkdocs-material>=9.7.6",
"mkdocstrings[python]>=1.0.4",
"ruff>=0.8.4",
]
# ── Customer Churn Prediction tunable knobs (shared ADR-0061) ─────────────────
# These three windows define the SQL label used to train the churn predictor.
# Tunable here so we can iterate without code change ; bin/ml/train_churn.py
# reads them via tomllib at runtime.
[tool.churn]
churn_window_days = 90 # last_order older than this → churned
min_active_period_days = 30 # gap between first + last order ; excludes one-shots
min_account_age_days = 120 # account younger than this → label not yet meaningful
[tool.churn.training]
random_seed = 42 # global PyTorch + numpy + Faker seed (determinism)
val_split = 0.2 # holdout fraction for early-stopping
batch_size = 64
max_epochs = 100
patience = 10 # early-stop patience on validation AUC
learning_rate = 0.001
weight_decay = 0.00001
auc_gate = 0.60 # acceptance gate ; CI fails the run if holdout AUC < this