Fix every real lint finding and drop degenerate splice fragments
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
import pytest
|
||||
|
||||
from ingestion.extract.models import Span
|
||||
from ingestion.segment.assembler import DuplicateDrugIdError, assemble
|
||||
|
||||
|
||||
def _span(text, page, y0, bold=True, size=9.5, printed=None, column="left"):
|
||||
return Span(
|
||||
physical_page=page, printed_page=printed if printed is not None else page + 1,
|
||||
column=column, block=0, line=0, span_index=0,
|
||||
x0=100.0, y0=y0, x1=200.0, y1=y0 + 12.0,
|
||||
text=text, font=("TimesNewRomanPS-BoldMT" if bold else "TimesNewRomanPSMT"), size=size,
|
||||
)
|
||||
|
||||
|
||||
def test_basic_single_monograph_with_sections_and_body():
|
||||
spans = [
|
||||
_span("ABACAVIR", 100, 60.0),
|
||||
_span("Tên chung quốc tế:", 100, 80.0),
|
||||
_span("Abacavir (Acyclovir-like).", 100, 92.0, bold=False),
|
||||
_span("Mã ATC:", 100, 104.0),
|
||||
_span("J05AF06", 100, 116.0, bold=False),
|
||||
_span("Chỉ định", 101, 60.0),
|
||||
_span("Điều trị nhiễm HIV.", 101, 72.0, bold=False),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert len(monographs) == 1
|
||||
m = monographs[0]
|
||||
assert m.drug_id == "abacavir"
|
||||
assert m.drug_name == "ABACAVIR"
|
||||
assert m.source_page_range == [100, 101]
|
||||
assert m.sections["ten_chung_quoc_te"].text == "Abacavir (Acyclovir-like)."
|
||||
assert m.sections["chi_dinh"].text == "Điều trị nhiễm HIV."
|
||||
assert m.atc_codes == ["J05AF06"]
|
||||
assert m.atc_stated_absent is False
|
||||
|
||||
|
||||
def test_non_bold_combined_heading_value_span_confirmed_real_amitriptylin_case():
|
||||
# AMITRIPTYLIN's real "Mã ATC:" heading is a single non-bold span
|
||||
# combining label and value ("Mã ATC: N06AA09."), unlike Abacavir's
|
||||
# bold-label + separate-value spans — see outlier item 20.
|
||||
spans = [
|
||||
_span("AMITRIPTYLIN", 184, 60.0),
|
||||
_span("Tên chung quốc tế: ", 184, 85.0),
|
||||
_span("Amitriptyline.", 184, 85.2, bold=False),
|
||||
_span("Mã ATC: N06AA09.", 184, 100.0, bold=False),
|
||||
_span("Loại thuốc:", 184, 115.0),
|
||||
_span("Thuốc chống trầm cảm.", 184, 115.2, bold=False),
|
||||
]
|
||||
m = list(assemble(spans))[0]
|
||||
assert m.sections["ma_atc"].text == "N06AA09."
|
||||
assert m.atc_codes == ["N06AA09"]
|
||||
|
||||
|
||||
def test_atc_stated_absent_propagates():
|
||||
spans = [
|
||||
_span("ADIPIODON", 100, 60.0),
|
||||
_span("Tên chung quốc tế:", 100, 72.0),
|
||||
_span("Adipiodon.", 100, 84.0, bold=False),
|
||||
_span("Mã ATC:", 100, 96.0),
|
||||
_span("Chưa có.", 100, 108.0, bold=False),
|
||||
]
|
||||
m = list(assemble(spans))[0]
|
||||
assert m.atc_codes == []
|
||||
assert m.atc_stated_absent is True
|
||||
|
||||
|
||||
def test_qualifier_line_disambiguates_same_name_monographs():
|
||||
# reproduces the confirmed real SALBUTAMOL case (outlier item 18):
|
||||
# same base title, disambiguated by a bold non-caps parenthesized line.
|
||||
spans = [
|
||||
_span("SALBUTAMOL", 1261, 60.0),
|
||||
_span("(Dùng trong hô hấp)", 1261, 72.0),
|
||||
_span("Tên chung quốc tế:", 1261, 84.0),
|
||||
_span("Salbutamol.", 1261, 96.0, bold=False),
|
||||
_span("Chỉ định", 1261, 108.0),
|
||||
_span("Điều trị hen.", 1261, 120.0, bold=False),
|
||||
_span("SALBUTAMOL", 1263, 60.0),
|
||||
_span("(Dùng trong sản khoa)", 1263, 72.0),
|
||||
_span("Tên chung quốc tế:", 1263, 84.0),
|
||||
_span("Salbutamol.", 1263, 96.0, bold=False),
|
||||
_span("Chỉ định", 1263, 108.0),
|
||||
_span("Điều trị dọa sinh non.", 1263, 120.0, bold=False),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert len(monographs) == 2
|
||||
assert monographs[0].drug_id == "salbutamol_dung_trong_ho_hap"
|
||||
assert monographs[0].drug_name == "SALBUTAMOL (Dùng trong hô hấp)"
|
||||
assert monographs[1].drug_id == "salbutamol_dung_trong_san_khoa"
|
||||
assert monographs[0].sections["chi_dinh"].text == "Điều trị hen."
|
||||
assert monographs[1].sections["chi_dinh"].text == "Điều trị dọa sinh non."
|
||||
|
||||
|
||||
def test_genuine_duplicate_drug_id_raises():
|
||||
spans = [
|
||||
_span("FOOBARDRUG", 200, 60.0),
|
||||
_span("Tên chung quốc tế:", 200, 72.0),
|
||||
_span("Foobardrug.", 200, 84.0, bold=False),
|
||||
_span("Chỉ định", 200, 96.0),
|
||||
_span("A.", 200, 108.0, bold=False),
|
||||
_span("FOOBARDRUG", 300, 60.0),
|
||||
_span("Tên chung quốc tế:", 300, 72.0),
|
||||
_span("Foobardrug.", 300, 84.0, bold=False),
|
||||
_span("Chỉ định", 300, 96.0),
|
||||
_span("B.", 300, 108.0, bold=False),
|
||||
]
|
||||
with pytest.raises(DuplicateDrugIdError):
|
||||
list(assemble(spans))
|
||||
|
||||
|
||||
def test_gonadotropin_wrap_does_not_falsely_trigger_duplicate_check():
|
||||
# regression: the multi-line wrap must merge BEFORE the duplicate check
|
||||
# runs, so this is never treated as two separate "GONADOTROPIN" titles
|
||||
spans = [
|
||||
_span("THUỐC TƯƠNG TỰ HORMON GIẢI PHÓNG", 1371, 664.4554443359375),
|
||||
_span("GONADOTROPIN", 1371, 676.2354736328125),
|
||||
_span("Tên chung quốc tế:", 1371, 690.0),
|
||||
_span("Gonadorelin.", 1371, 700.0, bold=False),
|
||||
_span("Chỉ định", 1371, 712.0),
|
||||
_span("X.", 1371, 724.0, bold=False),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert len(monographs) == 1
|
||||
assert monographs[0].drug_name == "THUỐC TƯƠNG TỰ HORMON GIẢI PHÓNG GONADOTROPIN"
|
||||
|
||||
|
||||
def test_front_matter_before_first_monograph_is_ignored():
|
||||
spans = [
|
||||
_span("Some front matter heading", 5, 60.0, bold=False, printed=6),
|
||||
_span("random body text", 5, 72.0, bold=False, printed=6),
|
||||
_span("ABACAVIR", 100, 60.0),
|
||||
_span("Tên chung quốc tế:", 100, 80.0),
|
||||
_span("Abacavir.", 100, 92.0, bold=False),
|
||||
_span("Chỉ định", 100, 104.0),
|
||||
_span("X.", 100, 116.0, bold=False),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert len(monographs) == 1
|
||||
assert monographs[0].drug_id == "abacavir"
|
||||
|
||||
|
||||
def test_empty_spans_yields_nothing():
|
||||
assert list(assemble([])) == []
|
||||
|
||||
|
||||
def test_table_header_false_positive_not_treated_as_monograph():
|
||||
# reproduces the confirmed real "HSV"/"CMV" table-column-header case
|
||||
# (outlier item 19, physical page 698, inside the Foscarnet natri
|
||||
# monograph's dosing table) — bold+all-caps+short, identical shape to a
|
||||
# real title, but never followed by "Tên chung quốc tế" before the next
|
||||
# real title. Must not be treated as a monograph boundary.
|
||||
spans = [
|
||||
_span("FOSCARNET NATRI", 690, 60.0),
|
||||
_span("Tên chung quốc tế:", 690, 80.0),
|
||||
_span("Foscarnet.", 690, 92.0, bold=False),
|
||||
_span("Chỉ định", 690, 104.0),
|
||||
_span("Điều trị CMV.", 690, 116.0, bold=False),
|
||||
_span("HSV", 698, 523.0),
|
||||
_span("HSV", 698, 523.0),
|
||||
_span("CMV", 698, 523.0),
|
||||
_span("CMV", 698, 523.0),
|
||||
_span("40 mg/kg cách nhau 12 giờ", 698, 540.0, bold=False),
|
||||
_span("ARTEMETHER", 700, 60.0),
|
||||
_span("Tên chung quốc tế:", 700, 80.0),
|
||||
_span("Artemether.", 700, 92.0, bold=False),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert [m.drug_id for m in monographs] == ["foscarnet_natri", "artemether"]
|
||||
# the table row's numbers/labels stay attached to Foscarnet's Chỉ định
|
||||
# section body (dropped from a dedicated section, which is fine — no
|
||||
# false monograph boundary is what matters here)
|
||||
assert "hsv" not in monographs[0].drug_id
|
||||
assert "cmv" not in monographs[0].drug_id
|
||||
|
||||
|
||||
def test_real_title_immediately_followed_by_anchor_is_kept():
|
||||
spans = [
|
||||
_span("ABACAVIR", 100, 60.0),
|
||||
_span("Tên chung quốc tế:", 100, 80.0),
|
||||
_span("Abacavir.", 100, 92.0, bold=False),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert len(monographs) == 1
|
||||
assert monographs[0].drug_id == "abacavir"
|
||||
|
||||
|
||||
def test_real_title_with_qualifier_before_anchor_is_still_kept():
|
||||
# the anchor lookahead must tolerate one intervening qualifier-line
|
||||
# event (the SALBUTAMOL case), not just immediate adjacency
|
||||
spans = [
|
||||
_span("SALBUTAMOL", 1261, 60.0),
|
||||
_span("(Dùng trong hô hấp)", 1261, 72.0),
|
||||
_span("Tên chung quốc tế:", 1261, 84.0),
|
||||
_span("Salbutamol.", 1261, 96.0, bold=False),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert len(monographs) == 1
|
||||
assert monographs[0].drug_id == "salbutamol_dung_trong_ho_hap"
|
||||
|
||||
|
||||
def test_class_level_monograph_sub_heading_not_treated_as_own_monograph():
|
||||
# reproduces the confirmed real case (outlier item 21): "SIMVASTATIN" is
|
||||
# a bold+all-caps+short sub-heading *inside* the class-level "CÁC CHẤT
|
||||
# ỨC CHẾ HMG-CoA REDUCTASE" monograph, immediately followed by its own
|
||||
# "Liều lượng và cách dùng" but NOT by "Tên chung quốc tế" (that section
|
||||
# belongs only to the parent). Must stay folded into the parent, not
|
||||
# become its own monograph.
|
||||
spans = [
|
||||
_span("CÁC CHẤT ỨC CHẾ HMG-CoA REDUCTASE", 284, 60.0, printed=285),
|
||||
_span("Tên chung quốc tế:", 284, 72.0, printed=285),
|
||||
_span("Simvastatin, Lovastatin.", 284, 84.0, bold=False, printed=285),
|
||||
_span("Chỉ định", 284, 96.0, printed=285),
|
||||
_span("Tăng lipid huyết.", 284, 108.0, bold=False, printed=285),
|
||||
_span("SIMVASTATIN", 285, 60.0, printed=286),
|
||||
_span("Liều lượng và cách dùng", 285, 72.0, printed=286),
|
||||
_span("Uống 10 - 20 mg mỗi tối.", 285, 84.0, bold=False, printed=286),
|
||||
_span("LOVASTATIN", 285, 96.0, printed=286),
|
||||
_span("Liều lượng và cách dùng", 285, 108.0, printed=286),
|
||||
_span("Uống 20 mg mỗi ngày.", 285, 120.0, bold=False, printed=286),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert len(monographs) == 1
|
||||
assert monographs[0].drug_name == "CÁC CHẤT ỨC CHẾ HMG-CoA REDUCTASE"
|
||||
# the sub-headings' own dosing text stays attached to the parent
|
||||
# monograph's content rather than vanishing or becoming new monographs
|
||||
assert "Uống 20 mg mỗi ngày." in monographs[0].sections["lieu_luong_va_cach_dung"].text
|
||||
|
||||
|
||||
def test_running_header_boilerplate_stripped_from_mid_section_body_confirmed_real_morphin_case():
|
||||
# exact confirmed real case: physical page 1008's running header
|
||||
# ("DTQGVN 2" / "1009" / "Morphin sulfat", all column="full_width",
|
||||
# y0~34, well inside the header band) falls squarely in the middle of
|
||||
# MORPHIN SULFAT's "Liều lượng và cách dùng" section, which spans the
|
||||
# page 1007->1008 boundary — see outlier-catalog item 13 / assembler.py
|
||||
# module docstring. Whole-corpus measured: 1,374/11,409 sections (12.0%)
|
||||
# affected before this fix, 671/682 monographs (98.4%) had at least one.
|
||||
spans = [
|
||||
_span("MORPHIN SULFAT", 1007, 60.0),
|
||||
_span("Tên chung quốc tế:", 1007, 80.0),
|
||||
_span("Morphini sulfas.", 1007, 92.0, bold=False),
|
||||
_span("Liều lượng và cách dùng", 1007, 700.0),
|
||||
_span("Với thuốc viên (viên nang hoặc viên nén) không nhai. Nếu", 1007, 785.4, bold=False),
|
||||
_span("DTQGVN 2", 1008, 34.6, bold=False, column="full_width"),
|
||||
_span("1009", 1008, 34.6, bold=False, column="full_width"),
|
||||
_span("Morphin sulfat", 1008, 34.4, bold=False, column="full_width"),
|
||||
_span("uống viên thuốc giải phóng chậm thì không được nghiền.", 1008, 60.8, bold=False),
|
||||
]
|
||||
m = list(assemble(spans))[0]
|
||||
section_text = m.sections["lieu_luong_va_cach_dung"].text
|
||||
assert "DTQGVN" not in section_text
|
||||
assert "1009" not in section_text
|
||||
# the two body spans are one sentence broken by a page boundary: "Nếu"
|
||||
# does not end a sentence, so normalize/text_flow rejoins them with a
|
||||
# space rather than preserving the PDF's visual wrap as a hard newline
|
||||
assert section_text == (
|
||||
"Với thuốc viên (viên nang hoặc viên nén) không nhai. Nếu "
|
||||
"uống viên thuốc giải phóng chậm thì không được nghiền."
|
||||
)
|
||||
|
||||
|
||||
def test_last_real_monograph_in_book_still_kept_near_end_of_input():
|
||||
# anchor lookahead must not require a "next title" to exist — the very
|
||||
# last monograph in the book has no following title at all
|
||||
spans = [
|
||||
_span("ZOLPIDEM", 1494, 60.0),
|
||||
_span("Tên chung quốc tế:", 1494, 80.0),
|
||||
_span("Zolpidem.", 1494, 92.0, bold=False),
|
||||
]
|
||||
monographs = list(assemble(spans))
|
||||
assert len(monographs) == 1
|
||||
assert monographs[0].drug_id == "zolpidem"
|
||||
|
||||
|
||||
def test_repeated_section_heading_appends_instead_of_overwriting():
|
||||
# measured real case: 33 monographs repeat a section heading (38
|
||||
# occurrences). CEFAMANDOL's "Liều lượng và cách dùng" resumes on
|
||||
# physical page 339 after a renal-dosing table; the old code replaced the
|
||||
# SectionSpan, destroying everything captured before the repeat — for
|
||||
# CEFAMANDOL that left the dosing section holding only the table.
|
||||
spans = [
|
||||
_span("CEFAMANDOL", 338, 60.0),
|
||||
_span("Tên chung quốc tế", 338, 80.0),
|
||||
_span("Cefamandolum.", 338, 92.0, bold=False),
|
||||
_span("Liều lượng và cách dùng", 338, 400.0),
|
||||
_span("Người lớn: 500 mg - 1 g, 4 - 8 giờ/lần.", 338, 412.0, bold=False),
|
||||
_span("Liều lượng và cách dùng", 339, 200.0),
|
||||
_span("Suy thận: giảm liều theo độ thanh thải creatinin.", 339, 212.0, bold=False),
|
||||
]
|
||||
m = list(assemble(spans))[0]
|
||||
text = m.sections["lieu_luong_va_cach_dung"].text
|
||||
assert "Người lớn: 500 mg - 1 g, 4 - 8 giờ/lần." in text
|
||||
assert "Suy thận: giảm liều theo độ thanh thải creatinin." in text
|
||||
# the first heading stays the provenance anchor
|
||||
assert m.sections["lieu_luong_va_cach_dung"].heading.physical_page == 338
|
||||
|
||||
|
||||
def test_a_plain_label_line_under_a_heading_is_body_not_a_new_section():
|
||||
"""FLUOROURACIL, physical page 681 — verified by rendering the page.
|
||||
|
||||
The book prints "Thời kỳ mang thai" / "Chống chỉ định." and "Thời kỳ cho
|
||||
con bú" / "Chống chỉ định.". The body line matches the section vocabulary,
|
||||
so it was read as a heading and both sections came out empty — dropping
|
||||
the statement that fluorouracil is contraindicated in pregnancy and while
|
||||
breastfeeding.
|
||||
"""
|
||||
spans = [
|
||||
_span("FLUOROURACIL", 681, 60.0),
|
||||
_span("Tên chung quốc tế", 681, 80.0),
|
||||
_span("Fluorouracilum.", 681, 92.0, bold=False),
|
||||
_span("Chống chỉ định", 681, 110.0),
|
||||
_span("Suy tủy nặng.", 681, 122.0, bold=False),
|
||||
_span("Thời kỳ mang thai", 681, 140.0),
|
||||
_span("Chống chỉ định.", 681, 152.0, bold=False),
|
||||
_span("Thời kỳ cho con bú", 681, 170.0),
|
||||
_span("Chống chỉ định.", 681, 182.0, bold=False),
|
||||
]
|
||||
monograph = list(assemble(spans))[0]
|
||||
assert monograph.sections["thoi_ky_mang_thai"].text == "Chống chỉ định."
|
||||
assert monograph.sections["thoi_ky_cho_con_bu"].text == "Chống chỉ định."
|
||||
assert monograph.sections["chong_chi_dinh"].text == "Suy tủy nặng."
|
||||
|
||||
|
||||
def test_a_bold_label_line_still_opens_its_section():
|
||||
spans = [
|
||||
_span("FLUOROURACIL", 681, 60.0),
|
||||
_span("Tên chung quốc tế", 681, 80.0),
|
||||
_span("Fluorouracilum.", 681, 92.0, bold=False),
|
||||
_span("Chống chỉ định", 681, 110.0),
|
||||
_span("Suy tủy nặng.", 681, 122.0, bold=False),
|
||||
]
|
||||
monograph = list(assemble(spans))[0]
|
||||
assert monograph.sections["chong_chi_dinh"].text == "Suy tủy nặng."
|
||||
Reference in New Issue
Block a user