-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
223 lines (204 loc) · 9.89 KB
/
Copy pathpyproject.toml
File metadata and controls
223 lines (204 loc) · 9.89 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
[project]
name = "aedifex"
version = "0.1.0"
description = "Evidence-grounded auditing platform for construction and infrastructure projects"
readme = "README.md"
# Supported: 3.12 (floor, tested in CI) and 3.13 (what the container runs).
# See docs/adr/0008-python-version-policy.md. Tooling below targets the floor.
requires-python = ">=3.12,<3.14"
license = { text = "Proprietary" }
classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Private :: Do Not Upload",
]
dependencies = [
"pydantic>=2.11,<3",
"pydantic-settings>=2.8,<3",
"structlog>=25.1,<26",
"pyyaml>=6.0.2,<7",
"sqlalchemy>=2.0.38,<3",
"alembic>=1.15,<2",
"psycopg[binary]>=3.2,<4",
"fastapi>=0.115,<1",
"uvicorn[standard]>=0.34,<1",
# Multipart parsing, required by FastAPI the moment a form or a file upload exists — and a
# document upload has to be multipart, because the alternative is base64 in JSON, which inflates
# every artifact by a third and puts the bytes through a text codec on the way to a hash.
# Declared rather than left as a FastAPI extra so that `import` and `pyproject` agree: without
# it the application does not start, which is a runtime dependency by any definition.
# Apache-2.0.
"python-multipart>=0.0.20,<0.1",
# Runtime as of the transport layer. It was previously dev-only, for the FastAPI test client.
# Chosen because httpcore exposes the one thing this design requires: connecting to an address
# while presenting a different name for TLS SNI and certificate verification, via the
# sni_hostname request extension. See docs/adr/0011-transport-boundary.md.
"httpx>=0.28,<1",
# Imported directly by the transport to build its SSL context, so it is declared rather than
# relied on as an httpx transitive. A dependency we import is a dependency we own: if httpx
# ever stops requiring certifi, this must not become an ImportError inside the TLS path.
"certifi>=2025.1.31",
# S3-compatible object storage. boto3 rather than the lighter minio SDK because AWS S3 is the
# production target and MinIO is the development stand-in, not the other way round — and because
# the checksum support this uses to have the *server* verify an upload is an S3 API feature that
# a MinIO-specific client would not exercise the same way.
"boto3>=1.36,<2",
# PDF text extraction. pypdf rather than PyMuPDF because PyMuPDF is AGPL and this project is
# proprietary; pypdf is BSD-3-Clause. pdfplumber would give per-character coordinates, which
# would make evidence spans richer, but it is not needed to locate a value to a page.
"pypdf>=6.1,<7",
# XLSX reading. Construction records -- BOQs, measurement sheets, running bills -- are
# spreadsheets far more often than they are prose, so a table reader is not optional for
# post-award documents. openpyxl is the maintained reader for the modern format and is
# MIT-licensed; xlrd handles only the legacy .xls and is explicitly not a substitute.
"openpyxl>=3.1,<4",
]
[project.optional-dependencies]
# OCR, and optional on purpose. Three of the corpus's most valuable primary documents are
# image-only — the Package V-A contract agreement with its Priced Bill of Quantities, and the
# ABP-III monthly IPC payment register — so this is required to read them and required by nothing
# else. A deployment that never touches a scan should not carry an ONNX runtime.
#
# rapidocr-onnxruntime is Apache-2.0 and pip-installable with no system binary, which is why it was
# chosen over Tesseract: Tesseract is equally well licensed but needs a system package on every
# machine, container and CI runner. Pillow (HPND) decodes the page images that pypdf hands over,
# including the CCITTFaxDecode fax scans.
ocr = [
# Pinned to the minor that every OCR measurement in this repository was taken on. The range was
# `>=1.2,<2`, and adding an unrelated dependency on 2026-08-21 caused a re-resolve that would
# have silently moved the engine to 1.4.4 — changing the transcription of every scanned page
# while the recorded benchmarks, the page-count limits and the SIGSEGV pixel budget all still
# described 1.2.3. An OCR upgrade is a decision with evidence behind it, not a side effect of
# locking. Exact rather than a range because 1.2.13 is equally untested; widen this
# deliberately, and re-measure when you do.
"rapidocr-onnxruntime==1.2.3",
"pillow>=11,<12",
]
dev = [
"pytest>=9.0.3,<10",
"pytest-cov>=6.0,<7",
"mypy>=1.15,<2",
"ruff>=0.9,<1",
"black>=26.3.1,<27",
"types-pyyaml>=6.0.12,<7",
# boto3 ships no type information, so mypy --strict would see every S3 call as Any and the
# storage layer would be the one place with no type checking at all.
"boto3-stubs[s3]>=1.36,<2",
# Same reasoning for openpyxl: the spreadsheet reader is where construction quantities and
# rates enter the system, and untyped cell access there is exactly where a wrong column would
# go unnoticed.
"types-openpyxl>=3.1,<4",
"pip-audit>=2.9,<3",
# Issues a throwaway CA and server certificates so the TLS path is tested with a real
# handshake. Mocking TLS would leave the security-critical half of the transport unverified.
"trustme>=1.2,<2",
# Turns a hung test into a failed test. The fetch layer is full of semaphores, deadlines, and
# sockets, and a bug that loses a slot blocks forever rather than failing — which in CI means
# a job that runs until the platform kills it and reports nothing useful. Twice during the
# rate limiter this cost ten minutes and produced no verdict.
"pytest-timeout>=2.3,<3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/aedifex"]
# ---------------------------------------------------------------------------
# Formatting / linting
# ---------------------------------------------------------------------------
[tool.black]
line-length = 100
target-version = ["py312"]
[tool.ruff]
line-length = 100
target-version = "py312"
src = ["src", "tests", "apps"]
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"A", # flake8-builtins
"C4", # comprehensions
"DTZ", # timezone-aware datetimes
"PTH", # prefer pathlib
"RET", # return simplification
"SIM", # simplification
"ARG", # unused arguments
"S", # bandit security checks
"T20", # no print in library code
"TRY", # exception anti-patterns
"RUF",
]
ignore = [
"TRY003", # long messages outside exception class: we prefer explicit messages
]
[tool.ruff.lint.per-file-ignores]
# Tests legitimately use asserts, hardcoded values, and long fixture literals.
# S104 fires on the string "0.0.0.0", which SSRF tests legitimately use as an address
# under test rather than as a bind address.
"tests/**" = ["S101", "S105", "S106", "S104", "ARG001", "ARG002"]
"scripts/**" = ["T20"]
# An operator command line prints; that is its entire interface. Logging goes to the log.
"apps/crawler/**" = ["T20"]
"migrations/**" = ["ARG001"]
[tool.ruff.lint.flake8-builtins]
builtins-allowed-modules = ["logging"]
# ---------------------------------------------------------------------------
# Typing
# ---------------------------------------------------------------------------
[tool.mypy]
python_version = "3.12"
files = ["src", "tests", "apps", "scripts"]
strict = true
warn_unreachable = true
warn_unused_ignores = true
disallow_any_explicit = false
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]]
# Alembic's generated env module is not typed and is not our code to fix.
module = ["migrations.*"]
ignore_errors = true
[[tool.mypy.overrides]]
# rapidocr-onnxruntime ships no py.typed marker. Imported in exactly one place, to bound ONNX
# Runtime's thread pool, and behind the OcrEngine protocol everywhere else.
module = ["rapidocr_onnxruntime.*"]
ignore_missing_imports = true
[[tool.mypy.overrides]]
# numpy and Pillow arrive with the optional `ocr` extra, and no install path this project documents
# includes it: CI and the Makefile both run `uv sync --locked --extra dev`. So `mypy`, whose `files`
# covers `src`, could not find them and failed on the two lazy imports in `extraction/ocr.py` —
# every push from 2026-08-21 to 2026-08-24, ten commits, red on a machine where they were absent and
# green on one where they were not. Local runs said "clean" the whole time.
#
# Ignored rather than installed, because the extra is genuinely optional and the tests already say
# so: `test_ocr.py` guards on `pytest.importorskip("PIL.Image", reason="Pillow ships with the ocr
# extra")`. An optional dependency must not be able to decide whether the type gate passes.
#
# Both are imported inside one function, five lines apart, behind the same OcrEngine boundary as
# rapidocr above. When the extra *is* installed mypy uses the real packages and checks them properly,
# so this only relaxes the case where they cannot be found at all.
module = ["numpy.*", "PIL.*"]
ignore_missing_imports = true
[tool.pydantic-mypy]
init_typed = true
warn_required_dynamic_aliases = true
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
[tool.pytest.ini_options]
testpaths = ["tests"]
# timeout is generous on purpose: it exists to catch a hang, not to police slow tests. The whole
# unit suite runs in about four seconds, so 60s per test only ever fires on something genuinely
# stuck (rule 81f — a gate that hangs reports nothing).
addopts = "-q --strict-markers --strict-config --timeout=60"
markers = [
"integration: requires live infrastructure (PostgreSQL, object storage)",
"e2e: full pipeline test across multiple subsystems",
]
[tool.coverage.run]
source = ["src/aedifex"]
branch = true