Some checks failed
CI / skinny-install (aco) (push) Successful in 1m12s
CI / skinny-install (api) (push) Successful in 30s
CI / skinny-install (bcda) (push) Successful in 36s
CI / skinny-install (bib) (push) Successful in 35s
CI / skinny-install (bls) (push) Successful in 27s
CI / skinny-install (ccw) (push) Successful in 32s
CI / skinny-install (cli) (push) Successful in 41s
CI / skinny-install (cms) (push) Successful in 37s
CI / skinny-install (conf) (push) Successful in 38s
CI / skinny-install (opps) (push) Successful in 33s
CI / skinny-install (perf) (push) Successful in 38s
CI / skinny-install (pfs) (push) Successful in 38s
CI / skinny-install (rex) (push) Successful in 34s
Deploy / build-scan-report (push) Failing after 46s
Infra CI / notebooks (push) Failing after 25s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Failing after 16s
CI / lint-test (push) Failing after 11m2s
Infra CI / mc (push) Successful in 21s
Infra CI / api (push) Successful in 29s
Package Supply Chain / pkg-supply-chain (push) Failing after 41s
Mail: Maddy on DO (corwins.media+Resend, fhirworx.io+Postmark), touchless/stateless/idempotent. Gitea SMTP via env_file. CMS inbox at cmsupdates@mail.fhirworx.io with IMAP→bib poller. Bib: regulations.gov v4 client, Federal Register discovery, 164K comment backfill (running), IMAP email ingest, Zotero sync routing. PRISMA: altcha PoW solver, CrossRef DOI resolution, 83/129 PDFs. Zotero: schema parity, ops module, CLI, fail-fast guard. CI: docs.Dockerfile COPY glob fix (tracks #341). Infra: Gitea+marimo fhirworx themes, IOM/OIG modules.
875 lines
28 KiB
Python
875 lines
28 KiB
Python
"""Tests for pfs.pipe helper functions and readers (lines 28-547).
|
|
|
|
Covers column map dicts, Zotero discovery, Excel/CSV readers,
|
|
encoding detection, column normalization, and file type detectors.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import polars as pl
|
|
import pytest
|
|
|
|
# ── openpyxl fake module ─────────────────────────────────────────────────────
|
|
# openpyxl is not installed in the test env, but pfs.pipe imports it lazily
|
|
# inside _find_header_row and _read_pprrvu. We inject a fake module into
|
|
# sys.modules so that `import openpyxl` succeeds and `patch()` can resolve it.
|
|
|
|
_fake_openpyxl = types.ModuleType("openpyxl")
|
|
_fake_openpyxl.load_workbook = MagicMock() # type: ignore[attr-defined]
|
|
|
|
# ── 1. Column map dicts (importing covers the lines) ────────────────────────
|
|
|
|
|
|
def test_rvu_columns_import():
|
|
from pfs.pipe import _RVU_COLUMNS
|
|
|
|
assert isinstance(_RVU_COLUMNS, dict)
|
|
assert _RVU_COLUMNS["HCPCS"] == "hcpcs"
|
|
assert _RVU_COLUMNS["WORK RVU"] == "work_rvu"
|
|
|
|
|
|
def test_gpci_columns_import():
|
|
from pfs.pipe import _GPCI_COLUMNS
|
|
|
|
assert isinstance(_GPCI_COLUMNS, dict)
|
|
assert _GPCI_COLUMNS["LOCALITY"] == "locality"
|
|
|
|
|
|
def test_labor_columns_import():
|
|
from pfs.pipe import _LABOR_COLUMNS
|
|
|
|
assert isinstance(_LABOR_COLUMNS, dict)
|
|
assert _LABOR_COLUMNS["hcpcs"] == "hcpcs"
|
|
assert _LABOR_COLUMNS["labor_code"] == "activity_code"
|
|
|
|
|
|
def test_supply_columns_import():
|
|
from pfs.pipe import _SUPPLY_COLUMNS
|
|
|
|
assert isinstance(_SUPPLY_COLUMNS, dict)
|
|
assert _SUPPLY_COLUMNS["cms_code"] == "supply_code"
|
|
|
|
|
|
def test_equipment_columns_import():
|
|
from pfs.pipe import _EQUIPMENT_COLUMNS
|
|
|
|
assert isinstance(_EQUIPMENT_COLUMNS, dict)
|
|
assert _EQUIPMENT_COLUMNS["cms_code"] == "equipment_code"
|
|
|
|
|
|
def test_work_time_columns_import():
|
|
from pfs.pipe import _WORK_TIME_COLUMNS
|
|
|
|
assert isinstance(_WORK_TIME_COLUMNS, dict)
|
|
assert _WORK_TIME_COLUMNS["cpt_code"] == "hcpcs"
|
|
|
|
|
|
# ── 2. _zotero_pfs_files ────────────────────────────────────────────────────
|
|
|
|
|
|
def _create_zotero_db(db_path: Path) -> None:
|
|
"""Create an in-memory-compatible Zotero SQLite schema at db_path."""
|
|
con = sqlite3.connect(str(db_path))
|
|
con.executescript(
|
|
"""
|
|
CREATE TABLE items (itemID INTEGER PRIMARY KEY, key TEXT);
|
|
CREATE TABLE tags (tagID INTEGER PRIMARY KEY, name TEXT);
|
|
CREATE TABLE itemTags (itemID INTEGER, tagID INTEGER);
|
|
CREATE TABLE fields (fieldID INTEGER PRIMARY KEY, fieldName TEXT);
|
|
CREATE TABLE itemData (itemID INTEGER, fieldID INTEGER, valueID INTEGER);
|
|
CREATE TABLE itemDataValues (valueID INTEGER PRIMARY KEY, value TEXT);
|
|
CREATE TABLE itemAttachments (
|
|
itemID INTEGER, parentItemID INTEGER, path TEXT
|
|
);
|
|
"""
|
|
)
|
|
con.close()
|
|
|
|
|
|
def _populate_basic_item(
|
|
db_path: Path,
|
|
*,
|
|
item_id: int = 1,
|
|
item_key: str = "ABCD1234",
|
|
year: int = 2026,
|
|
title: str = "CY 2026 PFS Final Rule",
|
|
att_id: int = 100,
|
|
att_key: str = "STOR0001",
|
|
filename: str = "Addendum_B_2026.xlsx",
|
|
) -> None:
|
|
"""Insert a single parent item + attachment into Zotero DB."""
|
|
con = sqlite3.connect(str(db_path))
|
|
# Parent item
|
|
con.execute("INSERT INTO items VALUES (?, ?)", (item_id, item_key))
|
|
# module:pfs tag
|
|
con.execute("INSERT INTO tags VALUES (1, 'module:pfs')")
|
|
con.execute("INSERT INTO itemTags VALUES (?, 1)", (item_id,))
|
|
# year tag
|
|
con.execute("INSERT INTO tags VALUES (2, ?)", (f"year:{year}",))
|
|
con.execute("INSERT INTO itemTags VALUES (?, 2)", (item_id,))
|
|
# title field
|
|
con.execute("INSERT INTO fields VALUES (1, 'title')")
|
|
con.execute("INSERT INTO itemDataValues VALUES (1, ?)", (title,))
|
|
con.execute("INSERT INTO itemData VALUES (?, 1, 1)", (item_id,))
|
|
# Attachment item (for storage key lookup)
|
|
con.execute("INSERT INTO items VALUES (?, ?)", (att_id, att_key))
|
|
# Attachment record
|
|
con.execute(
|
|
"INSERT INTO itemAttachments VALUES (?, ?, ?)",
|
|
(att_id, item_id, f"storage:{filename}"),
|
|
)
|
|
con.commit()
|
|
con.close()
|
|
|
|
|
|
def test_zotero_pfs_files_basic(tmp_path: Path):
|
|
"""Basic discovery: one parent item with one xlsx attachment."""
|
|
from pfs.pipe import _zotero_pfs_files
|
|
|
|
db_path = tmp_path / "zotero.sqlite"
|
|
storage = tmp_path / "storage"
|
|
_create_zotero_db(db_path)
|
|
_populate_basic_item(db_path)
|
|
|
|
# Create the file on disk
|
|
file_dir = storage / "STOR0001"
|
|
file_dir.mkdir(parents=True)
|
|
(file_dir / "Addendum_B_2026.xlsx").write_bytes(b"fake")
|
|
|
|
results = _zotero_pfs_files(db_path, storage)
|
|
assert len(results) == 1
|
|
r = results[0]
|
|
assert r["item_key"] == "ABCD1234"
|
|
assert r["year"] == 2026
|
|
assert r["title"] == "CY 2026 PFS Final Rule"
|
|
assert r["filename"] == "Addendum_B_2026.xlsx"
|
|
assert r["ext"] == ".xlsx"
|
|
assert "STOR0001" in r["path"]
|
|
|
|
|
|
def test_zotero_pfs_files_macos_resource_fork_skip(tmp_path: Path):
|
|
"""Files starting with ._ (macOS resource forks) are skipped."""
|
|
from pfs.pipe import _zotero_pfs_files
|
|
|
|
db_path = tmp_path / "zotero.sqlite"
|
|
storage = tmp_path / "storage"
|
|
_create_zotero_db(db_path)
|
|
_populate_basic_item(db_path, filename="._Addendum_B.xlsx")
|
|
|
|
# Create the file on disk
|
|
file_dir = storage / "STOR0001"
|
|
file_dir.mkdir(parents=True)
|
|
(file_dir / "._Addendum_B.xlsx").write_bytes(b"fake")
|
|
|
|
results = _zotero_pfs_files(db_path, storage)
|
|
assert len(results) == 0
|
|
|
|
|
|
def test_zotero_pfs_files_missing_storage_key(tmp_path: Path):
|
|
"""Attachment whose itemID has no items row is skipped."""
|
|
from pfs.pipe import _zotero_pfs_files
|
|
|
|
db_path = tmp_path / "zotero.sqlite"
|
|
storage = tmp_path / "storage"
|
|
_create_zotero_db(db_path)
|
|
|
|
con = sqlite3.connect(str(db_path))
|
|
# Parent item
|
|
con.execute("INSERT INTO items VALUES (1, 'KEY1')")
|
|
con.execute("INSERT INTO tags VALUES (1, 'module:pfs')")
|
|
con.execute("INSERT INTO itemTags VALUES (1, 1)")
|
|
# Attachment record pointing to att_id=100, but NO items row for 100
|
|
con.execute("INSERT INTO itemAttachments VALUES (100, 1, 'storage:test.xlsx')")
|
|
con.commit()
|
|
con.close()
|
|
|
|
results = _zotero_pfs_files(db_path, storage)
|
|
assert len(results) == 0
|
|
|
|
|
|
def test_zotero_pfs_files_file_not_exists(tmp_path: Path):
|
|
"""Attachment whose file doesn't exist on disk is skipped."""
|
|
from pfs.pipe import _zotero_pfs_files
|
|
|
|
db_path = tmp_path / "zotero.sqlite"
|
|
storage = tmp_path / "storage"
|
|
_create_zotero_db(db_path)
|
|
_populate_basic_item(db_path)
|
|
# Do NOT create the file on disk
|
|
results = _zotero_pfs_files(db_path, storage)
|
|
assert len(results) == 0
|
|
|
|
|
|
def test_zotero_pfs_files_no_year_tag(tmp_path: Path):
|
|
"""Item with no year: tag gets year=0."""
|
|
from pfs.pipe import _zotero_pfs_files
|
|
|
|
db_path = tmp_path / "zotero.sqlite"
|
|
storage = tmp_path / "storage"
|
|
_create_zotero_db(db_path)
|
|
|
|
con = sqlite3.connect(str(db_path))
|
|
con.execute("INSERT INTO items VALUES (1, 'KEY1')")
|
|
con.execute("INSERT INTO tags VALUES (1, 'module:pfs')")
|
|
con.execute("INSERT INTO itemTags VALUES (1, 1)")
|
|
# No year tag — only module:pfs
|
|
# title
|
|
con.execute("INSERT INTO fields VALUES (1, 'title')")
|
|
con.execute("INSERT INTO itemDataValues VALUES (1, 'Some Title')")
|
|
con.execute("INSERT INTO itemData VALUES (1, 1, 1)")
|
|
# attachment
|
|
con.execute("INSERT INTO items VALUES (100, 'STOR0002')")
|
|
con.execute("INSERT INTO itemAttachments VALUES (100, 1, 'storage:data.csv')")
|
|
con.commit()
|
|
con.close()
|
|
|
|
file_dir = storage / "STOR0002"
|
|
file_dir.mkdir(parents=True)
|
|
(file_dir / "data.csv").write_bytes(b"a,b\n1,2\n")
|
|
|
|
results = _zotero_pfs_files(db_path, storage)
|
|
assert len(results) == 1
|
|
assert results[0]["year"] == 0
|
|
assert results[0]["title"] == "Some Title"
|
|
|
|
|
|
def test_zotero_pfs_files_no_title(tmp_path: Path):
|
|
"""Item with no title field gets empty string."""
|
|
from pfs.pipe import _zotero_pfs_files
|
|
|
|
db_path = tmp_path / "zotero.sqlite"
|
|
storage = tmp_path / "storage"
|
|
_create_zotero_db(db_path)
|
|
|
|
con = sqlite3.connect(str(db_path))
|
|
con.execute("INSERT INTO items VALUES (1, 'KEY1')")
|
|
con.execute("INSERT INTO tags VALUES (1, 'module:pfs')")
|
|
con.execute("INSERT INTO itemTags VALUES (1, 1)")
|
|
# No title data at all
|
|
# attachment
|
|
con.execute("INSERT INTO items VALUES (100, 'STOR0003')")
|
|
con.execute("INSERT INTO itemAttachments VALUES (100, 1, 'storage:data.txt')")
|
|
con.commit()
|
|
con.close()
|
|
|
|
file_dir = storage / "STOR0003"
|
|
file_dir.mkdir(parents=True)
|
|
(file_dir / "data.txt").write_bytes(b"col1\tcol2\n1\t2\n")
|
|
|
|
results = _zotero_pfs_files(db_path, storage)
|
|
assert len(results) == 1
|
|
assert results[0]["title"] == ""
|
|
assert results[0]["year"] == 0
|
|
|
|
|
|
# ── 3. _find_header_row ─────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture(autouse=False)
|
|
def _inject_openpyxl():
|
|
"""Temporarily place the fake openpyxl module into sys.modules."""
|
|
prev = sys.modules.get("openpyxl")
|
|
sys.modules["openpyxl"] = _fake_openpyxl
|
|
yield
|
|
if prev is None:
|
|
sys.modules.pop("openpyxl", None)
|
|
else:
|
|
sys.modules["openpyxl"] = prev
|
|
|
|
|
|
def test_find_header_row_found(_inject_openpyxl):
|
|
"""Header row detected when a row has >= min_cols short string cells."""
|
|
from pfs.pipe import _find_header_row
|
|
|
|
mock_ws = MagicMock()
|
|
mock_ws.iter_rows.return_value = [
|
|
("Copyright 2026 CMS", None, None, None, None),
|
|
(None, None, None, None, None),
|
|
("HCPCS", "MOD", "DESC", "STATUS", "WORK RVU", "PE RVU"),
|
|
]
|
|
mock_wb = MagicMock()
|
|
mock_wb.active = mock_ws
|
|
|
|
with patch.object(_fake_openpyxl, "load_workbook", return_value=mock_wb):
|
|
result = _find_header_row("fake.xlsx", min_cols=5)
|
|
assert result == 2
|
|
mock_wb.close.assert_called_once()
|
|
|
|
|
|
def test_find_header_row_not_found(_inject_openpyxl):
|
|
"""Returns 0 when no row matches the header criteria."""
|
|
from pfs.pipe import _find_header_row
|
|
|
|
mock_ws = MagicMock()
|
|
mock_ws.iter_rows.return_value = [
|
|
(1, 2, 3, 4, 5),
|
|
(None, None, None, None, None),
|
|
]
|
|
mock_wb = MagicMock()
|
|
mock_wb.active = mock_ws
|
|
|
|
with patch.object(_fake_openpyxl, "load_workbook", return_value=mock_wb):
|
|
result = _find_header_row("fake.xlsx")
|
|
assert result == 0
|
|
mock_wb.close.assert_called_once()
|
|
|
|
|
|
# ── 4. _read_excel ──────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_read_excel_auto_header():
|
|
"""_read_excel with no explicit header calls _find_header_row."""
|
|
from pfs.pipe import _read_excel
|
|
|
|
fake_df = pl.DataFrame({"a": [1]})
|
|
with (
|
|
patch("pfs.pipe._find_header_row", return_value=3) as mock_find,
|
|
patch("polars.read_excel", return_value=fake_df) as mock_read,
|
|
):
|
|
result = _read_excel("test.xlsx")
|
|
mock_find.assert_called_once_with("test.xlsx")
|
|
mock_read.assert_called_once_with("test.xlsx", read_options={"header_row": 3})
|
|
assert result.shape == (1, 1)
|
|
|
|
|
|
def test_read_excel_explicit_header():
|
|
"""_read_excel with explicit header_row skips _find_header_row."""
|
|
from pfs.pipe import _read_excel
|
|
|
|
fake_df = pl.DataFrame({"b": [2]})
|
|
with (
|
|
patch("pfs.pipe._find_header_row") as mock_find,
|
|
patch("polars.read_excel", return_value=fake_df) as mock_read,
|
|
):
|
|
result = _read_excel("test.xlsx", header_row=5)
|
|
mock_find.assert_not_called()
|
|
mock_read.assert_called_once_with("test.xlsx", read_options={"header_row": 5})
|
|
assert result.shape == (1, 1)
|
|
|
|
|
|
# ── 5. _read_pprrvu ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_read_pprrvu_positional_rename(_inject_openpyxl):
|
|
"""PPRRVU reader does positional rename and drops internal cols."""
|
|
from pfs.pipe import _read_pprrvu
|
|
|
|
# Build a mock workbook that has HCPCS at row 5
|
|
mock_ws = MagicMock()
|
|
rows = []
|
|
for _ in range(5):
|
|
rows.append(("title row", None, None))
|
|
# Row 5: starts with HCPCS
|
|
rows.append(("HCPCS", "MOD", "DESCRIPTION"))
|
|
mock_ws.iter_rows.return_value = rows
|
|
mock_wb = MagicMock()
|
|
mock_wb.active = mock_ws
|
|
|
|
# Build a DataFrame with enough columns for positional rename
|
|
# 32 columns matching expected_order length
|
|
col_names = [f"col{i}" for i in range(32)]
|
|
data = {c: ["val"] for c in col_names}
|
|
fake_df = pl.DataFrame(data)
|
|
|
|
with (
|
|
patch.object(_fake_openpyxl, "load_workbook", return_value=mock_wb),
|
|
patch("polars.read_excel", return_value=fake_df),
|
|
):
|
|
result = _read_pprrvu("fake.xlsx")
|
|
|
|
# Internal cols (_nf_na_indicator, _f_na_indicator, _pric_ind) dropped
|
|
assert "_nf_na_indicator" not in result.columns
|
|
assert "_f_na_indicator" not in result.columns
|
|
assert "_pric_ind" not in result.columns
|
|
# Public cols present
|
|
assert "hcpcs" in result.columns
|
|
assert "mod" in result.columns
|
|
assert "description" in result.columns
|
|
|
|
|
|
def test_read_pprrvu_too_few_columns(_inject_openpyxl):
|
|
"""PPRRVU with fewer columns than expected returns df unchanged.
|
|
|
|
Real PPRRVU files are 31 (CY2015-2025) or 32 (CY2026+) columns.
|
|
Anything less triggers a warning and the original df falls through
|
|
so a later pass (_normalize_columns) can try name-based renaming.
|
|
"""
|
|
from pfs.pipe import _read_pprrvu
|
|
|
|
mock_ws = MagicMock()
|
|
mock_ws.iter_rows.return_value = [
|
|
("HCPCS", "MOD"),
|
|
]
|
|
mock_wb = MagicMock()
|
|
mock_wb.active = mock_ws
|
|
|
|
# Only 5 columns — well below the 31/32 positional minimum
|
|
fake_df = pl.DataFrame({"A": ["x"], "B": ["y"], "C": ["z"], "D": ["w"], "E": ["v"]})
|
|
|
|
with (
|
|
patch.object(_fake_openpyxl, "load_workbook", return_value=mock_wb),
|
|
patch("polars.read_excel", return_value=fake_df),
|
|
):
|
|
result = _read_pprrvu("fake.xlsx")
|
|
|
|
# No rename applied — original columns preserved
|
|
assert list(result.columns) == ["A", "B", "C", "D", "E"]
|
|
|
|
|
|
def test_read_pprrvu_no_hcpcs_row(_inject_openpyxl):
|
|
"""PPRRVU with no 'HCPCS' row uses row 0."""
|
|
from pfs.pipe import _read_pprrvu
|
|
|
|
mock_ws = MagicMock()
|
|
mock_ws.iter_rows.return_value = [
|
|
("Title", None, None),
|
|
("Other", "stuff", "here"),
|
|
]
|
|
mock_wb = MagicMock()
|
|
mock_wb.active = mock_ws
|
|
|
|
fake_df = pl.DataFrame({"A": ["x"]})
|
|
|
|
with (
|
|
patch.object(_fake_openpyxl, "load_workbook", return_value=mock_wb),
|
|
patch("polars.read_excel", return_value=fake_df) as mock_read,
|
|
):
|
|
_read_pprrvu("fake.xlsx")
|
|
|
|
# hcpcs_row stays 0 since no row starts with "HCPCS". Reader now
|
|
# pins engine='calamine' because xlsx2csv silently ignored
|
|
# read_options['header_row'] and returned a single-column title
|
|
# DataFrame — see pfs.pipe._read_pprrvu docstring.
|
|
mock_read.assert_called_once_with(
|
|
"fake.xlsx",
|
|
engine="calamine",
|
|
read_options={"header_row": 0},
|
|
)
|
|
|
|
|
|
# ── 6. _read_csv_with_header ────────────────────────────────────────────────
|
|
|
|
|
|
def test_read_csv_with_header_skips_title_rows(tmp_path: Path):
|
|
"""Title rows before the actual header are skipped."""
|
|
from pfs.pipe import _read_csv_with_header
|
|
|
|
csv_file = tmp_path / "rvu.csv"
|
|
csv_file.write_text(
|
|
"CY 2026 Physician Fee Schedule\n"
|
|
"Published January 2026\n"
|
|
"HCPCS,MOD,DESCRIPTION,STATUS,WORK_RVU\n"
|
|
"99213, ,Office Visit,A,0.97\n"
|
|
"99214, ,Office Visit,A,1.50\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = _read_csv_with_header(str(csv_file))
|
|
assert "HCPCS" in result.columns
|
|
assert result.shape[0] == 2
|
|
|
|
|
|
def test_read_csv_with_header_no_valid_header(tmp_path: Path):
|
|
"""When no row looks like a header, i falls through to 0."""
|
|
from pfs.pipe import _read_csv_with_header
|
|
|
|
csv_file = tmp_path / "numbers.csv"
|
|
# All rows have only numeric or empty cells — no alpha
|
|
csv_file.write_text(
|
|
"1,2,3\n4,5,6\n7,8,9\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = _read_csv_with_header(str(csv_file))
|
|
# Falls through to i=0, reads from start
|
|
assert result.shape[0] >= 2
|
|
|
|
|
|
# ── 7. _detect_encoding ─────────────────────────────────────────────────────
|
|
|
|
|
|
def test_detect_encoding_utf16_le(tmp_path: Path):
|
|
from pfs.pipe import _detect_encoding
|
|
|
|
f = tmp_path / "le.txt"
|
|
f.write_bytes(b"\xff\xfe" + "hello".encode("utf-16-le"))
|
|
assert _detect_encoding(str(f)) == "utf-16"
|
|
|
|
|
|
def test_detect_encoding_utf16_be(tmp_path: Path):
|
|
from pfs.pipe import _detect_encoding
|
|
|
|
f = tmp_path / "be.txt"
|
|
f.write_bytes(b"\xfe\xff" + "hello".encode("utf-16-be"))
|
|
assert _detect_encoding(str(f)) == "utf-16"
|
|
|
|
|
|
def test_detect_encoding_utf8(tmp_path: Path):
|
|
from pfs.pipe import _detect_encoding
|
|
|
|
f = tmp_path / "utf8.txt"
|
|
f.write_bytes(b"hello world")
|
|
assert _detect_encoding(str(f)) == "utf-8-sig"
|
|
|
|
|
|
# ── 8. _read_tsv ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_read_tsv_basic(tmp_path: Path):
|
|
from pfs.pipe import _read_tsv
|
|
|
|
tsv = tmp_path / "data.txt"
|
|
tsv.write_text(
|
|
"Title line ignored\n"
|
|
"HCPCS\tMOD\tDESCRIPTION\tSTATUS\n"
|
|
"99213\t\tOffice Visit\tA\n"
|
|
"99214\t\tOffice Visit\tA\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = _read_tsv(str(tsv))
|
|
assert "HCPCS" in result.columns
|
|
assert result.shape[0] == 2
|
|
|
|
|
|
def test_read_tsv_utf16(tmp_path: Path):
|
|
"""UTF-16 LE encoded TSV is handled correctly."""
|
|
from pfs.pipe import _read_tsv
|
|
|
|
content = "Title line\nCOL_A\tCOL_B\tCOL_C\nval1\tval2\tval3\n"
|
|
tsv = tmp_path / "utf16.txt"
|
|
tsv.write_bytes(content.encode("utf-16"))
|
|
|
|
result = _read_tsv(str(tsv))
|
|
# Should find header and parse
|
|
assert result.shape[0] >= 1
|
|
|
|
|
|
def test_read_tsv_no_title_rows(tmp_path: Path):
|
|
"""TSV where the first row IS the header."""
|
|
from pfs.pipe import _read_tsv
|
|
|
|
tsv = tmp_path / "clean.txt"
|
|
tsv.write_text(
|
|
"ALPHA\tBETA\tGAMMA\n1\t2\t3\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = _read_tsv(str(tsv))
|
|
assert "ALPHA" in result.columns
|
|
assert result.shape[0] == 1
|
|
|
|
|
|
# ── 9. _normalize_columns ───────────────────────────────────────────────────
|
|
|
|
|
|
def test_normalize_columns_rename():
|
|
"""Columns are renamed via case-insensitive lookup."""
|
|
from pfs.pipe import _normalize_columns
|
|
|
|
df = pl.DataFrame({"HCPCS": ["99213"], "MOD": [""], "DESCRIPTION": ["x"]})
|
|
col_map = {"HCPCS": "hcpcs", "MOD": "mod", "DESCRIPTION": "description"}
|
|
result = _normalize_columns(df, col_map)
|
|
assert list(result.columns) == ["hcpcs", "mod", "description"]
|
|
|
|
|
|
def test_normalize_columns_drops_unmapped():
|
|
"""Columns not in the map are dropped."""
|
|
from pfs.pipe import _normalize_columns
|
|
|
|
df = pl.DataFrame({"HCPCS": ["99213"], "EXTRA_COL": ["x"], "MOD": [""]})
|
|
col_map = {"HCPCS": "hcpcs", "MOD": "mod"}
|
|
result = _normalize_columns(df, col_map)
|
|
assert "EXTRA_COL" not in result.columns
|
|
assert list(result.columns) == ["hcpcs", "mod"]
|
|
|
|
|
|
def test_normalize_columns_internal_col_dropped():
|
|
"""Columns mapped to _internal names are renamed then dropped."""
|
|
from pfs.pipe import _normalize_columns
|
|
|
|
df = pl.DataFrame({"hcpcs": ["99213"], "source": ["cms"], "global_period": ["10"]})
|
|
col_map = {
|
|
"hcpcs": "hcpcs",
|
|
"source": "_source",
|
|
"global_period": "_global_period",
|
|
}
|
|
result = _normalize_columns(df, col_map)
|
|
assert "_source" not in result.columns
|
|
assert "_global_period" not in result.columns
|
|
assert "hcpcs" in result.columns
|
|
|
|
|
|
def test_normalize_columns_no_matches():
|
|
"""When no columns match, returns original df unchanged."""
|
|
from pfs.pipe import _normalize_columns
|
|
|
|
df = pl.DataFrame({"UNKNOWN_A": [1], "UNKNOWN_B": [2]})
|
|
col_map = {"HCPCS": "hcpcs"}
|
|
result = _normalize_columns(df, col_map)
|
|
# No matches → keep is empty → returns df as-is
|
|
assert list(result.columns) == ["UNKNOWN_A", "UNKNOWN_B"]
|
|
|
|
|
|
def test_normalize_columns_newline_normalization():
|
|
"""Column names with embedded newlines are normalized before lookup."""
|
|
from pfs.pipe import _normalize_columns
|
|
|
|
df = pl.DataFrame({"WORK\nRVU": [0.97], "MP\nRVU": [0.07]})
|
|
col_map = {"WORK RVU": "work_rvu", "MP RVU": "mp_rvu"}
|
|
result = _normalize_columns(df, col_map)
|
|
assert "work_rvu" in result.columns
|
|
assert "mp_rvu" in result.columns
|
|
|
|
|
|
def test_normalize_columns_case_insensitive():
|
|
"""Lookup is case-insensitive."""
|
|
from pfs.pipe import _normalize_columns
|
|
|
|
df = pl.DataFrame({"hcpcs": ["99213"], "mod": [""]})
|
|
col_map = {"HCPCS": "hcpcs", "MOD": "mod"}
|
|
result = _normalize_columns(df, col_map)
|
|
assert list(result.columns) == ["hcpcs", "mod"]
|
|
|
|
|
|
# ── 10. File type detectors ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestIsAddendumB:
|
|
def test_addendum_b_space(self):
|
|
from pfs.pipe import _is_addendum_b
|
|
|
|
assert _is_addendum_b("Addendum B 2026.xlsx") is True
|
|
|
|
def test_addendum_b_underscore(self):
|
|
from pfs.pipe import _is_addendum_b
|
|
|
|
assert _is_addendum_b("Addendum_B_2026.xlsx") is True
|
|
|
|
def test_addendum_b_startswith(self):
|
|
from pfs.pipe import _is_addendum_b
|
|
|
|
assert _is_addendum_b("ADDENDUM_B2026.xlsx") is True
|
|
|
|
def test_not_addendum_b(self):
|
|
from pfs.pipe import _is_addendum_b
|
|
|
|
assert _is_addendum_b("Addendum_E_2026.xlsx") is False
|
|
|
|
def test_not_addendum_at_all(self):
|
|
from pfs.pipe import _is_addendum_b
|
|
|
|
assert _is_addendum_b("GPCI_2026.xlsx") is False
|
|
|
|
|
|
class TestIsPprrvu:
|
|
def test_pprrvu(self):
|
|
from pfs.pipe import _is_pprrvu
|
|
|
|
assert _is_pprrvu("PPRRVU26A.xlsx") is True
|
|
|
|
def test_not_pprrvu(self):
|
|
from pfs.pipe import _is_pprrvu
|
|
|
|
assert _is_pprrvu("RVU26A.csv") is False
|
|
|
|
def test_pprrvu_lowercase(self):
|
|
from pfs.pipe import _is_pprrvu
|
|
|
|
assert _is_pprrvu("pprrvu26b.xlsx") is True
|
|
|
|
|
|
class TestIsGpciFile:
|
|
def test_gpci(self):
|
|
from pfs.pipe import _is_gpci_file
|
|
|
|
assert _is_gpci_file("GPCI_2026.csv") is True
|
|
|
|
def test_addendum_e_space(self):
|
|
from pfs.pipe import _is_gpci_file
|
|
|
|
assert _is_gpci_file("Addendum E 2026.xlsx") is True
|
|
|
|
def test_addendum_e_end_of_string(self):
|
|
from pfs.pipe import _is_gpci_file
|
|
|
|
assert _is_gpci_file("Addendum_E.xlsx") is True
|
|
|
|
def test_gpci_but_addendum_b(self):
|
|
from pfs.pipe import _is_gpci_file
|
|
|
|
assert _is_gpci_file("GPCI ADDENDUM B 2026.xlsx") is False
|
|
|
|
def test_not_gpci(self):
|
|
from pfs.pipe import _is_gpci_file
|
|
|
|
assert _is_gpci_file("RVU_2026.csv") is False
|
|
|
|
|
|
class TestIsPufLabor:
|
|
def test_puf_labor(self):
|
|
from pfs.pipe import _is_puf_labor
|
|
|
|
assert _is_puf_labor("PUF_LABOR_2026.csv") is True
|
|
|
|
def test_puf_labor_task_excluded(self):
|
|
from pfs.pipe import _is_puf_labor
|
|
|
|
assert _is_puf_labor("PUF_LABOR_TASK_2026.csv") is False
|
|
|
|
def test_puf_labor_old_excluded(self):
|
|
from pfs.pipe import _is_puf_labor
|
|
|
|
assert _is_puf_labor("PUF_LABOR_OLD.csv") is False
|
|
|
|
def test_not_puf_labor(self):
|
|
from pfs.pipe import _is_puf_labor
|
|
|
|
assert _is_puf_labor("PUF_SUPPLY.csv") is False
|
|
|
|
|
|
class TestIsPufSupply:
|
|
def test_puf_supply(self):
|
|
from pfs.pipe import _is_puf_supply
|
|
|
|
assert _is_puf_supply("PUF_SUPPLY_2026.csv") is True
|
|
|
|
def test_not_puf_supply(self):
|
|
from pfs.pipe import _is_puf_supply
|
|
|
|
assert _is_puf_supply("PUF_LABOR.csv") is False
|
|
|
|
|
|
class TestIsPufEquip:
|
|
def test_puf_equip(self):
|
|
from pfs.pipe import _is_puf_equip
|
|
|
|
assert _is_puf_equip("PUF_EQUIP_2026.csv") is True
|
|
|
|
def test_puf_equipment(self):
|
|
from pfs.pipe import _is_puf_equip
|
|
|
|
assert _is_puf_equip("PUF_EQUIPMENT_2026.csv") is True
|
|
|
|
def test_not_puf_equip(self):
|
|
from pfs.pipe import _is_puf_equip
|
|
|
|
assert _is_puf_equip("PUF_LABOR.csv") is False
|
|
|
|
|
|
class TestIsWorkTime:
|
|
def test_work_time_underscore(self):
|
|
from pfs.pipe import _is_work_time
|
|
|
|
assert _is_work_time("WORK_TIME_2026.csv") is True
|
|
|
|
def test_work_time_space(self):
|
|
from pfs.pipe import _is_work_time
|
|
|
|
assert _is_work_time("Work Time 2026.csv") is True
|
|
|
|
def test_phystime(self):
|
|
from pfs.pipe import _is_work_time
|
|
|
|
assert _is_work_time("PHYSTIME2026.csv") is True
|
|
|
|
def test_not_work_time(self):
|
|
from pfs.pipe import _is_work_time
|
|
|
|
assert _is_work_time("RVU_2026.csv") is False
|
|
|
|
|
|
class TestIsCarrierFile:
|
|
def test_pfall(self):
|
|
from pfs.pipe import _is_carrier_file
|
|
|
|
assert _is_carrier_file("PFALL26.TXT") is True
|
|
|
|
def test_per_state_2_letters(self):
|
|
from pfs.pipe import _is_carrier_file
|
|
|
|
assert _is_carrier_file("PFNY26R.TXT") is True
|
|
|
|
def test_per_state_3_letters(self):
|
|
from pfs.pipe import _is_carrier_file
|
|
|
|
assert _is_carrier_file("PFPRV26.TXT") is True
|
|
|
|
def test_not_carrier(self):
|
|
from pfs.pipe import _is_carrier_file
|
|
|
|
assert _is_carrier_file("RVU26.TXT") is False
|
|
|
|
def test_not_txt(self):
|
|
from pfs.pipe import _is_carrier_file
|
|
|
|
assert _is_carrier_file("PFALL26.CSV") is False
|
|
|
|
def test_pf_one_letter(self):
|
|
"""PF with only 1 state letter does not match."""
|
|
from pfs.pipe import _is_carrier_file
|
|
|
|
assert _is_carrier_file("PFA26.TXT") is False
|
|
|
|
|
|
class TestIsZipCarrierFile:
|
|
def test_zip5(self):
|
|
from pfs.pipe import _is_zip_carrier_file
|
|
|
|
assert _is_zip_carrier_file("ZIP5_2026.TXT") is True
|
|
|
|
def test_zip5_layout_excluded(self):
|
|
from pfs.pipe import _is_zip_carrier_file
|
|
|
|
assert _is_zip_carrier_file("ZIP5_LYOUT.TXT") is False
|
|
|
|
def test_not_zip5(self):
|
|
from pfs.pipe import _is_zip_carrier_file
|
|
|
|
assert _is_zip_carrier_file("PFALL26.TXT") is False
|
|
|
|
def test_zip5_csv_not_matched(self):
|
|
from pfs.pipe import _is_zip_carrier_file
|
|
|
|
assert _is_zip_carrier_file("ZIP5_2026.CSV") is False
|
|
|
|
|
|
class TestExtractYearFromTitle:
|
|
def test_cy_year(self):
|
|
from pfs.pipe import _extract_year_from_title
|
|
|
|
assert _extract_year_from_title("CY 2026 PFS Final Rule") == 2026
|
|
|
|
def test_cy_no_space(self):
|
|
from pfs.pipe import _extract_year_from_title
|
|
|
|
assert _extract_year_from_title("CY2025 PFS Proposed") == 2025
|
|
|
|
def test_no_match(self):
|
|
from pfs.pipe import _extract_year_from_title
|
|
|
|
assert _extract_year_from_title("Physician Fee Schedule") is None
|
|
|
|
|
|
class TestIsFinalRule:
|
|
def test_final_capitalized(self):
|
|
from pfs.pipe import _is_final_rule
|
|
|
|
assert _is_final_rule("CY 2026 PFS Final Rule") is True
|
|
|
|
def test_final_lowercase(self):
|
|
from pfs.pipe import _is_final_rule
|
|
|
|
assert _is_final_rule("CY 2026 PFS final rule") is True
|
|
|
|
def test_proposed(self):
|
|
from pfs.pipe import _is_final_rule
|
|
|
|
assert _is_final_rule("CY 2026 PFS Proposed Rule") is False
|