Files
stack/pyproject.toml
kert 35dc05c4d0 feat(nlm): UMLS / SNOMED CT / RxNorm releases and VSAC value sets
A UMLS licence key (UMLS_API_KEY, read via conf.secret("nlm.api_key"))
now drives a new `nlm` module and `stack nlm` CLI:

- `stack nlm releases` lists the current file of every UTS release
  stream (14 today: RxNorm full/prescribable/weekly + RxNav-in-a-Box,
  SNOMED CT US/International/Spanish/CORE/transitive closure/ICD-10-CM
  map, UMLS full/Metathesaurus/MRCONSO).
- `stack nlm fetch [--stream …]` downloads them through the UTS
  /download endpoint to `<nlm.dir>/<stream>/<version>/<file>`, with a
  sidecar (what, where from, bytes, sha256, when). A finished file is
  skipped; an interrupted one resumes from its .part with a Range
  request; a non-zip answer (a login page) is refused.
- `stack nlm vsac` saves every VSAC value-set definition (18.6k
  versions, paged FHIR search) as gzipped JSON plus index.jsonl, then
  $expands each one — large expansions are paged and joined — with
  backoff on 429/5xx, 4 workers, resumable, failures to errors.jsonl.

The key rides as a query parameter (UTS) or basic auth (VSAC);
nlm.scrub() strips it from every error before it can reach a log.
nlm.dir defaults to the gitignored data/nlm: the content is
UMLS-licensed and must never be published.

Live-checked: listing, an authenticated download with sidecar, and a
768-code value set expanded in pages of 25 matching the one-shot
expansion exactly. Tests mock httpx (100% of the new module).
2026-10-27 13:00:28 -04:00

251 lines
5.6 KiB
TOML

