Some checks failed
CI / lint (push) Successful in 58s
CI / notebooks-smoke (push) Successful in 1m44s
CI / test (push) Failing after 3m3s
Infra CI / notebooks (push) Successful in 1m13s
Infra CI / docs (push) Successful in 1m46s
Infra CI / llm (push) Successful in 49s
Infra CI / mc (push) Failing after 36s
Infra CI / zotero (push) Successful in 27s
Infra CI / api (push) Successful in 1m9s
Infra CI / bcda (push) Successful in 1m37s
CI / lint (pull_request) Successful in 41s
CI / notebooks-smoke (pull_request) Successful in 1m49s
CI / test (pull_request) Failing after 2m50s
Infra CI / docs (pull_request) Successful in 17s
Infra CI / notebooks (pull_request) Successful in 59s
Infra CI / api (pull_request) Successful in 14s
Infra CI / bcda (pull_request) Successful in 14s
Infra CI / llm (pull_request) Successful in 20s
Infra CI / zotero (pull_request) Successful in 14s
Infra CI / mc (pull_request) Failing after 34s
"No TIN available" is a fact about the data, not a bug in ours. Institutional claims never carry a TIN: the real Large-entity v3 pull has 0 billing TINs on every claim type and 0 CCNs on every institutional claim (the dictionary's NamingSystem/CCN identifier is not emitted and half the institutional claims have no billing provider at all), and CCLF1 has an OSCAR/CCN and NPIs but no TIN either. The only TIN on claims is the carrier/DME CLM_RNDRG_PRVDR_TAX_NUM, the TIN the line was billed under, so the connector now gives professional and DME lines that value as billing_tin too (institutional stays NULL; the facility is its NPI and CCN). CMS publishes no provider TIN or EIN on data.cms.gov — checked every enrollment, ownership, change-of-ownership, Provider of Services and cost-report family in the catalog. What can be vendored is CCN ↔ NPI ↔ organization. cms.datacms resolves a dataset family to its newest version in the data.cms.gov catalog, downloads the CSV (or pages the API) and names columns like the generated cms.table models. `stack load cms` (aco.load.cms) loads the institutional universe — hospital, SNF, HHA, hospice, FQHC and RHC enrollments, the Provider of Services file and the three cost reports — as cms.* with provenance columns and ingest_log rows, then derives cms.institutional_provider, cms.ccn_crosswalk (PECOS ∪ POS-only CCNs, 65,161 rows today), cms.provider_tin_observed (NPI → TIN as seen on our own claims) and cms.ccn_tin (CCN → NPI → TIN, with cms.aco_participant_tin as the authoritative hook when a participant list is loaded). The command prints facility-NPI → CCN → TIN coverage; the Data page gets the same query. On the sandbox nothing resolves — its 8 facility NPIs are synthetic — which is the expected answer there. refs #1139
243 lines
8.2 KiB
Python
243 lines
8.2 KiB
Python
"""cms.datacms — catalog resolution, column naming, download paths."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from cms import datacms
|
|
|
|
HOSPITAL_HEADER = [
|
|
"ENROLLMENT ID",
|
|
"ENROLLMENT STATE",
|
|
"PROVIDER TYPE CODE",
|
|
"PROVIDER TYPE TEXT",
|
|
"NPI",
|
|
"MULTIPLE NPI FLAG",
|
|
"CCN",
|
|
"ASSOCIATE ID",
|
|
"ORGANIZATION NAME",
|
|
"DOING BUSINESS AS NAME",
|
|
"INCORPORATION DATE",
|
|
"INCORPORATION STATE",
|
|
"ORGANIZATION TYPE STRUCTURE",
|
|
"ORGANIZATION OTHER TYPE TEXT",
|
|
"PROPRIETARY NONPROFIT",
|
|
"ADDRESS LINE 1",
|
|
"ADDRESS LINE 2",
|
|
"CITY",
|
|
"STATE",
|
|
"ZIP CODE",
|
|
"PRACTICE LOCATION TYPE",
|
|
"LOCATION OTHER TYPE TEXT",
|
|
"SUBGROUP - GENERAL",
|
|
"SUBGROUP - ACUTE CARE",
|
|
"SUBGROUP - ALCOHOL DRUG",
|
|
"SUBGROUP - CHILDREN",
|
|
]
|
|
|
|
|
|
def test_column_names_match_generated_models() -> None:
|
|
from cms.table.hospital_enrollments import HospitalEnrollments
|
|
from cms.table.hospital_provider_cost_report import HospitalProviderCostReport
|
|
from cms.table.skilled_nursing_facility_enrollments import (
|
|
SkilledNursingFacilityEnrollments,
|
|
)
|
|
|
|
fields = set(HospitalEnrollments.model_fields)
|
|
# the generated model predates a few SUBGROUP columns CMS added; the
|
|
# loader takes its columns from the CSV, so only the stable core must map
|
|
for h in HOSPITAL_HEADER[:22]:
|
|
assert datacms.sanitize_column_name(h) in fields, h
|
|
assert datacms.sanitize_column_name("PROPRIETARY_NONPROFIT") in set(
|
|
SkilledNursingFacilityEnrollments.model_fields
|
|
)
|
|
cost = set(HospitalProviderCostReport.model_fields)
|
|
for h in (
|
|
"rpt_rec_num",
|
|
"Provider CCN",
|
|
"Hospital Name",
|
|
"CCN Facility Type",
|
|
"Type of Control",
|
|
):
|
|
assert datacms.sanitize_column_name(h) in cost, h
|
|
assert datacms.sanitize_column_name("3-Day Rule") == "_3_day_rule"
|
|
assert datacms.sanitize_column_name("Type") == "type_"
|
|
|
|
|
|
def test_family_title_and_snapshot_date() -> None:
|
|
assert (
|
|
datacms.family_title("Hospital Enrollments : 2026-10-01")
|
|
== "Hospital Enrollments"
|
|
)
|
|
assert datacms.family_title("Hospital Enrollments") == "Hospital Enrollments"
|
|
assert (
|
|
datacms.snapshot_date_from_name("Hospital_Enrollments_2026.09.29.csv")
|
|
== "2026-09-29"
|
|
)
|
|
assert (
|
|
datacms.snapshot_date_from_name("Hospital_and_other.DATA.Q3_2026.csv")
|
|
== "2026-Q3"
|
|
)
|
|
assert datacms.snapshot_date_from_name("CostReport_2021_Final.csv") == "2021"
|
|
assert datacms.snapshot_date_from_name("nothing") == ""
|
|
|
|
|
|
def _catalog() -> list[dict]:
|
|
fam = "f6f6505c-e8b0-4d57-b258-e2b94133aaf2"
|
|
return [
|
|
{
|
|
"title": "Hospital Enrollments : 2026-09-01",
|
|
"modified": "2026-09-03",
|
|
"identifier": f"https://data.cms.gov/data-api/v1/dataset/{fam}/data-viewer",
|
|
"distribution": [
|
|
{
|
|
"mediaType": "text/csv",
|
|
"downloadURL": "https://x/old/Hospital_Enrollments_2026.08.29.csv",
|
|
},
|
|
{"accessURL": f"https://data.cms.gov/data-api/v1/dataset/{fam}/data"},
|
|
],
|
|
},
|
|
{
|
|
"title": "Hospital Enrollments : 2026-10-01",
|
|
"modified": "2026-10-19",
|
|
"identifier": f"https://data.cms.gov/data-api/v1/dataset/{fam}/data-viewer",
|
|
"distribution": [
|
|
{"accessURL": f"https://data.cms.gov/data-api/v1/dataset/{fam}/data"},
|
|
{
|
|
"mediaType": "text/csv",
|
|
"downloadURL": "https://x/new/Hospital_Enrollments_2026.09.29.csv",
|
|
},
|
|
{
|
|
"accessURL": "https://data.cms.gov/data-api/v1/dataset/71b9466f-aaaa-bbbb-cccc-000000000000/data"
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"title": "Hospital Provider Cost Report : 2021-12-05",
|
|
"modified": "2026-10-05",
|
|
"identifier": "https://data.cms.gov/data-api/v1/dataset/98b7383e-ad67-44d5-a7d8-0a80a92a72c1/data-viewer",
|
|
"distribution": [
|
|
{
|
|
"accessURL": "https://data.cms.gov/data-api/v1/dataset/98b7383e-ad67-44d5-a7d8-0a80a92a72c1/data"
|
|
}
|
|
],
|
|
},
|
|
]
|
|
|
|
|
|
def test_resolve_picks_newest_version_and_family_api() -> None:
|
|
ds = datacms.resolve("hospital_enrollments", _catalog())
|
|
assert ds.version_title.endswith("2026-10-01") and ds.modified == "2026-10-19"
|
|
assert ds.csv_url.endswith("Hospital_Enrollments_2026.09.29.csv")
|
|
assert ds.api_url.endswith("f6f6505c-e8b0-4d57-b258-e2b94133aaf2/data")
|
|
assert (
|
|
ds.filename == "Hospital_Enrollments_2026.09.29.csv"
|
|
and ds.snapshot_date == "2026-09-29"
|
|
)
|
|
cost = datacms.resolve("hospital_provider_cost_report", _catalog())
|
|
assert (
|
|
cost.csv_url == "" and cost.filename == "hospital_provider_cost_report.api.csv"
|
|
)
|
|
with pytest.raises(LookupError):
|
|
datacms.resolve("hospice_enrollments", _catalog())
|
|
with pytest.raises(KeyError):
|
|
datacms.resolve("nope", _catalog())
|
|
assert (
|
|
datacms.resolve("nope", _catalog(), title="Hospital Enrollments").table
|
|
== "nope"
|
|
)
|
|
|
|
|
|
def test_catalog_cache(tmp_path: Path, monkeypatch) -> None:
|
|
calls = []
|
|
|
|
class R:
|
|
def json(self): # noqa: ANN202
|
|
calls.append(1)
|
|
return {"dataset": _catalog()}
|
|
|
|
monkeypatch.setattr(datacms, "_get", lambda url, **kw: R())
|
|
cache = tmp_path / "catalog.json"
|
|
assert len(datacms.catalog(cache)) == 3 and cache.exists() and calls == [1]
|
|
assert len(datacms.catalog(cache)) == 3 and calls == [1] # served from disk
|
|
assert len(datacms.catalog(cache, refresh=True)) == 3 and calls == [1, 1]
|
|
assert len(datacms.catalog(None)) == 3
|
|
|
|
|
|
def test_download_csv_and_api_paging(tmp_path: Path, monkeypatch) -> None:
|
|
ds = datacms.resolve("hospital_enrollments", _catalog())
|
|
|
|
class Stream:
|
|
def __init__(self, *a, **k): # noqa: ANN002, ANN003
|
|
pass
|
|
|
|
def __enter__(self): # noqa: ANN204
|
|
return self
|
|
|
|
def __exit__(self, *a): # noqa: ANN002, ANN204
|
|
return False
|
|
|
|
def raise_for_status(self) -> None:
|
|
pass
|
|
|
|
def iter_bytes(self, n): # noqa: ANN001, ANN202
|
|
yield b'"CCN","NPI"\n'
|
|
yield b"440058,1467408781\n"
|
|
|
|
monkeypatch.setattr(datacms.httpx, "stream", lambda *a, **k: Stream())
|
|
p = datacms.download(ds, tmp_path)
|
|
assert p.name == ds.filename and p.read_text().count("\n") == 2
|
|
# cached: the stream is not opened again
|
|
monkeypatch.setattr(
|
|
datacms.httpx,
|
|
"stream",
|
|
lambda *a, **k: (_ for _ in ()).throw(AssertionError("re-download")),
|
|
)
|
|
assert datacms.download(ds, tmp_path) == p
|
|
|
|
cost = datacms.resolve("hospital_provider_cost_report", _catalog())
|
|
pages = [
|
|
[{"Provider CCN": "010001", "Hospital Name": "A"}] * datacms.PAGE_SIZE,
|
|
[{"Provider CCN": "010002", "Hospital Name": "B"}],
|
|
]
|
|
|
|
class J:
|
|
def __init__(self, rows): # noqa: ANN001
|
|
self.rows = rows
|
|
|
|
def json(self): # noqa: ANN202
|
|
return self.rows
|
|
|
|
seen = []
|
|
|
|
def fake_get(url, **kw): # noqa: ANN001, ANN003, ANN202
|
|
seen.append(kw["params"]["offset"])
|
|
return J(pages.pop(0) if pages else [])
|
|
|
|
monkeypatch.setattr(datacms, "_get", fake_get)
|
|
p = datacms.download(cost, tmp_path)
|
|
assert seen == [0, datacms.PAGE_SIZE]
|
|
lines = p.read_text().splitlines()
|
|
assert (
|
|
lines[0] == "Provider CCN,Hospital Name" and len(lines) == datacms.PAGE_SIZE + 2
|
|
)
|
|
|
|
|
|
def test_get_retries(monkeypatch) -> None:
|
|
attempts = []
|
|
|
|
def fake(url, **kw): # noqa: ANN001, ANN003, ANN202
|
|
attempts.append(url)
|
|
if len(attempts) == 1:
|
|
raise httpx.ConnectError("boom")
|
|
if len(attempts) == 2:
|
|
return httpx.Response(503, request=httpx.Request("GET", url))
|
|
return httpx.Response(200, json={"ok": 1}, request=httpx.Request("GET", url))
|
|
|
|
monkeypatch.setattr(datacms.httpx, "get", fake)
|
|
monkeypatch.setattr(datacms.time, "sleep", lambda s: None)
|
|
assert datacms._get("https://x").json() == {"ok": 1} and len(attempts) == 3
|