write_cpt_edition is delete-then-insert per edition_year; writing an
empty parse would silently erase whatever real data is already on file
for that year. (parse_epub's own guard — raising when _chapter_groups
finds no chapter content at all — landed in the prior commit, since it
shares the same content_root refactor in pfs/cpt_epub.py.)
write_cpt_edition now refuses (raises ValueError) an edition with zero
codes unless force=True. cpt_load.ingest wraps each edition's parse +
write in try/except ValueError, surfaces the failure as
out[year] = {"error": <message>} and continues with the other
requested editions instead of aborting the whole run.
540 lines
17 KiB
Python
540 lines
17 KiB
Python
"""pfs.codetables — DDL + writers for the code element/event/family tables."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import duckdb
|
|
import pytest
|
|
|
|
from pfs.codetables import (
|
|
ElementRow,
|
|
EventRow,
|
|
FamilyRow,
|
|
ReviewRow,
|
|
cpt_years,
|
|
ensure_tables,
|
|
is_missing_table_error,
|
|
read_all_elements,
|
|
read_all_events,
|
|
read_cpt_codes,
|
|
read_cpt_instructions,
|
|
read_cpt_sections,
|
|
read_elements,
|
|
read_events,
|
|
read_families,
|
|
write_cpt_edition,
|
|
write_elements,
|
|
write_events,
|
|
write_families,
|
|
)
|
|
from pfs.cpt_model import (
|
|
CptAlternate,
|
|
CptCode,
|
|
CptCrosswalk,
|
|
CptEdition,
|
|
CptInstruction,
|
|
CptListEntry,
|
|
CptReference,
|
|
CptSection,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def con():
|
|
c = duckdb.connect(":memory:")
|
|
ensure_tables(c)
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
def _el(code="99490", value="consent", year=2015):
|
|
return ElementRow(
|
|
code, year, "activity", value, "", "Consent;", "DE2VH9PD", 1245, 67716, "fr"
|
|
)
|
|
|
|
|
|
class TestIsMissingTableError:
|
|
def test_true_for_a_real_duckdb_catalog_error(self, con):
|
|
with pytest.raises(Exception) as exc_info:
|
|
con.execute("SELECT * FROM pfs.nope")
|
|
assert is_missing_table_error(exc_info.value)
|
|
|
|
def test_false_when_only_catalog_present(self):
|
|
assert not is_missing_table_error(Exception("Catalog Error: something else"))
|
|
|
|
def test_false_when_only_does_not_exist_present(self):
|
|
# A message that happens to say "does not exist" for an unrelated
|
|
# reason (not a Catalog error) must not be treated as "missing
|
|
# table, just nothing derived yet" — that would swallow a real bug.
|
|
assert not is_missing_table_error(Exception("file does not exist"))
|
|
|
|
def test_true_when_both_substrings_present(self):
|
|
assert is_missing_table_error(
|
|
Exception('Catalog Error: Table "pfs.code_family" does not exist')
|
|
)
|
|
|
|
|
|
class TestDDL:
|
|
def test_idempotent(self, con):
|
|
ensure_tables(con)
|
|
names = {
|
|
r[0]
|
|
for r in con.execute(
|
|
"SELECT table_name FROM information_schema.tables WHERE table_schema='pfs'"
|
|
).fetchall()
|
|
}
|
|
assert {
|
|
"code_element",
|
|
"code_element_review",
|
|
"code_event",
|
|
"code_family",
|
|
"cpt_section",
|
|
"cpt_code",
|
|
"cpt_instruction",
|
|
"cpt_reference",
|
|
"cpt_crosswalk",
|
|
"cpt_list",
|
|
"cpt_code_alt",
|
|
} <= names
|
|
|
|
|
|
class TestElements:
|
|
def test_write_replaces_per_code(self, con):
|
|
assert (
|
|
write_elements(
|
|
con,
|
|
"99490",
|
|
[_el(), _el(value="24-7-access")],
|
|
[ReviewRow("99490", "odd line", "activity", "", "DE2VH9PD", 1246)],
|
|
)
|
|
== 2
|
|
)
|
|
assert write_elements(con, "99490", [_el()], []) == 1
|
|
rows = read_elements(con, "99490")
|
|
assert [r.value for r in rows] == ["consent"]
|
|
assert (
|
|
con.execute(
|
|
"SELECT count(*) FROM pfs.code_element_review WHERE code='99490'"
|
|
).fetchone()[0]
|
|
== 0
|
|
)
|
|
|
|
def test_other_codes_untouched(self, con):
|
|
write_elements(con, "99490", [_el()], [])
|
|
write_elements(con, "G0556", [_el(code="G0556")], [])
|
|
write_elements(con, "99490", [], [])
|
|
assert read_elements(con, "99490") == []
|
|
assert len(read_elements(con, "G0556")) == 1
|
|
|
|
|
|
class TestReadAll:
|
|
def test_read_all_elements_matches_per_code_reader(self, con):
|
|
write_elements(
|
|
con, "99490", [_el(value="24-7-access"), _el(value="consent")], []
|
|
)
|
|
write_elements(con, "G0556", [_el(code="G0556")], [])
|
|
out = read_all_elements(con)
|
|
assert set(out) == {"99490", "G0556"}
|
|
assert out["99490"] == read_elements(con, "99490")
|
|
assert out["G0556"] == read_elements(con, "G0556")
|
|
|
|
def test_read_all_elements_empty(self, con):
|
|
assert read_all_elements(con) == {}
|
|
|
|
def test_read_all_events_matches_per_code_reader(self, con):
|
|
rows = [
|
|
EventRow(
|
|
"99439",
|
|
2021,
|
|
"replaces",
|
|
"G2058",
|
|
"99439",
|
|
"YBM4IZUS",
|
|
1578,
|
|
84639,
|
|
"fr",
|
|
True,
|
|
"",
|
|
),
|
|
EventRow("99439", 2021, "appeared", "", "", "", 0, 0, "rvu", True, ""),
|
|
]
|
|
write_events(con, "99439", rows)
|
|
write_events(
|
|
con,
|
|
"G2058",
|
|
[EventRow("G2058", 2020, "appeared", "", "", "", 0, 0, "rvu", True, "")],
|
|
)
|
|
out = read_all_events(con)
|
|
assert set(out) == {"99439", "G2058"}
|
|
assert out["99439"] == read_events(con, "99439")
|
|
assert out["G2058"] == read_events(con, "G2058")
|
|
|
|
def test_read_all_events_empty(self, con):
|
|
assert read_all_events(con) == {}
|
|
|
|
|
|
class TestEvents:
|
|
def test_roundtrip_sorted_by_year(self, con):
|
|
rows = [
|
|
EventRow(
|
|
"99439",
|
|
2021,
|
|
"replaces",
|
|
"G2058",
|
|
"99439",
|
|
"YBM4IZUS",
|
|
1578,
|
|
84639,
|
|
"fr",
|
|
True,
|
|
"",
|
|
),
|
|
EventRow("99439", 2021, "appeared", "", "", "", 0, 0, "rvu", True, ""),
|
|
]
|
|
assert write_events(con, "99439", rows) == 2
|
|
got = read_events(con, "99439")
|
|
assert [e.kind for e in got] == ["appeared", "replaces"]
|
|
assert got[1].anchored is True
|
|
|
|
|
|
class TestFamilies:
|
|
def test_full_replace(self, con):
|
|
write_families(
|
|
con,
|
|
[
|
|
FamilyRow(
|
|
"CCM", "Chronic Care Management", "99490", "base", 2015, None, "", 0
|
|
)
|
|
],
|
|
)
|
|
write_families(
|
|
con,
|
|
[
|
|
FamilyRow(
|
|
"CCM",
|
|
"Chronic Care Management",
|
|
"99439",
|
|
"add-on",
|
|
2021,
|
|
None,
|
|
"YBM4IZUS",
|
|
1578,
|
|
)
|
|
],
|
|
)
|
|
fams = read_families(con)
|
|
assert len(fams) == 1 and fams[0].code == "99439" and fams[0].role == "add-on"
|
|
|
|
def test_note_round_trips(self, con):
|
|
# Ruling C1: `note` is the last field, defaulted, so a caller that
|
|
# omits it (as `test_full_replace` above does) still works — and
|
|
# a caller that sets it gets it back unchanged.
|
|
path_key = (
|
|
"Evaluation and Management > Care Management Services > "
|
|
"Chronic Care Management Services"
|
|
)
|
|
write_families(
|
|
con,
|
|
[
|
|
FamilyRow(
|
|
"CCM",
|
|
"Chronic Care Management",
|
|
"99490",
|
|
"base",
|
|
2015,
|
|
None,
|
|
"",
|
|
0,
|
|
path_key,
|
|
)
|
|
],
|
|
)
|
|
fams = read_families(con)
|
|
assert len(fams) == 1
|
|
assert fams[0].note == path_key
|
|
|
|
def test_note_defaults_to_empty_string(self, con):
|
|
write_families(
|
|
con,
|
|
[
|
|
FamilyRow(
|
|
"CCM", "Chronic Care Management", "99490", "base", 2015, None, "", 0
|
|
)
|
|
],
|
|
)
|
|
assert read_families(con)[0].note == ""
|
|
|
|
def test_ensure_tables_upgrades_a_pre_existing_table_without_note(self):
|
|
# A replica created before `note` existed has an 8-column
|
|
# pfs.code_family — ensure_tables must add the column in place,
|
|
# not require a drop/recreate (Ruling C1).
|
|
con = duckdb.connect(":memory:")
|
|
try:
|
|
con.execute("CREATE SCHEMA IF NOT EXISTS pfs;")
|
|
con.execute(
|
|
"CREATE TABLE pfs.code_family ("
|
|
"key VARCHAR, name VARCHAR, code VARCHAR, role VARCHAR, "
|
|
"since INTEGER, until INTEGER, item_key VARCHAR, p_id INTEGER)"
|
|
)
|
|
con.execute(
|
|
"INSERT INTO pfs.code_family VALUES "
|
|
"('CCM', 'Chronic Care Management', '99490', 'base', 2015, NULL, '', 0)"
|
|
)
|
|
ensure_tables(con)
|
|
cols = {
|
|
r[0]
|
|
for r in con.execute(
|
|
"SELECT column_name FROM information_schema.columns "
|
|
"WHERE table_schema='pfs' AND table_name='code_family'"
|
|
).fetchall()
|
|
}
|
|
assert "note" in cols
|
|
# The pre-existing row survives the upgrade with note = NULL,
|
|
# and a fresh write still round-trips.
|
|
row = read_families(con)[0]
|
|
assert row.code == "99490" and row.note is None
|
|
write_families(
|
|
con,
|
|
[
|
|
FamilyRow(
|
|
"CCM",
|
|
"Chronic Care Management",
|
|
"99491",
|
|
"base",
|
|
2015,
|
|
None,
|
|
"",
|
|
0,
|
|
"some path key",
|
|
)
|
|
],
|
|
)
|
|
assert read_families(con)[0].note == "some path key"
|
|
finally:
|
|
con.close()
|
|
|
|
|
|
def _tiny_edition(year=2024, codes=None, alternates=()):
|
|
sections = (
|
|
CptSection(
|
|
sec_id="sec_1",
|
|
level=2,
|
|
title="Chronic Care Management Services",
|
|
path=(
|
|
"Evaluation and Management",
|
|
"Care Management Services",
|
|
"Chronic Care Management Services",
|
|
),
|
|
code_lo="99490",
|
|
code_hi="99491",
|
|
guideline="Sample guideline text.",
|
|
),
|
|
)
|
|
if codes is None:
|
|
codes = (
|
|
CptCode(
|
|
code="99490",
|
|
sec_id="sec_1",
|
|
descriptor="Chronic care management services first 20 minutes.",
|
|
stem="Chronic care management services",
|
|
elements=("first element;",),
|
|
tail="first 20 minutes.",
|
|
addon=False,
|
|
resequenced=False,
|
|
new=False,
|
|
revised=False,
|
|
telemedicine=False,
|
|
parent="",
|
|
mod51_exempt=False,
|
|
audio_only=False,
|
|
fda_pending=False,
|
|
pla=False,
|
|
category="I",
|
|
),
|
|
CptCode(
|
|
code="99439",
|
|
sec_id="sec_1",
|
|
descriptor="each additional 20 minutes.",
|
|
stem="Chronic care management services",
|
|
elements=("first element;",),
|
|
tail="each additional 20 minutes.",
|
|
addon=True,
|
|
resequenced=False,
|
|
new=False,
|
|
revised=False,
|
|
telemedicine=False,
|
|
parent="99490",
|
|
mod51_exempt=False,
|
|
audio_only=False,
|
|
fda_pending=False,
|
|
pla=False,
|
|
category="I",
|
|
),
|
|
)
|
|
instructions = (
|
|
CptInstruction(
|
|
code="99439",
|
|
kind="use-with",
|
|
text="(Use 99439 in conjunction with 99490)",
|
|
targets=("99490",),
|
|
),
|
|
)
|
|
references = (
|
|
CptReference(
|
|
code="99490",
|
|
kind="cpt-changes",
|
|
years=(2015, 2022),
|
|
text="CPT Changes: An Insider's View 2015, 2022",
|
|
),
|
|
)
|
|
crosswalks = (
|
|
CptCrosswalk(
|
|
current_code="99490",
|
|
former_code="99091T",
|
|
year_deleted="2022",
|
|
citations="CPT Changes 2022",
|
|
),
|
|
)
|
|
lists = (CptListEntry(appendix="D", code="99439"),)
|
|
return CptEdition(
|
|
year=year,
|
|
sections=sections,
|
|
codes=codes,
|
|
instructions=instructions,
|
|
references=references,
|
|
crosswalks=crosswalks,
|
|
lists=lists,
|
|
alternates=alternates,
|
|
)
|
|
|
|
|
|
class TestCptEditionEmptyGuardF4:
|
|
def test_zero_codes_refused_without_force(self, con):
|
|
edition = _tiny_edition(codes=())
|
|
with pytest.raises(ValueError, match="zero codes"):
|
|
write_cpt_edition(con, edition, "GQGTPGYV")
|
|
# And it must never have wiped whatever was already on file —
|
|
# write a real edition first, then the refused zero-code write
|
|
# for the same year must leave it untouched.
|
|
write_cpt_edition(con, _tiny_edition(), "GQGTPGYV")
|
|
with pytest.raises(ValueError, match="zero codes"):
|
|
write_cpt_edition(con, edition, "GQGTPGYV")
|
|
assert len(read_cpt_codes(con, 2024)) == 2
|
|
|
|
def test_zero_codes_written_with_force(self, con):
|
|
edition = _tiny_edition(codes=())
|
|
counts = write_cpt_edition(con, edition, "GQGTPGYV", force=True)
|
|
assert counts["codes"] == 0
|
|
|
|
|
|
class TestCptEdition:
|
|
def test_write_returns_counts_per_table(self, con):
|
|
counts = write_cpt_edition(con, _tiny_edition(), "GQGTPGYV")
|
|
assert counts == {
|
|
"sections": 1,
|
|
"codes": 2,
|
|
"instructions": 1,
|
|
"references": 1,
|
|
"crosswalks": 1,
|
|
"lists": 1,
|
|
"alternates": 0,
|
|
}
|
|
|
|
def test_alternates_round_trip(self, con):
|
|
alt = CptAlternate(
|
|
code="99490", sec_id="sec_guideline", reason="guidelines-reprint"
|
|
)
|
|
counts = write_cpt_edition(con, _tiny_edition(alternates=(alt,)), "GQGTPGYV")
|
|
assert counts["alternates"] == 1
|
|
row = con.execute(
|
|
"SELECT edition_year, item_key, code, sec_id, reason FROM pfs.cpt_code_alt"
|
|
).fetchone()
|
|
assert row == (2024, "GQGTPGYV", "99490", "sec_guideline", "guidelines-reprint")
|
|
|
|
def test_round_trip_reads(self, con):
|
|
write_cpt_edition(con, _tiny_edition(), "GQGTPGYV")
|
|
|
|
secs = read_cpt_sections(con, 2024)
|
|
assert len(secs) == 1
|
|
assert secs[0].item_key == "GQGTPGYV"
|
|
assert secs[0].path == [
|
|
"Evaluation and Management",
|
|
"Care Management Services",
|
|
"Chronic Care Management Services",
|
|
]
|
|
assert (
|
|
secs[0].path_key
|
|
== "Evaluation and Management > Care Management Services > Chronic Care Management Services"
|
|
)
|
|
|
|
codes = read_cpt_codes(con, 2024)
|
|
assert {c.code for c in codes} == {"99490", "99439"}
|
|
addon = next(c for c in codes if c.code == "99439")
|
|
assert addon.addon is True
|
|
assert addon.parent == "99490"
|
|
assert addon.elements == ["first element;"]
|
|
|
|
instr = read_cpt_instructions(con, 2024)
|
|
assert len(instr) == 1
|
|
assert instr[0].targets == ["99490"]
|
|
|
|
assert cpt_years(con) == [2024]
|
|
|
|
def test_delete_then_insert_per_edition_year(self, con):
|
|
write_cpt_edition(con, _tiny_edition(year=2021), "AJM4KF4G")
|
|
write_cpt_edition(con, _tiny_edition(year=2024), "GQGTPGYV")
|
|
assert cpt_years(con) == [2021, 2024]
|
|
|
|
write_cpt_edition(
|
|
con, _tiny_edition(year=2024), "GQGTPGYV"
|
|
) # idempotent re-run
|
|
assert cpt_years(con) == [2021, 2024]
|
|
assert len(read_cpt_codes(con, 2021)) == 2 # untouched by the 2024 rewrite
|
|
assert len(read_cpt_codes(con, 2024)) == 2
|
|
|
|
def test_duplicate_code_raises_and_writes_nothing(self, con):
|
|
# C8: the loader's own last-ditch guard against a resequenced
|
|
# placeholder row slipping past the parser's filter.
|
|
dup_codes = (
|
|
CptCode(
|
|
code="99490",
|
|
sec_id="sec_1",
|
|
descriptor="a",
|
|
stem="a",
|
|
elements=(),
|
|
tail="",
|
|
addon=False,
|
|
resequenced=False,
|
|
new=False,
|
|
revised=False,
|
|
telemedicine=False,
|
|
parent="",
|
|
mod51_exempt=False,
|
|
audio_only=False,
|
|
fda_pending=False,
|
|
pla=False,
|
|
category="I",
|
|
),
|
|
CptCode(
|
|
code="99490",
|
|
sec_id="sec_1",
|
|
descriptor="b",
|
|
stem="b",
|
|
elements=(),
|
|
tail="",
|
|
addon=False,
|
|
resequenced=False,
|
|
new=False,
|
|
revised=False,
|
|
telemedicine=False,
|
|
parent="",
|
|
mod51_exempt=False,
|
|
audio_only=False,
|
|
fda_pending=False,
|
|
pla=False,
|
|
category="I",
|
|
),
|
|
)
|
|
with pytest.raises(ValueError, match="99490"):
|
|
write_cpt_edition(con, _tiny_edition(codes=dup_codes), "GQGTPGYV")
|
|
assert read_cpt_codes(con, 2024) == []
|