-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
173 lines (160 loc) · 6.49 KB
/
Copy pathpyproject.toml
File metadata and controls
173 lines (160 loc) · 6.49 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
[project]
name = "fraudlens"
version = "0.1.0"
description = "Fraud decisioning: scoring, economics-driven policy, and the monitoring needed to operate it"
requires-python = ">=3.10"
# Pinned to the versions the research findings were produced against. The
# published numbers are only meaningful alongside the code that computed them,
# and sklearn in particular has changed HistGradientBoosting defaults between
# minor versions.
dependencies = [
"numpy==2.2.6",
"pandas==2.3.3",
"scikit-learn==1.7.2",
"pyarrow>=17.0.0",
# Not a serving concern: `fraudlens.config` is the bottom layer and every consumer
# of the cost model needs the business constants, so the settings library belongs
# in the base dependency set rather than in the serving extra.
"pydantic>=2.9.0",
"pydantic-settings>=2.6.0",
]
[project.optional-dependencies]
# Split by deployment unit rather than lumped together: the scoring container
# has no reason to carry MLflow's dependency tree, and shipping it would widen
# the attack surface of the only service on the checkout path.
serving = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"pydantic>=2.9.0",
"pydantic-settings>=2.6.0",
"prometheus-client>=0.21.0",
"opentelemetry-api>=1.28.0",
"opentelemetry-sdk>=1.28.0",
"opentelemetry-instrumentation-fastapi>=0.49b0",
]
tracking = [
# Floor at 3.x: `fraudlens.models.tracking` uses a local SQLite backend because
# MLflow 3 put the `./mlruns` file store into maintenance mode and refuses it
# without an opt-out env var. On 2.x that code path is untested.
"mlflow>=3.15.1,<4",
]
streaming = [
"psycopg[binary]>=3.2.0",
"sqlalchemy>=2.0.0",
]
dev = [
"pytest>=8.3.0",
"pytest-cov>=6.0.0",
"hypothesis>=6.115.0",
"ruff>=0.7.0",
"mypy>=1.13.0",
"import-linter>=2.1",
"pre-commit>=4.0.0",
# Transport for `fastapi.testclient.TestClient`. The serving contract tests drive the
# app in-process; without this they would need a running uvicorn, which the testing
# standard in §9a rules out.
"httpx>=0.27.0",
# Reads the provisioned alert rules and datasource files in tests/deploy. Already
# present transitively via pre-commit and mlflow, which is exactly why it is declared
# here: the dashboard drift test would start failing the day an unrelated dependency
# dropped it, and the failure would point at the wrong thing.
"pyyaml>=6.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/fraudlens"]
[tool.ruff]
line-length = 100
target-version = "py310"
src = ["src", "tests"]
[tool.ruff.lint]
select = [
"E", "W", # pycodestyle
"F", # pyflakes
"I", # import sorting
"N", # pep8-naming
"UP", # pyupgrade
"B", # bugbear
"C4", # comprehensions
"SIM", # simplify
"PTH", # use pathlib
"C90", # mccabe complexity
"S", # bandit security
"DTZ", # naive datetimes — a decision ledger with ambiguous timestamps
# cannot be audited across a DST boundary
"RUF",
]
# Complexity budget. A branchy function in the economics or policy path is a
# function nobody can fully reason about, and this is the code that decides
# whether to take money from a customer.
[tool.ruff.lint.mccabe]
max-complexity = 10
# Research scripts are the provenance of the published findings (ADR-0001).
# They are preserved exactly as they ran, so style rules are waived: reformatting
# them would mean the committed code is no longer the code that produced the
# published numbers. Correctness rules (F: pyflakes) still apply — an actual bug
# in there would invalidate the findings and we want to know.
[tool.ruff.lint.per-file-ignores]
"research/*" = ["E", "W", "N", "UP", "B", "C4", "SIM", "PTH", "RUF", "I", "C90", "S", "DTZ"]
"tests/*" = ["S101"] # assert is the point of a test
[tool.mypy]
python_version = "3.10"
strict = true
files = ["src/fraudlens"]
# Numeric stack ships incomplete stubs; strictness here would produce noise
# without catching anything real.
[[tool.mypy.overrides]]
module = ["sklearn.*", "pandas.*", "pyarrow.*", "mlflow.*", "prometheus_client.*"]
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"golden: locks a published finding; failure means a number moved",
"integration: requires the compose stack",
"slow: takes over ten seconds",
]
addopts = "--strict-markers -q"
[tool.coverage.report]
# 85% on the library. Not applied to research/, which is preserved rather than
# maintained.
fail_under = 85
show_missing = true
[tool.importlinter]
root_package = "fraudlens"
# Dependencies flow one way. A cycle here would mean the economics module
# depends on serving, at which point the cost functions can no longer be tested
# or reasoned about in isolation — and they are the part that has to be exactly
# right.
[[tool.importlinter.contracts]]
name = "Layered architecture"
type = "layers"
# Parenthesised layers are optional, so the contract is enforceable from the
# first commit rather than only once every layer exists. Layers are added as
# the epics land; the constraint applies to whichever are present.
layers = [
"(fraudlens.serving)",
# The flywheel reads monitoring's drift signals to decide when to retrain and the
# registry to decide what to promote, so it sits above monitoring and below serving.
# The placement is the load-bearing part: a retraining decision must never be
# reachable from the request path, and putting it here makes that a build failure
# rather than a 3 a.m. discovery that a promotion ran inside a 150 ms budget.
"(fraudlens.flywheel)",
# Monitoring and lineage read the ledger and the model registry; nothing on
# the request path may read them. Placing them below serving and above
# streaming states that: a drift computation can never end up inside the
# 150 ms budget by accident, because the import would break the contract.
"(fraudlens.monitoring)",
"(fraudlens.lineage)",
# Streaming sits above policy on purpose. The ledger records an
# already-computed decision rather than computing one, so it must not
# import the policy layer — an audit trail that depends on the thing it
# audits cannot be tested, or written, independently of it.
"(fraudlens.streaming)",
"(fraudlens.policy)",
"(fraudlens.economics)",
"(fraudlens.models)",
"(fraudlens.features)",
"(fraudlens.config)",
]