sourcelock 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hc_source/__init__.py +5 -0
- hc_source/adapters/__init__.py +500 -0
- hc_source/adapters/_demo.py +258 -0
- hc_source/adapters/_demo_fixture.json +25 -0
- hc_source/adapters/_leie_sample.csv +15 -0
- hc_source/adapters/codes.py +1232 -0
- hc_source/adapters/coverage.py +1569 -0
- hc_source/adapters/hcc.py +1450 -0
- hc_source/adapters/leie.py +1310 -0
- hc_source/adapters/provider.py +1159 -0
- hc_source/cache.py +664 -0
- hc_source/cli.py +959 -0
- hc_source/cli_manifest.py +207 -0
- hc_source/data/codes/hcpcs_2026q3.csv.gz +0 -0
- hc_source/data/codes/icd10cm_fy2026.csv.gz +0 -0
- hc_source/data/codes/icd10cm_fy2027.csv.gz +0 -0
- hc_source/data/codes/manifest.json +75 -0
- hc_source/data/codes/regenerate.py +291 -0
- hc_source/data/hcc/hcc_data.json.zlib +0 -0
- hc_source/doctor.py +472 -0
- hc_source/guard.py +877 -0
- hc_source/http.py +541 -0
- hc_source/interfaces.py +395 -0
- hc_source/lockfile.py +236 -0
- hc_source/manifest.py +422 -0
- hc_source/mcp_server.py +203 -0
- hc_source/npi.py +50 -0
- hc_source/receipts.py +74 -0
- hc_source/schemas.py +339 -0
- sourcelock-0.1.0.dist-info/METADATA +272 -0
- sourcelock-0.1.0.dist-info/RECORD +34 -0
- sourcelock-0.1.0.dist-info/WHEEL +4 -0
- sourcelock-0.1.0.dist-info/entry_points.txt +2 -0
- sourcelock-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""``hc-source manifest``: build, sign, verify, and key management.
|
|
2
|
+
|
|
3
|
+
Registered onto the main app in ``cli.py``. Exit codes follow the CLI-wide
|
|
4
|
+
contract: 0 on success, 1 on any usage, validation, or verification failure.
|
|
5
|
+
A failing ``verify`` is exit 1 with the reason on stdout -- CI can gate on it
|
|
6
|
+
directly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
from .lockfile import default_lock_path
|
|
19
|
+
from .manifest import (
|
|
20
|
+
SIGNING_KEY_ENV,
|
|
21
|
+
ManifestError,
|
|
22
|
+
build_envelope,
|
|
23
|
+
generate_keypair,
|
|
24
|
+
load_envelope,
|
|
25
|
+
load_private_key,
|
|
26
|
+
load_public_key,
|
|
27
|
+
public_key_fingerprint,
|
|
28
|
+
save_envelope,
|
|
29
|
+
sign_envelope,
|
|
30
|
+
verify_envelope,
|
|
31
|
+
verify_lockfile,
|
|
32
|
+
verify_vendored_data,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = ["manifest_app"]
|
|
36
|
+
|
|
37
|
+
manifest_app = typer.Typer(
|
|
38
|
+
help="Signed manifests of the current pins, vendored data, and tool versions.",
|
|
39
|
+
no_args_is_help=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
DEFAULT_MANIFEST_FILENAME = "sourcelock-manifest.json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _fail(message: str) -> None:
|
|
46
|
+
typer.echo(message)
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@manifest_app.command("build")
|
|
51
|
+
def manifest_build(
|
|
52
|
+
lock: Optional[Path] = typer.Option(
|
|
53
|
+
None, "--lock", help="Path to source-lock.json (default: ./source-lock.json)."
|
|
54
|
+
),
|
|
55
|
+
out: Path = typer.Option(
|
|
56
|
+
Path(DEFAULT_MANIFEST_FILENAME),
|
|
57
|
+
"--out",
|
|
58
|
+
help=f"Where to write the manifest (default: ./{DEFAULT_MANIFEST_FILENAME}).",
|
|
59
|
+
),
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Build an unsigned manifest from the lockfile, vendored data, and tool versions."""
|
|
62
|
+
lock_path = lock or default_lock_path()
|
|
63
|
+
try:
|
|
64
|
+
envelope = build_envelope(lock_path)
|
|
65
|
+
except FileNotFoundError as exc:
|
|
66
|
+
_fail(str(exc))
|
|
67
|
+
raise AssertionError # pragma: no cover - _fail always exits
|
|
68
|
+
except ValidationError as exc:
|
|
69
|
+
_fail(
|
|
70
|
+
f"source-lock.json at {lock_path} is not valid: {exc.error_count()} problem(s). "
|
|
71
|
+
"Regenerate it with `hc-source lock init`."
|
|
72
|
+
)
|
|
73
|
+
raise AssertionError # pragma: no cover
|
|
74
|
+
except ManifestError as exc:
|
|
75
|
+
_fail(str(exc))
|
|
76
|
+
raise AssertionError # pragma: no cover
|
|
77
|
+
|
|
78
|
+
save_envelope(envelope, out)
|
|
79
|
+
payload = envelope["payload"]
|
|
80
|
+
typer.echo(
|
|
81
|
+
f"wrote {out} -- {len(payload['lockfile']['sources'])} source(s), "
|
|
82
|
+
f"{len(payload['vendored_data'])} vendored file(s), unsigned"
|
|
83
|
+
)
|
|
84
|
+
typer.echo("sign it with `hc-source manifest sign` before shipping it to anyone.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@manifest_app.command("sign")
|
|
88
|
+
def manifest_sign(
|
|
89
|
+
manifest: Path = typer.Argument(..., help="Manifest file produced by `manifest build`."),
|
|
90
|
+
key: Optional[Path] = typer.Option(
|
|
91
|
+
None,
|
|
92
|
+
"--key",
|
|
93
|
+
help=f"Private key path (default: ${SIGNING_KEY_ENV}).",
|
|
94
|
+
),
|
|
95
|
+
out: Optional[Path] = typer.Option(
|
|
96
|
+
None, "--out", help="Where to write the signed manifest (default: in place)."
|
|
97
|
+
),
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Sign a manifest with the founding-service Ed25519 private key."""
|
|
100
|
+
key_path = key or (Path(os.environ[SIGNING_KEY_ENV]) if os.environ.get(SIGNING_KEY_ENV) else None)
|
|
101
|
+
if key_path is None:
|
|
102
|
+
_fail(
|
|
103
|
+
f"no signing key: set {SIGNING_KEY_ENV} to the private key path "
|
|
104
|
+
"(or pass --key). The key lives in the ops directory, never in this repo."
|
|
105
|
+
)
|
|
106
|
+
raise AssertionError # pragma: no cover
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
envelope = load_envelope(manifest)
|
|
110
|
+
private_key = load_private_key(key_path)
|
|
111
|
+
signed = sign_envelope(envelope, private_key)
|
|
112
|
+
except ManifestError as exc:
|
|
113
|
+
_fail(str(exc))
|
|
114
|
+
raise AssertionError # pragma: no cover
|
|
115
|
+
|
|
116
|
+
destination = save_envelope(signed, out or manifest)
|
|
117
|
+
fingerprint = signed["signature"]["public_key_fingerprint"]
|
|
118
|
+
typer.echo(f"signed {destination} (Ed25519, key fingerprint {fingerprint[:16]}...)")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@manifest_app.command("verify")
|
|
122
|
+
def manifest_verify(
|
|
123
|
+
manifest: Path = typer.Argument(..., help="Signed manifest file to verify."),
|
|
124
|
+
key: Path = typer.Option(..., "--key", help="Trusted Ed25519 public key (PEM)."),
|
|
125
|
+
check_data: bool = typer.Option(
|
|
126
|
+
False,
|
|
127
|
+
"--check-data",
|
|
128
|
+
help="Also compare the manifest's vendored-data hashes against this install.",
|
|
129
|
+
),
|
|
130
|
+
check_lock: Optional[Path] = typer.Option(
|
|
131
|
+
None,
|
|
132
|
+
"--check-lock",
|
|
133
|
+
help=(
|
|
134
|
+
"Also bind the manifest to a lockfile on disk: the file at this path must "
|
|
135
|
+
"hash to the one the manifest signed. Pass the source-lock.json doctor "
|
|
136
|
+
"actually reads."
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Verify a manifest's signature against a trusted public key.
|
|
141
|
+
|
|
142
|
+
The signature alone proves that SOME lockfile was signed. `--check-lock`
|
|
143
|
+
proves it was THIS one -- the difference between "the pins were signed" and
|
|
144
|
+
"the pins in use were signed", which is the only version of the claim worth
|
|
145
|
+
anything in an audit.
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
envelope = load_envelope(manifest)
|
|
149
|
+
public_key = load_public_key(key)
|
|
150
|
+
verify_envelope(envelope, public_key)
|
|
151
|
+
except ManifestError as exc:
|
|
152
|
+
_fail(f"verification FAILED: {exc}")
|
|
153
|
+
raise AssertionError # pragma: no cover
|
|
154
|
+
|
|
155
|
+
payload = envelope["payload"]
|
|
156
|
+
typer.echo(
|
|
157
|
+
f"verification OK: signature valid (Ed25519, key fingerprint "
|
|
158
|
+
f"{public_key_fingerprint(public_key)[:16]}...)"
|
|
159
|
+
)
|
|
160
|
+
typer.echo(
|
|
161
|
+
f" manifest generated_at={payload['generated_at']} "
|
|
162
|
+
f"sources={len(payload['lockfile']['sources'])} "
|
|
163
|
+
f"lockfile_sha256={payload['lockfile']['sha256'][:16]}..."
|
|
164
|
+
)
|
|
165
|
+
# Inside the signed payload since round 2; a signing time sitting beside the
|
|
166
|
+
# signature was one nobody had signed.
|
|
167
|
+
typer.echo(f" signed_at={payload.get('signed_at', '(not recorded)')}")
|
|
168
|
+
|
|
169
|
+
if check_lock is not None:
|
|
170
|
+
problems = verify_lockfile(payload, check_lock)
|
|
171
|
+
if problems:
|
|
172
|
+
for problem in problems:
|
|
173
|
+
typer.echo(f" lockfile mismatch: {problem}")
|
|
174
|
+
_fail("the lockfile on disk is not the one this manifest signed.")
|
|
175
|
+
raise AssertionError # pragma: no cover
|
|
176
|
+
typer.echo(f" lockfile at {check_lock} matches the signed manifest.")
|
|
177
|
+
|
|
178
|
+
if check_data:
|
|
179
|
+
problems = verify_vendored_data(payload)
|
|
180
|
+
if problems:
|
|
181
|
+
for problem in problems:
|
|
182
|
+
typer.echo(f" data mismatch: {problem}")
|
|
183
|
+
_fail("vendored data does not match the signed manifest.")
|
|
184
|
+
raise AssertionError # pragma: no cover
|
|
185
|
+
typer.echo(f" vendored data matches ({len(payload['vendored_data'])} file(s) checked).")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@manifest_app.command("keygen")
|
|
189
|
+
def manifest_keygen(
|
|
190
|
+
out_dir: Path = typer.Option(
|
|
191
|
+
..., "--out-dir", help="Directory for the new keypair (outside any repo)."
|
|
192
|
+
),
|
|
193
|
+
name: str = typer.Option(
|
|
194
|
+
"manifest-signing", "--name", help="Basename: writes <name>.key and <name>.pub."
|
|
195
|
+
),
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Generate an Ed25519 keypair for manifest signing. Never overwrites."""
|
|
198
|
+
try:
|
|
199
|
+
priv_path, pub_path = generate_keypair(out_dir, name)
|
|
200
|
+
except ManifestError as exc:
|
|
201
|
+
_fail(str(exc))
|
|
202
|
+
raise AssertionError # pragma: no cover
|
|
203
|
+
|
|
204
|
+
fingerprint = public_key_fingerprint(load_public_key(pub_path))
|
|
205
|
+
typer.echo(f"wrote {priv_path} (private, mode 0600 -- keep out of every git repo)")
|
|
206
|
+
typer.echo(f"wrote {pub_path} (public -- this one you distribute)")
|
|
207
|
+
typer.echo(f"fingerprint {fingerprint}")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"route": "codes",
|
|
3
|
+
"generated_at": "2026-08-01",
|
|
4
|
+
"regenerate": "python hc_source/data/codes/regenerate.py",
|
|
5
|
+
"canonicalization": "canonical_sha256 = SHA-256 of the uncompressed CSV bytes (UTF-8, LF line endings, header row first, source row order preserved). Drift decisions compare canonical hashes; raw zip sha256 values are kept for forensics only.",
|
|
6
|
+
"cpt_exclusion": "Only the ANWEB fixed-width master file is ingested from the HCPCS zip; proc_notes_*.txt and all .xlsx members are excluded (CPT mentions / unverified content). Numeric-leading (CPT Level I) and D-prefixed (ADA CDT) records are dropped defensively.",
|
|
7
|
+
"cm_april_status": {
|
|
8
|
+
"2026": "posted",
|
|
9
|
+
"2027": "unknown"
|
|
10
|
+
},
|
|
11
|
+
"cm_releases": {
|
|
12
|
+
"fy2026": {
|
|
13
|
+
"source_url": "https://www.cms.gov/files/zip/2026-code-descriptions-tabular-order.zip",
|
|
14
|
+
"source_zip_sha256": "55a9124a27ca78a4a5c41e1101eda064ec2e08d105ff9e78d1fbd1a83298ef11",
|
|
15
|
+
"source_last_modified": "Fri, 06 Jun 2025 12:34:30 GMT",
|
|
16
|
+
"inner_file": "icd10cm_codes_2026.txt",
|
|
17
|
+
"inner_sha256": "a7e2a77e4627ed55c8afe2b8a7ae22efcbab6b6b162d96cf145213772c1246ba",
|
|
18
|
+
"fetched_at": "2026-08-01",
|
|
19
|
+
"effective_from": "2025-10-01",
|
|
20
|
+
"effective_to": "2026-09-30",
|
|
21
|
+
"table": "icd10cm_fy2026.csv.gz",
|
|
22
|
+
"canonical_sha256": "f85c0c389d1749389b05bd6e975427b811bd7348582a3a456dbdf6a487aa5328",
|
|
23
|
+
"row_count": 74719
|
|
24
|
+
},
|
|
25
|
+
"fy2026-april": {
|
|
26
|
+
"source_url": "https://www.cms.gov/files/zip/april-1-2026-code-descriptions-tabular-order.zip",
|
|
27
|
+
"source_zip_sha256": "4fd9d8b37f02ab42827c7e7be30595c005b0cc3a6bae7a515e3f4c86b6918688",
|
|
28
|
+
"source_last_modified": "Wed, 17 Dec 2025 14:15:25 GMT",
|
|
29
|
+
"inner_file": "Code Descriptions/icd10cm_codes_2026.txt",
|
|
30
|
+
"inner_sha256": "a7e2a77e4627ed55c8afe2b8a7ae22efcbab6b6b162d96cf145213772c1246ba",
|
|
31
|
+
"fetched_at": "2026-08-01",
|
|
32
|
+
"effective_from": "2026-04-01",
|
|
33
|
+
"effective_to": "2026-09-30",
|
|
34
|
+
"table": "icd10cm_fy2026.csv.gz",
|
|
35
|
+
"content_identical_to": "fy2026",
|
|
36
|
+
"canonical_sha256": "f85c0c389d1749389b05bd6e975427b811bd7348582a3a456dbdf6a487aa5328",
|
|
37
|
+
"row_count": 74719
|
|
38
|
+
},
|
|
39
|
+
"fy2027": {
|
|
40
|
+
"source_url": "https://www.cms.gov/files/zip/2027-code-descriptions-tabular-order.zip",
|
|
41
|
+
"source_zip_sha256": "91c6c9d1117764ce72375a0f3a5493b1725dafbc1ab283b55799076c9e194965",
|
|
42
|
+
"source_last_modified": "Fri, 05 Jun 2026 16:52:26 GMT",
|
|
43
|
+
"inner_file": "Code Descriptions/icd10cm_codes_2027.txt",
|
|
44
|
+
"inner_sha256": "3c0583a38ee0e848f7dc0ac8ce88e8f001bbba8c6385ed825d44d46f6e5297c9",
|
|
45
|
+
"fetched_at": "2026-08-01",
|
|
46
|
+
"effective_from": "2026-10-01",
|
|
47
|
+
"effective_to": "2027-09-30",
|
|
48
|
+
"table": "icd10cm_fy2027.csv.gz",
|
|
49
|
+
"canonical_sha256": "43fabd3a3640eb2bf9dab6d2989966f74235428df51c2eab7e69408ed5c16a4d",
|
|
50
|
+
"row_count": 74879
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"hcpcs_releases": {
|
|
54
|
+
"2026q3": {
|
|
55
|
+
"source_url": "https://www.cms.gov/files/zip/july-2026-alpha-numeric-hcpcs-file.zip",
|
|
56
|
+
"source_zip_sha256": "5591fed257e4d2307c1d2dc9c66abc8e346b1f4c076e9e48559799e2de14b70a",
|
|
57
|
+
"source_last_modified": "Wed, 17 Jun 2026 15:59:47 GMT",
|
|
58
|
+
"inner_file": "HCPC2026_JUL_ANWEB_06172026.txt",
|
|
59
|
+
"inner_sha256": "38e9b478568bb267edc57de50e9a7ba6af71782a90d26f3b6c2d8d4ccdf25074",
|
|
60
|
+
"fetched_at": "2026-08-01",
|
|
61
|
+
"effective_from": "2026-07-01",
|
|
62
|
+
"effective_to": "2026-09-30",
|
|
63
|
+
"table": "hcpcs_2026q3.csv.gz",
|
|
64
|
+
"canonical_sha256": "ed67edc4414ccbd249f032ceb9d3ddd3ba3d6b787ce2b9fa1952994cc2559645",
|
|
65
|
+
"row_count": 16826,
|
|
66
|
+
"cpt_cdt_records_excluded": 0
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"cdc_root_listing": {
|
|
70
|
+
"source_url": "https://ftp.cdc.gov/pub/Health_Statistics/NCHS/Publications/ICD10CM/",
|
|
71
|
+
"raw_sha256": "e34231a4161dfab8956ebd368c5fa024f0bbdb2bf9973392cee4d1287897c6ed",
|
|
72
|
+
"fetched_at": "2026-08-01",
|
|
73
|
+
"fixture": "tests/fixtures/codes/cdc_root_listing.html"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Regenerate the vendored derived tables for the ``codes`` route.
|
|
2
|
+
|
|
3
|
+
Run from the repository root::
|
|
4
|
+
|
|
5
|
+
python hc_source/data/codes/regenerate.py
|
|
6
|
+
|
|
7
|
+
Downloads the pinned CMS release zips (ICD-10-CM FY + April update, HCPCS
|
|
8
|
+
Level II quarterly) and the CDC ICD-10-CM directory listing, derives compact
|
|
9
|
+
gzipped CSV tables from them, and rewrites ``manifest.json`` with full
|
|
10
|
+
provenance (source URL, raw zip sha256, inner-file sha256, fetch date,
|
|
11
|
+
canonical table hashes, row counts).
|
|
12
|
+
|
|
13
|
+
Design rules honoured here:
|
|
14
|
+
|
|
15
|
+
* **Derive, don't dump** -- only the code/description/date columns the adapter
|
|
16
|
+
reads are kept. Raw zips are cached locally and never committed.
|
|
17
|
+
* **Canonical content hashes** -- each table's ``canonical_sha256`` is the
|
|
18
|
+
SHA-256 of the *uncompressed* CSV bytes (UTF-8, ``\n`` line endings, stable
|
|
19
|
+
column order), so drift decisions survive gzip re-compression. Gzip output
|
|
20
|
+
uses ``mtime=0`` so the compressed bytes are reproducible too.
|
|
21
|
+
* **CPT (HCPCS Level I) exclusion** -- only the ANWEB fixed-width master file
|
|
22
|
+
is ingested from the HCPCS zip. ``proc_notes_*.txt`` (contains CPT code
|
|
23
|
+
mentions in prose) and every ``.xlsx`` are never read. Defensively, any
|
|
24
|
+
record whose code is numeric-leading (CPT Level I) or ``D``-prefixed (ADA
|
|
25
|
+
CDT dental) is dropped and counted in the manifest (expected count: 0 --
|
|
26
|
+
verified zero in the July 2026 file).
|
|
27
|
+
|
|
28
|
+
Requires network access. The test suite never runs this; it consumes the
|
|
29
|
+
committed outputs offline.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import csv
|
|
35
|
+
import gzip
|
|
36
|
+
import hashlib
|
|
37
|
+
import io
|
|
38
|
+
import json
|
|
39
|
+
import re
|
|
40
|
+
import sys
|
|
41
|
+
import zipfile
|
|
42
|
+
from datetime import date, timezone, datetime
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
HERE = Path(__file__).resolve().parent
|
|
46
|
+
REPO = HERE.parents[3]
|
|
47
|
+
sys.path.insert(0, str(REPO))
|
|
48
|
+
|
|
49
|
+
from hc_source.http import fetch # noqa: E402
|
|
50
|
+
|
|
51
|
+
CACHE = Path.home() / ".cache" / "sourcelock-codes-raw"
|
|
52
|
+
CACHE.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# Inner filenames keep the FY year and the nesting varies by release (FY2026
|
|
55
|
+
# is flat; the April-1-2026 AND FY2027 zips nest under "Code Descriptions/"),
|
|
56
|
+
# so members are located by basename, never by full path.
|
|
57
|
+
CM_RELEASES = [
|
|
58
|
+
# label, url, inner basename, effective_from, nominal effective_to
|
|
59
|
+
(
|
|
60
|
+
"fy2026",
|
|
61
|
+
"https://www.cms.gov/files/zip/2026-code-descriptions-tabular-order.zip",
|
|
62
|
+
"icd10cm_codes_2026.txt",
|
|
63
|
+
"2025-10-01",
|
|
64
|
+
"2026-09-30",
|
|
65
|
+
),
|
|
66
|
+
(
|
|
67
|
+
"fy2026-april",
|
|
68
|
+
"https://www.cms.gov/files/zip/april-1-2026-code-descriptions-tabular-order.zip",
|
|
69
|
+
"icd10cm_codes_2026.txt",
|
|
70
|
+
"2026-04-01",
|
|
71
|
+
"2026-09-30",
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
"fy2027",
|
|
75
|
+
"https://www.cms.gov/files/zip/2027-code-descriptions-tabular-order.zip",
|
|
76
|
+
"icd10cm_codes_2027.txt",
|
|
77
|
+
"2026-10-01",
|
|
78
|
+
"2027-09-30",
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
HCPCS_RELEASES = [
|
|
83
|
+
(
|
|
84
|
+
"2026q3",
|
|
85
|
+
"https://www.cms.gov/files/zip/july-2026-alpha-numeric-hcpcs-file.zip",
|
|
86
|
+
re.compile(r"^HCPC\d{4}_[A-Z]{3}_ANWEB_\d{8}\.txt$"),
|
|
87
|
+
"2026-07-01",
|
|
88
|
+
"2026-09-30",
|
|
89
|
+
),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
CDC_ROOT = "https://ftp.cdc.gov/pub/Health_Statistics/NCHS/Publications/ICD10CM/"
|
|
93
|
+
|
|
94
|
+
# April-update publication status per fiscal year, as verifiable on the fetch
|
|
95
|
+
# date. "posted" = the April package is published; "absent" = April 1 of that
|
|
96
|
+
# FY has passed with no April package (settled fact); "unknown" = April 1 of
|
|
97
|
+
# that FY is in the future relative to the fetch date, so the package may yet
|
|
98
|
+
# be published. The adapter fails closed on "unknown".
|
|
99
|
+
CM_APRIL_STATUS = {"2026": "posted", "2027": "unknown"}
|
|
100
|
+
|
|
101
|
+
# HCPCS Level I (CPT, 5-char numeric-leading) and ADA CDT (5-char D-prefix)
|
|
102
|
+
# exclusion. Two-character records are CMS Level II *modifiers* (they sit in
|
|
103
|
+
# cols 4-5 with cols 1-3 blank, e.g. "DA"), never CPT/CDT content.
|
|
104
|
+
_CPT_OR_CDT = re.compile(r"^(\d|D)[0-9A-Z]{4}$")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _download(url: str) -> tuple[bytes, str, str]:
|
|
108
|
+
name = url.rsplit("/", 1)[-1]
|
|
109
|
+
cached = CACHE / name
|
|
110
|
+
meta = CACHE / (name + ".meta.json")
|
|
111
|
+
if cached.exists() and meta.exists():
|
|
112
|
+
info = json.loads(meta.read_text())
|
|
113
|
+
return cached.read_bytes(), info["sha256"], info.get("last_modified", "")
|
|
114
|
+
print(f"fetching {url}")
|
|
115
|
+
result = fetch(url, timeout=300.0)
|
|
116
|
+
cached.write_bytes(result.content)
|
|
117
|
+
last_modified = result.headers.get("last-modified", "")
|
|
118
|
+
meta.write_text(json.dumps({"sha256": result.sha256, "last_modified": last_modified}))
|
|
119
|
+
return result.content, result.sha256, last_modified
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _write_table(path: Path, header: list[str], rows: list[list[str]]) -> tuple[str, int]:
|
|
123
|
+
buf = io.StringIO()
|
|
124
|
+
writer = csv.writer(buf, lineterminator="\n")
|
|
125
|
+
writer.writerow(header)
|
|
126
|
+
writer.writerows(rows)
|
|
127
|
+
raw = buf.getvalue().encode("utf-8")
|
|
128
|
+
canonical = hashlib.sha256(raw).hexdigest()
|
|
129
|
+
with path.open("wb") as fh:
|
|
130
|
+
with gzip.GzipFile(fileobj=fh, mode="wb", mtime=0) as gz:
|
|
131
|
+
gz.write(raw)
|
|
132
|
+
return canonical, len(rows)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _parse_cm(text: str) -> list[list[str]]:
|
|
136
|
+
rows = []
|
|
137
|
+
for line in text.splitlines():
|
|
138
|
+
if not line.strip():
|
|
139
|
+
continue
|
|
140
|
+
code = line[:8].strip()
|
|
141
|
+
desc = line[8:].strip()
|
|
142
|
+
rows.append([code, desc])
|
|
143
|
+
return rows
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _parse_hcpcs(text: str) -> tuple[list[list[str]], int]:
|
|
147
|
+
rows: list[list[str]] = []
|
|
148
|
+
excluded = 0
|
|
149
|
+
for line in text.splitlines():
|
|
150
|
+
if not line.strip():
|
|
151
|
+
continue
|
|
152
|
+
line = line.ljust(320)
|
|
153
|
+
code = line[0:5].strip() # modifiers sit in cols 4-5 with cols 1-3 blank
|
|
154
|
+
if _CPT_OR_CDT.match(code):
|
|
155
|
+
excluded += 1
|
|
156
|
+
continue
|
|
157
|
+
rows.append(
|
|
158
|
+
[
|
|
159
|
+
code,
|
|
160
|
+
line[5:10].strip(),
|
|
161
|
+
line[10:11].strip(),
|
|
162
|
+
line[11:91].strip(),
|
|
163
|
+
line[91:119].strip(),
|
|
164
|
+
line[268:276].strip(),
|
|
165
|
+
line[276:284].strip(),
|
|
166
|
+
line[284:292].strip(),
|
|
167
|
+
line[292:293].strip(),
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
return rows, excluded
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main() -> None:
|
|
174
|
+
fetched_at = datetime.now(timezone.utc).date().isoformat()
|
|
175
|
+
manifest: dict = {
|
|
176
|
+
"route": "codes",
|
|
177
|
+
"generated_at": fetched_at,
|
|
178
|
+
"regenerate": "python hc_source/data/codes/regenerate.py",
|
|
179
|
+
"canonicalization": (
|
|
180
|
+
"canonical_sha256 = SHA-256 of the uncompressed CSV bytes (UTF-8, LF line "
|
|
181
|
+
"endings, header row first, source row order preserved). Drift decisions "
|
|
182
|
+
"compare canonical hashes; raw zip sha256 values are kept for forensics only."
|
|
183
|
+
),
|
|
184
|
+
"cpt_exclusion": (
|
|
185
|
+
"Only the ANWEB fixed-width master file is ingested from the HCPCS zip; "
|
|
186
|
+
"proc_notes_*.txt and all .xlsx members are excluded (CPT mentions / "
|
|
187
|
+
"unverified content). Numeric-leading (CPT Level I) and D-prefixed (ADA CDT) "
|
|
188
|
+
"records are dropped defensively."
|
|
189
|
+
),
|
|
190
|
+
"cm_april_status": CM_APRIL_STATUS,
|
|
191
|
+
"cm_releases": {},
|
|
192
|
+
"hcpcs_releases": {},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
cm_tables: dict[str, str] = {} # inner sha -> table filename, for dedupe
|
|
196
|
+
for label, url, basename, eff_from, eff_to in CM_RELEASES:
|
|
197
|
+
content, zip_sha, last_modified = _download(url)
|
|
198
|
+
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
|
199
|
+
matches = [n for n in zf.namelist() if n.rsplit("/", 1)[-1] == basename]
|
|
200
|
+
if len(matches) != 1:
|
|
201
|
+
raise SystemExit(f"expected exactly one {basename} in {url}, got {matches}")
|
|
202
|
+
member = matches[0]
|
|
203
|
+
inner = zf.read(member)
|
|
204
|
+
inner_sha = hashlib.sha256(inner).hexdigest()
|
|
205
|
+
entry = {
|
|
206
|
+
"source_url": url,
|
|
207
|
+
"source_zip_sha256": zip_sha,
|
|
208
|
+
"source_last_modified": last_modified,
|
|
209
|
+
"inner_file": member,
|
|
210
|
+
"inner_sha256": inner_sha,
|
|
211
|
+
"fetched_at": fetched_at,
|
|
212
|
+
"effective_from": eff_from,
|
|
213
|
+
"effective_to": eff_to,
|
|
214
|
+
}
|
|
215
|
+
if inner_sha in cm_tables:
|
|
216
|
+
entry["table"] = cm_tables[inner_sha]
|
|
217
|
+
entry["content_identical_to"] = next(
|
|
218
|
+
k for k, v in manifest["cm_releases"].items() if v["table"] == cm_tables[inner_sha]
|
|
219
|
+
)
|
|
220
|
+
ref = manifest["cm_releases"][entry["content_identical_to"]]
|
|
221
|
+
entry["canonical_sha256"] = ref["canonical_sha256"]
|
|
222
|
+
entry["row_count"] = ref["row_count"]
|
|
223
|
+
else:
|
|
224
|
+
table_name = f"icd10cm_{label.replace('-', '_')}.csv.gz"
|
|
225
|
+
rows = _parse_cm(inner.decode("utf-8", errors="strict"))
|
|
226
|
+
canonical, count = _write_table(HERE / table_name, ["code", "description"], rows)
|
|
227
|
+
entry.update({"table": table_name, "canonical_sha256": canonical, "row_count": count})
|
|
228
|
+
cm_tables[inner_sha] = table_name
|
|
229
|
+
manifest["cm_releases"][label] = entry
|
|
230
|
+
print(f"{label}: {entry['row_count']} rows -> {entry['table']}")
|
|
231
|
+
|
|
232
|
+
for label, url, member_re, eff_from, eff_to in HCPCS_RELEASES:
|
|
233
|
+
content, zip_sha, last_modified = _download(url)
|
|
234
|
+
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
|
235
|
+
members = [n for n in zf.namelist() if member_re.match(n)]
|
|
236
|
+
if len(members) != 1:
|
|
237
|
+
raise SystemExit(f"expected exactly one ANWEB master in {url}, got {members}")
|
|
238
|
+
inner = zf.read(members[0])
|
|
239
|
+
inner_sha = hashlib.sha256(inner).hexdigest()
|
|
240
|
+
rows, excluded = _parse_hcpcs(inner.decode("latin-1"))
|
|
241
|
+
table_name = f"hcpcs_{label}.csv.gz"
|
|
242
|
+
canonical, count = _write_table(
|
|
243
|
+
HERE / table_name,
|
|
244
|
+
[
|
|
245
|
+
"code",
|
|
246
|
+
"seq",
|
|
247
|
+
"record_id",
|
|
248
|
+
"long_description",
|
|
249
|
+
"short_description",
|
|
250
|
+
"code_added",
|
|
251
|
+
"action_effective",
|
|
252
|
+
"termination",
|
|
253
|
+
"action_code",
|
|
254
|
+
],
|
|
255
|
+
rows,
|
|
256
|
+
)
|
|
257
|
+
manifest["hcpcs_releases"][label] = {
|
|
258
|
+
"source_url": url,
|
|
259
|
+
"source_zip_sha256": zip_sha,
|
|
260
|
+
"source_last_modified": last_modified,
|
|
261
|
+
"inner_file": members[0],
|
|
262
|
+
"inner_sha256": inner_sha,
|
|
263
|
+
"fetched_at": fetched_at,
|
|
264
|
+
"effective_from": eff_from,
|
|
265
|
+
"effective_to": eff_to,
|
|
266
|
+
"table": table_name,
|
|
267
|
+
"canonical_sha256": canonical,
|
|
268
|
+
"row_count": count,
|
|
269
|
+
"cpt_cdt_records_excluded": excluded,
|
|
270
|
+
}
|
|
271
|
+
print(f"{label}: {count} rows ({excluded} CPT/CDT excluded) -> {table_name}")
|
|
272
|
+
|
|
273
|
+
listing = fetch(CDC_ROOT, timeout=60.0)
|
|
274
|
+
fixture = HERE.parent / "cdc_root_listing.html"
|
|
275
|
+
fixture.write_bytes(listing.content)
|
|
276
|
+
manifest["cdc_root_listing"] = {
|
|
277
|
+
"source_url": CDC_ROOT,
|
|
278
|
+
"raw_sha256": listing.sha256,
|
|
279
|
+
"fetched_at": fetched_at,
|
|
280
|
+
"fixture": "tests/fixtures/codes/cdc_root_listing.html",
|
|
281
|
+
}
|
|
282
|
+
print(f"cdc listing: {len(listing.content)} bytes -> {fixture}")
|
|
283
|
+
|
|
284
|
+
(HERE / "manifest.json").write_text(
|
|
285
|
+
json.dumps(manifest, indent=2, sort_keys=False) + "\n", encoding="utf-8"
|
|
286
|
+
)
|
|
287
|
+
print("wrote manifest.json")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
main()
|
|
Binary file
|