-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
214 lines (203 loc) · 15.3 KB
/
Copy pathpyproject.toml
File metadata and controls
214 lines (203 loc) · 15.3 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
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# Single source of truth for the version: hatchling reads __version__ from the package module, so the
# literal lives in exactly one place (no pyproject-vs-__init__ drift). Bump it in messagefoundry/__init__.py.
[tool.hatch.version]
path = "messagefoundry/__init__.py"
# sdist file selection — an ALLOWLIST, not hatchling's default whole-repo VCS sweep. WITHOUT this,
# `python -m build` packs every git-tracked file (docs/, tests/, scripts/, CLAUDE.md, .claude/ …) into the
# sdist, and release.yml uploads that sdist to PUBLIC PyPI — which shipped the private security-posture
# docs (docs/security/*, docs/reviews/*, Secure_Development_Standards.md — the publish-denylist set) on
# every release 0.1.0..0.2.15. The mirror's leak gate (publish.ps1 + scan_forbidden.py) governs only the
# git mirror, NOT the PyPI path, so the sdist is pinned here to the package + its metadata. (The wheel was
# already package-only.) pyproject.toml + PKG-INFO are always added by hatchling; LICENSE/NOTICE also ship
# via [project].license-files. release.yml has a belt-and-suspenders "sdist is package-only" gate.
[tool.hatch.build.targets.sdist]
only-include = ["messagefoundry", "README.md", "CHANGELOG.md", "LICENSE", "NOTICE"]
[project]
name = "messagefoundry"
dynamic = ["version"] # single-sourced from messagefoundry/__init__.py (see [tool.hatch.version])
description = "Open-source healthcare integration engine — route, transform, and validate messages across many formats and connection types"
readme = "README.md"
requires-python = ">=3.14"
license = "AGPL-3.0-or-later"
license-files = ["LICENSE", "NOTICE"] # ship the license + copyright/attribution NOTICE in sdist+wheel (PEP 639)
classifiers = [
"Development Status :: 4 - Beta", # Early Access
"Intended Audience :: Healthcare Industry",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Topic :: Communications",
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: System :: Networking",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.14",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Environment :: Console",
"Typing :: Typed",
]
dependencies = [
"hl7apy>=1.3", # version-aware validation + profiles
"hl7>=0.4.5", # python-hl7: fast, tolerant parsing for routing
"pydantic>=2.6", # channel config models / validation
"aiosqlite>=0.20", # async SQLite for the message store/queue
"fastapi>=0.137.1", # localhost engine API
"starlette>=1.3.1", # explicit floor — fastapi's own pin allows an older Starlette; >=1.3.1 fixes the Host/path + form-DoS CVEs (CVE-2026-48710 @1.0.1, CVE-2026-54282 @1.3.0, CVE-2026-54283 @1.3.1)
"uvicorn[standard]>=0.29",
"argon2-cffi>=23.1", # argon2id password hashing (OWASP-recommended)
"cryptography>=48.0.1", # AES-256-GCM PHI-at-rest encryption for the store (>=48.0.1: GHSA-537c-gmf6-5ccf)
"ldap3>=2.9", # Active Directory / LDAP authentication (pure-Python)
"pyspnego>=0.10", # SPNEGO/Kerberos negotiate for Windows SSO
"tomlkit>=0.12", # comment/format-preserving TOML writer for connections.toml (ADR 0007)
"tzdata>=2024.1", # IANA tz database for stdlib zoneinfo (Windows has no system tzdata)
"prometheus-client>=0.20", # Prometheus /metrics exposition (BACKLOG #21)
"defusedxml>=0.7.1", # hardened XML parse for RawMessage.xml() — XXE/DTD/entity-expansion safe (ADR 0004, BACKLOG #31)
"psutil>=6.0", # host CPU/mem gauges on the /metrics surface (BACKLOG #74)
]
# Shown on the PyPI sidebar; point at the PUBLIC repo/site (the wshallwshall repo is private).
[project.urls]
Homepage = "https://messagefoundry.org/"
Documentation = "https://messagefoundry.org/"
Repository = "https://github.com/MEFORORG/MessageFoundry"
Issues = "https://github.com/MEFORORG/MessageFoundry/issues"
[project.optional-dependencies]
# The standalone PySide6 test HARNESS (harness/ — synthetic send/receive/load/failover GUI). The
# desktop admin console was retired in favour of the web console (BACKLOG #103, ADR 0032 retired); the
# harness kept the reusable Qt view widgets (rehomed to harness/_console_widgets.py + _login.py) and is
# now the sole consumer of PySide6. keyring was dropped with the console (OS-token cache was
# launcher-only). httpx/truststore back the shared apiclient the harness monitor uses.
harness = [
"PySide6>=6.6", # the harness GUI (LGPL — OSS-distributable)
"httpx>=0.27", # the harness monitor's HTTP client to the engine API (via messagefoundry.apiclient)
"truststore>=0.10", # verify the engine's TLS cert against the OS trust store (enterprise/AD-CS roots) so a remote harness monitor needs no per-PC CA wrangling
]
# EXPERIMENTAL SQL Server store backend. Also needs the Microsoft ODBC Driver 18 for SQL Server
# installed at the OS level (not pip-installable). Lazy-imported, so SQLite-only installs skip it.
# aioodbc pulls pyodbc transitively; the lock resolves pyodbc 5.3.0 — the newest release and the
# first to support Python 3.14 (nothing older ships py3.14 wheels). 5.3.0 has an UPSTREAM py3.14
# parameter-binding segfault against SQL Server 2025 (mkleehammer/pyodbc#1459, still open), which the
# CI throughput-invariant step works around with scripts/ci/retry-native-crash.sh. When #1459 ships a
# fix, raise the floor to that pyodbc release here and drop that CI retry wrapper.
sqlserver = ["aioodbc>=0.5"]
# PostgreSQL store backend (production server-DB with single-node parity). No OS-level dependency
# (ships compiled C/Cython wheels). Lazy-imported, so SQLite-only installs skip it.
postgres = ["asyncpg>=0.29"]
# SFTP (SSH) transport for the REMOTEFILE connector. FTP/FTPS use the stdlib (ftplib), no dep.
# Lazy-imported, so installs that never use SFTP skip paramiko.
# Floor >=5.0: 5.0 removed SHA-1 entirely, excluding CVE-2026-44405 (rsakey.py SHA-1 permitted); >=3.4
# already excluded Terrapin (CVE-2023-48795). The lock resolves 5.0.0 — the floor makes the spec match.
sftp = ["paramiko>=5.0"]
# Optional OpenTelemetry metrics export (BACKLOG #21). Off by default; the /metrics Prometheus path
# needs none of this. otel imports are function-local, so SQLite-only installs skip these entirely.
otel = ["opentelemetry-sdk>=1.20", "opentelemetry-exporter-otlp>=1.20"]
# FHIR (R5/R4B/STU3) typed-model + FHIRPath codec for parsing/fhir/ (ADR 0022, BACKLOG #20). Lazy-imported,
# so SQLite-only installs skip it. fhir.resources>=7.1.0 is the pydantic-v2 floor (it dropped plain-R4 in
# 7.0.0 and pydantic-v1 in 7.1.0); it drags fhir-core (BSD-3, the pydantic-v2 base model) + pydantic-core
# (compiled). fhirpathpy is pure-Python (antlr4 runtime + python-dateutil). FHIR-XML rides fhir.resources'
# optional lxml extra and is deferred (JSON-only MVP) — never bare-parsed (ADR 0022 §6, Options #5).
fhir = ["fhir.resources>=7.1.0", "fhirpathpy>=2.2.0"]
# DICOM (DIMSE C-STORE SCP/SCU) connectors + codec for parsing/dicom/ (ADR 0025, BACKLOG #24). Lazy-imported,
# so SQLite-only installs skip it. pynetdicom (the DIMSE upper-layer) drags pydicom (the DICOM dataset/SR codec);
# both pure-Python — HEADERS/SR ONLY, so NO numpy (numpy is pydicom's optional pixel-data dep, never used here).
# Floors per dep-vet (2026-06-20): pydicom>=3.0.2 excludes CVE-2026-32711 (2.4.0–2.4.4 FileSet/DICOMDIR path
# traversal); pynetdicom>=3.0.4 requires pydicom>=3,<4 (the 3.x lines pair cleanly). dicomweb-client (DICOMweb
# STOW-RS, Phase-2) is a SEPARATE extra — it drags numpy+pillow+requests, so it is added in the Phase-2 PR, never
# folded into [dicom].
dicom = ["pynetdicom>=3.0.4,<4", "pydicom>=3.0.2,<4"]
# Opt-in STRICT X12 validation for parsing/x12/validate.py (ADR 0012, BACKLOG #32). Lazy-imported, so
# the tolerant X12Peek/X12Message hot path (the default) never needs it. pyx12 is the reference
# implementation-guide validator (e.g. 005010X222A1 for 837P) and also emits the 997/999 ack as a
# by-product. Its SOLE runtime dependency is defusedxml (already in tree, used to parse pyx12's bundled
# TRUSTED map XML — not attacker input). >=4.0.0 is the first py3.12+/modern-typing release.
x12 = ["pyx12>=4.0.0"]
# Optional XML/SOAP message accessor + schema validation + signature for parsing/xml/ (BACKLOG #31).
# Lazy-imported, so installs that never touch XML skip it. lxml is the hardened XPath/serialize engine
# (parser locked down directly — defusedxml does NOT cover lxml and defusedxml.lxml is deprecated);
# xmlschema validates against XSD with remote schemaLocation fetch disabled; signxml does XML-DSig
# (it drags cryptography — registered in scripts/security/crypto_inventory_check.py). Floors are the
# current major lines (lxml 6.x dropped py<3.8 + bundled modern libxml2; xmlschema 4.x; signxml 4.x).
xml = ["lxml>=6.0.0", "xmlschema>=4.0.0", "signxml>=4.0.0"]
# WebAuthn/FIDO2 browser second factor for the ops console (ADR 0068, WP-14b; BACKLOG #11). PyPI name
# is EXACTLY "webauthn" (duo-labs/py_webauthn, BSD-3-Clause) — "py_webauthn"/"py-webauthn" pip-normalize
# to AS207960's UNRELATED package; never "fix" this name to match the GitHub repo. Lazy-imported
# (auth/webauthn.py), so installs without browser passkeys skip it. Floor >=3.0.0: its cbor2>=6.1.2
# floor guarantees the fix for the HIGH cbor2 DoS advisory that explicitly cites WebAuthn flows
# (CVE-2026-26209, fixed in cbor2 5.9.0); <4 because the 2.x/3.x cbor2 ranges are disjoint and a bare
# >= spec silently jumps majors on re-lock. Deliberate fallback line if 3.x regresses: 2.8.0
# (cbor2>=5.6.5,<6 — a re-lock, never a bare >=2.8 spec). Net-new transitives (dep-vet 2026-07-03):
# pyOpenSSL>=26.3.0 (maintenance-mode upstream; hard-caps cryptography<50 — kept OUT of core so
# repo-wide cryptography upgrades stay uncoupled), cbor2>=6.1.2 (parses untrusted browser CBOR — its
# advisory stream is WebAuthn-relevant supply chain; never let this floor drift down), and
# pyasn1-modules>=0.4.2. pyasn1 was ALREADY in-tree via ldap3 (pin unchanged at 0.6.3 — exactly the
# CVE-2026-30922 decoder-DoS fix version; treat 0.6.3 as a floor, never let a re-lock regress it).
# OSV sweep 2026-07-03: all five pins CLEAR (webauthn/pyopenssl/pyasn1/pyasn1-modules/cbor2).
webauthn = ["webauthn>=3.0.0,<4"]
# HashiCorp Vault KeyProvider (ADR 0019 §3, BACKLOG #196): envelope-decrypt the store DEK via Vault
# Transit. `hvac` is the OFFICIAL HashiCorp Vault Python client (Apache-2.0), named in ADR 0019 §3's
# provider table. Lazy-imported (store/keyprovider_vault.py), so installs that never select
# `[store].key_provider=vault` skip it and the base install still pulls ZERO Vault SDK. Floor >=2.3.0:
# the current 2.x line (Python 3.8+, requests-based); a bare `>=` would silently jump a major on re-lock,
# so keep the floor pinned to the 2.x series. Net-new transitives (dep-vet 2026-07-10): requests + urllib3
# (both ubiquitous, mature; the lock resolves urllib3>=2.7.0 — CVE-2025-50181/50182 SSRF-redirect fixes)
# plus charset-normalizer/idna/certifi. hvac ships NO type stubs — mypy-strict containment lives inside
# store/keyprovider_vault.py (a targeted typed local), never a repo-wide ignore.
vault = ["hvac>=2.3.0"]
# The browser ops console ([api].serve_ui, ADR 0065) is a separately-versioned second wheel
# (messagefoundry-webconsole, in packaging/messagefoundry-webconsole/) mounted same-origin in-process via
# mount_ui. It is deliberately NOT declared as a [webconsole] extra yet: the wheel isn't published to an
# index, so a `messagefoundry-webconsole` dep would break `uv lock`. Until the release phase publishes it
# (adding the extra + a PEP 508 range), install it editable — CI uses `-e packaging/messagefoundry-webconsole`.
# serve_ui=true without the package installed fails LOUD at startup (__main__ find_spec guard).
dev = [
"pytest>=9.0.3", # >=9.0.3 fixes CVE-2025-71176 (insecure /tmp dir perms, CWE-379; dev/UNIX-only)
"pytest-asyncio>=0.26", # >=0.26 adds asyncio_default_test_loop_scope (BACKLOG #17 shared-loop fix)
"pytest-timeout>=2.3", # per-test watchdog: a hung test dumps stacks + fails fast in 60s, never wedges the leg
"pytest-rerunfailures>=16.0", # in-run auto-retry for the known harness-monitor timing flake: a single flake occurrence self-heals instead of reding the whole matrix + blocking auto-merge
"ruff>=0.4",
"mypy>=1.10",
"httpx>=0.27", # ASGI test client for the API + harness/load polling (the harness GUI ships its own copy via [harness])
]
[project.scripts]
messagefoundry = "messagefoundry.__main__:main"
# The offline common-password screening corpus (messagefoundry/auth/data/, loaded via
# importlib.resources) ships in the wheel automatically: hatchling packages every file under the
# messagefoundry/ package, data included. (A previous force-include of that dir double-added the files
# and broke `build --wheel`.) tests/test_password_*.py load the corpus via importlib.resources, so a
# build-config change that dropped it would fail the suite — that's the guard, not a force-include.
[tool.pytest.ini_options]
asyncio_mode = "auto"
# Run the whole suite on ONE shared asyncio event loop (tests AND fixtures). pytest-asyncio's default
# fresh-loop-per-test puts a function-scoped async fixture's aiosqlite connection on one loop while the
# test (or its teardown) runs on another — a cross-loop split where a completed query's result is
# delivered to a loop nobody is awaiting. A single session-scoped loop removes that churn entirely and
# also matches production topology (the engine runs on one long-lived asyncio.run() loop). BOTH keys
# are required and must match: setting only the *test* scope leaves fixtures function-scoped (still
# cross-loop). REQUIRES pytest-asyncio>=0.26 (the release that added asyncio_default_test_loop_scope;
# the fixture key needs >=0.24); the dev floor below is >=0.26 to match, and the lock pins 1.4.0.
asyncio_default_test_loop_scope = "session"
asyncio_default_fixture_loop_scope = "session"
testpaths = ["tests"]
# Per-test watchdog: a single hung await/socket is killed at 60s with a full thread-stack dump naming
# the culprit, instead of burning the CI job's wall-clock. `thread` method works cross-platform (no
# SIGALRM) and fires even when the main thread is blocked in a C call. Individual tests run well under
# 60s (the whole suite is ~2–5 min).
addopts = "--timeout=60 --timeout-method=thread"
markers = [
"win2025_acceptance: Windows Server 2025 on-server acceptance probes (env-gated; skip off-server).",
]
[tool.ruff]
line-length = 100
# The project requires 3.14 (see requires-python + mypy below), but ruff's formatter is held to py313
# idioms deliberately: a py314 target makes it apply PEP 758 and strip the parens from every
# `except (A, B):` (-> `except A, B:`) repo-wide. We keep the parenthesized form for readability; bump
# this to py314 if/when we choose to adopt the unparenthesized style in a dedicated formatting pass.
target-version = "py313"
[tool.mypy]
python_version = "3.14"
strict = true
# Third-party libraries that ship no type stubs / py.typed marker.
[[tool.mypy.overrides]]
module = ["hl7", "hl7.*", "hl7apy.*", "aiosqlite.*", "aioodbc.*", "pyodbc.*", "asyncpg.*", "ldap3.*", "paramiko.*", "opentelemetry.*", "defusedxml", "defusedxml.*", "pyx12", "pyx12.*", "lxml", "lxml.*", "xmlschema", "xmlschema.*", "signxml", "signxml.*", "psutil", "psutil.*"]
ignore_missing_imports = true