[project]
name = "stack"
version = "0.1.0"
description = "Healthcare data platform — ACO analytics, CMS data, BCDA FHIR, PFS equations"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.0.0",
# AGPL-3.0 — flag if shipping outside internal use. <2: pymupdf 2.x folds
# in the former pymupdf_layout code (Polyform Noncommercial / Artifex
# commercial) and pulls onnxruntime into every image — a licence call,
# not a routine bump.
"pymupdf>=1.27.2,<2",
"python-docx>=1.1",
"xlrd>=2.0.2",
]
[project.scripts]
stack = "cli:main"
[project.optional-dependencies]
conf = [
"pydantic>=2.0.0",
"httpx>=0.28.1",
"duckdb>=1.5.4",
]
aco = [
"stack[conf]",
"narwhals>=2.17.0",
"sqlglot>=26.0.0",
"pyarrow>=23.0.1",
"duckdb>=1.5.4",
]
api = [
"stack[conf]",
"fastapi>=0.139.0",
"uvicorn>=0.41.0",
"httpx>=0.28.1",
"pyjwt>=2.15.1",
"cryptography>=50.0.0",
]
bcda = [
"stack[conf]",
"httpx>=0.28.1",
"fsspec>=2026.6.0",
"narwhals>=2.17.0",
"pyarrow>=23.0.1",
]
bib = [
"stack[conf]",
"pydantic>=2.0.0",
"markdown-it-py>=4.0",
"mdit-py-plugins>=0.4",
]
bls = []
llm = [
"stack[conf]",
"stack[bib]",
"httpx>=0.28.1",
"fastapi>=0.139.0",
"uvicorn>=0.41.0",
"langchain-core>=0.3.0",
"langchain-ollama>=0.2.0",
"langchain-postgres>=0.0.12",
"psycopg[binary]>=3.2.0",
"sqlalchemy>=2.0.0",
"pyyaml>=6.0.0", # dev/scripts/llm_golden.py (golden set YAML) runs inside the llm container
]
ccw = [
"pydantic>=2.0.0",
]
cli = [
"stack[aco]",
"stack[api]",
"stack[bib]",
"stack[llm]",
"stack[mail]",
"stack[nlm]",
"typer>=0.24.1",
"uvicorn>=0.41.0",
]
nlm = [
"stack[conf]",
"httpx>=0.28.1",
]
mail = [
"httpx>=0.28.1",
"pydo>=0.29.0",
"resend>=2.0.0",
]
cms = [
"narwhals>=2.17.0",
"pydantic>=2.0.0",
]
opps = [
"stack[conf]",
"stack[rex]",
"duckdb>=1.5.4",
"narwhals>=2.17.0",
]
pfs = [
"stack[conf]",
"stack[rex]",
"duckdb>=1.5.4",
"narwhals>=2.17.0",
]
qpp = [
"stack[conf]",
"pydantic>=2.0.0",
]
rec = [
"stack[conf]",
"stack[pfs]",
"duckdb>=1.5.4",
"narwhals>=2.17.0",
"typer>=0.24.1",
]
rex = [
"stack[conf]",
"narwhals>=2.17.0",
"pyarrow>=23.0.1",
"fsspec>=2026.6.0",
]
prisma = [
"stack[conf]",
"stack[bib]",
"anthropic>=0.40.0",
"httpx[socks]>=0.28.1",
"pyyaml>=6.0.0",
"pydo>=0.29.0",
"pdfminer.six>=20221105",
"resend>=2.0.0",
]
perf = [
"stack[conf]",
"opentelemetry-api>=1.45.1",
"opentelemetry-sdk>=1.45.1",
"opentelemetry-exporter-otlp-proto-grpc>=1.25.0",
"opentelemetry-exporter-prometheus>=0.46b0",
"opentelemetry-instrumentation-fastapi>=0.46b0",
"opentelemetry-semantic-conventions>=0.46b0",
"psutil>=5.9.0",
]
sem = [
"coverage>=7.13.4",
]
lake = [
"stack[aco]",
"databricks-sdk>=0.85.0",
"databricks-bundles>=0.295.0",
]
aws = [
"boto3>=1.35.0",
"s3fs>=2026.2.0",
]
gcp = [
"google-cloud-storage>=2.18.0",
"gcsfs>=2024.1.0",
]
azure = [
"azure-storage-blob>=12.23.0",
"adlfs>=2024.1.0",
]
all = [
"stack[aco]",
"stack[api]",
"stack[bcda]",
"stack[bib]",
"stack[bls]",
"stack[ccw]",
"stack[cli]",
"stack[cms]",
"stack[llm]",
"stack[perf]",
"stack[pfs]",
"stack[qpp]",
"stack[rec]",
"stack[rex]",
"stack[lake]",
"stack[nlm]",
]
[dependency-groups]
dev = [
"stack[all]",
"coverage>=7.13.4",
"dbt-core==1.10.15",
"dbt-duckdb>=1.10,<1.11",
"openpyxl>=3.1.5",
"pdfplumber>=0.11.9",
"polars>=1.38.1",
"pytest>=9.0.2",
"pytest-cov>=7.0.0",
"pytest-xdist>=3.5.0",
"marimo>=0.23.13",
"ruff>=0.11.0",
"altair>=6.0.0",
"pandas>=3.0.1",
"obstore>=0.9.2",
"s3fs>=2026.2.0",
"jinja2>=3.1.0",
"fastexcel>=0.19.0",
]
[build-system]
requires = ["uv_build>=0.7,<1"]
build-backend = "uv_build"
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["E501", "E741"]
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"stub: marks tests that report stub vs implemented status (deselect with '-m \"not stub\"')",
"live: marks tests that need a live DuckDB replica on disk (skipped without one)",
]
filterwarnings = [
"error::ResourceWarning",
"ignore::ResourceWarning:coverage.*",
]
[tool.coverage.run]
# Required for reliable pytest-cov + pytest-xdist combining: each worker
# writes .coverage.<host>.<pid>, then pytest-cov auto-combines at exit.
# Without this, parallel test runs randomly drop coverage data.
parallel = true
source = ["src"]
concurrency = ["multiprocessing", "thread"]
[tool.uv]
# Supply-chain cooldown: resolve only releases at least a week old, so a
# hijacked or yanked-within-days upload never lands in the lock. Renovate's
# minimumReleaseAge matches. Exempt one package with
# `exclude-newer-package` when a CVE fix can't wait. Lives here, not in a
# uv.toml: images copy only pyproject.toml + uv.lock, and the lock records
# the span, so `uv sync --locked` fails wherever the setting isn't visible.
exclude-newer = "7 days"
# Prevents broken .venv symlinks when uv runs under different users.
link-mode = "copy"
python-preference = "managed"
[tool.uv.build-backend]
module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "llm", "mail", "nlm", "opps", "perf", "pfs", "prisma", "qpp", "rec", "rex", "sem"]
namespace = true
source-exclude = ["compose.yml","infra/**","data/**","notebooks/**","tuva/**","assets/**","docs/**","dev/**","bundle/**","cloud/**"]