Files
stack/tests/rex/test_sieve.py
kert 8f99eeb154
Some checks failed
ci/woodpecker/push/deploy Pipeline failed
ci/woodpecker/push/ci Pipeline failed
add comprehensive test suite, CI/CD quality gates, and package publishing
- 5519 unit tests covering all modules (aco, bcda, bls, cms, pfs, rex, bib)
- ruff lint + format enforcement across entire codebase (377 files reformatted)
- pre-commit hook: ruff check, ruff format, pytest
- Woodpecker CI split into ci.yml (quality gate) and deploy.yml (package + images)
- ci.yml: lint → test → validate-compose, runs on every push/PR
- deploy.yml: build + publish Python package to Gitea PyPI registry, then
  container image builds, Trivy scans, and registry push (main branch only)
- Gitea branch protection on main: requires CI status checks to pass
- .gitignore updated for .coverage, dist/, *.egg-info/
- grafana config moved to dev/grafana/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 14:58:48 -05:00

487 lines
18 KiB
Python

"""Tests for rex.sieve — Zone, Sieve, classify(), sift(), FieldMap."""
from __future__ import annotations
import re
from typing import Iterator
import pytest
from rex.sieve import FieldMap, Sieve, Zone, classify, sift
# ── Zone ──────────────────────────────────────────────────────────────────────
class TestZone:
def test_zone_values_are_strings(self) -> None:
assert Zone.HEAD == "head"
assert Zone.RULE == "rule"
assert Zone.DATA == "data"
assert Zone.SKIP == "skip"
def test_zone_is_str_enum(self) -> None:
assert isinstance(Zone.HEAD, str)
assert isinstance(Zone.DATA, str)
def test_zone_members(self) -> None:
members = set(Zone)
assert members == {Zone.HEAD, Zone.RULE, Zone.DATA, Zone.SKIP}
# ── Sieve construction ────────────────────────────────────────────────────────
class TestSieveConstruction:
def test_minimal_sieve_data_only(self) -> None:
s = Sieve(name="minimal", data=re.compile(r"^\d"))
assert s.name == "minimal"
assert s.head is None
assert s.rule is None
assert s.skip is None
assert s.fields is None
assert s.encoding == "utf-8"
def test_full_sieve(self) -> None:
s = Sieve(
name="full",
head=re.compile(r"^HEAD"),
rule=re.compile(r"^---"),
data=re.compile(r"^\d"),
skip=re.compile(r"^\s*$"),
)
assert s.head is not None
assert s.rule is not None
assert s.data is not None
assert s.skip is not None
def test_custom_encoding(self) -> None:
s = Sieve(name="ebcdic", data=re.compile(r"."), encoding="cp037")
assert s.encoding == "cp037"
def test_sieve_with_fields(self, fixed_width_field_map: FieldMap) -> None:
s = Sieve(
name="with_fields", data=re.compile(r"."), fields=fixed_width_field_map
)
assert s.fields is not None
assert "claim_id" in s.fields.positions
def test_sieve_name_preserved(self) -> None:
s = Sieve(name="my_format", data=re.compile(r"^X"))
assert s.name == "my_format"
# ── classify() ────────────────────────────────────────────────────────────────
class TestClassify:
@pytest.fixture
def full_sieve(self) -> Sieve:
return Sieve(
name="test",
head=re.compile(r"^The SAS System", re.IGNORECASE),
rule=re.compile(r"^\s+Obs\s+"),
data=re.compile(r"^\s+\d+\s+\w"),
skip=re.compile(r"^\s*(Total|NOTE:|$)"),
)
def test_head_line_classified_as_head(self, full_sieve: Sieve) -> None:
assert classify("The SAS System 10:30 AM", full_sieve) == Zone.HEAD
def test_rule_line_classified_as_rule(self, full_sieve: Sieve) -> None:
assert classify(" Obs claim_id paid_amount", full_sieve) == Zone.RULE
def test_data_line_classified_as_data(self, full_sieve: Sieve) -> None:
assert classify(" 1 CLM001 75.00", full_sieve) == Zone.DATA
def test_skip_line_classified_as_skip(self, full_sieve: Sieve) -> None:
assert classify(" Total: 450.00", full_sieve) == Zone.SKIP
def test_blank_line_classified_as_skip(self, full_sieve: Sieve) -> None:
assert classify("", full_sieve) == Zone.SKIP
def test_unmatched_line_defaults_to_skip(self, full_sieve: Sieve) -> None:
# A line that matches no pattern should default to SKIP
assert classify("??GARBAGE??LINE", full_sieve) == Zone.SKIP
def test_head_takes_priority_over_data(self) -> None:
"""head pattern tested before data — head wins when both match."""
s = Sieve(
name="priority",
head=re.compile(r"^X"),
data=re.compile(r"^X"), # same pattern
)
assert classify("XDATA", s) == Zone.HEAD
def test_skip_takes_priority_over_data(self) -> None:
"""skip pattern tested before data — skip wins when both match."""
s = Sieve(
name="priority_skip",
skip=re.compile(r"^\d"),
data=re.compile(r"^\d"),
)
assert classify("123 data", s) == Zone.SKIP
def test_data_only_sieve(self) -> None:
s = Sieve(name="data_only", data=re.compile(r"^\d"))
assert classify("123 abc", s) == Zone.DATA
assert classify("abc 123", s) == Zone.SKIP # no match → skip
def test_none_patterns_do_not_raise(self) -> None:
s = Sieve(name="sparse", data=re.compile(r"^\d"))
# Should not raise even if head/rule/skip are None
assert classify("not a digit line", s) == Zone.SKIP
# ── sift() ───────────────────────────────────────────────────────────────────
class TestSift:
@pytest.fixture
def sas_sieve(self) -> Sieve:
return Sieve(
name="sas",
head=re.compile(r"^\s*(The SAS System|\x0c)"),
rule=re.compile(r"^\s+Obs\s+"),
data=re.compile(r"^\s+\d+\s+\w"),
skip=re.compile(r"^\s*(Total|NOTE:|$)"),
)
def _lines(self, text: str) -> Iterator[str]:
for line in text.splitlines():
yield line
def test_sift_yields_correct_zones(self, sas_sieve: Sieve) -> None:
text = (
"The SAS System 10:30 AM\n"
"\n"
" Obs claim_id paid_amount\n"
" 1 CLM001 75.00\n"
" 2 CLM002 150.00\n"
" Total: 225.00\n"
)
results = list(sift(self._lines(text), sas_sieve))
zones = [z for z, _ in results]
assert Zone.HEAD in zones
assert Zone.RULE in zones
assert Zone.DATA in zones
assert Zone.SKIP in zones
def test_sift_data_lines_count(self, sas_sieve: Sieve) -> None:
text = (
"The SAS System\n"
" Obs claim_id\n"
" 1 CLM001\n"
" 2 CLM002\n"
" 3 CLM003\n"
" Total:\n"
)
data_lines = [
line
for zone, line in sift(self._lines(text), sas_sieve)
if zone == Zone.DATA
]
assert len(data_lines) == 3
def test_sift_empty_input_yields_nothing(self, sas_sieve: Sieve) -> None:
results = list(sift(iter([]), sas_sieve))
assert results == []
def test_sift_all_skip_file(self, sas_sieve: Sieve) -> None:
text = "\n\n\nNOTE: something\nNOTE: else\n"
results = list(sift(self._lines(text), sas_sieve))
for zone, _ in results:
assert zone == Zone.SKIP
def test_sift_preserves_data_content(self, sas_sieve: Sieve) -> None:
text = "The SAS System\n Obs id\n 1 HELLO\n"
data_lines = [
line
for zone, line in sift(self._lines(text), sas_sieve)
if zone == Zone.DATA
]
assert len(data_lines) == 1
assert "HELLO" in data_lines[0]
def test_sift_yields_tuples_of_zone_and_str(self, sas_sieve: Sieve) -> None:
text = " 1 CLM001\n"
results = list(sift(self._lines(text), sas_sieve))
for item in results:
assert len(item) == 2
zone, line = item
assert isinstance(zone, Zone)
assert isinstance(line, str)
def test_sift_builds_field_map_from_ruler_line(self) -> None:
"""When sieve.fields is None, sift should auto-build FieldMap from first rule line."""
s = Sieve(
name="automap",
rule=re.compile(r"^\s+Obs\s+"),
data=re.compile(r"^\s+\d+\s+\w"),
)
text = " Obs claim_id paid_amount\n 1 CLM001 75.00\n"
# Should not raise — FieldMap detection is part of sift's contract
results = list(sift(self._lines(text), s))
data_lines = [l for z, l in results if z == Zone.DATA]
assert len(data_lines) == 1
def test_sift_uses_explicit_field_map_when_provided(
self, fixed_width_field_map: FieldMap
) -> None:
s = Sieve(
name="explicit",
data=re.compile(r"^CLM"),
fields=fixed_width_field_map,
)
text = "CLM001 P001 20240115000007500\n"
results = list(sift(iter([text.strip()]), s))
assert len(results) == 1
assert results[0][0] == Zone.DATA
# fields should still be the provided map
assert s.fields is fixed_width_field_map
# ── FieldMap ─────────────────────────────────────────────────────────────────
class TestFieldMap:
# ── construction ──────────────────────────────────────────────────────────
def test_empty_field_map(self) -> None:
fm = FieldMap()
assert fm.positions == {}
assert fm.delimiter == ""
assert fm.indices == {}
def test_fixed_width_field_map_construction(
self, fixed_width_field_map: FieldMap
) -> None:
assert "claim_id" in fixed_width_field_map.positions
assert "person_id" in fixed_width_field_map.positions
assert fixed_width_field_map.positions["claim_id"] == (0, 10)
def test_delimited_field_map_construction(
self, delimited_field_map: FieldMap
) -> None:
assert delimited_field_map.delimiter == "|"
assert delimited_field_map.indices["claim_id"] == 0
assert delimited_field_map.indices["paid_amount"] == 3
# ── extract() — fixed-width mode ──────────────────────────────────────────
def test_extract_fixed_width_basic(self, fixed_width_field_map: FieldMap) -> None:
# "CLM001 P001 20240115000007500"
# positions: claim_id=(0,10), person_id=(10,20), service_date=(20,28), paid_amount=(28,37)
line = "CLM001 P001 20240115000007500"
result = fixed_width_field_map.extract(line)
assert "claim_id" in result
assert result["claim_id"].strip() == "CLM001"
def test_extract_fixed_width_person_id(
self, fixed_width_field_map: FieldMap
) -> None:
line = "CLM001 P001 20240115000007500"
result = fixed_width_field_map.extract(line)
assert result["person_id"].strip() == "P001"
def test_extract_fixed_width_service_date(
self, fixed_width_field_map: FieldMap
) -> None:
line = "CLM001 P001 20240115000007500"
result = fixed_width_field_map.extract(line)
assert result["service_date"] == "20240115"
def test_extract_fixed_width_returns_dict(
self, fixed_width_field_map: FieldMap
) -> None:
line = "CLM001 P001 20240115000007500"
result = fixed_width_field_map.extract(line)
assert isinstance(result, dict)
assert set(result.keys()) == {
"claim_id",
"person_id",
"service_date",
"paid_amount",
}
def test_extract_fixed_width_short_line(
self, fixed_width_field_map: FieldMap
) -> None:
"""Extraction on a short line should not raise — fields beyond end are empty."""
line = "CLM001"
result = fixed_width_field_map.extract(line)
assert "claim_id" in result
# Fields that extend beyond the line length should be empty string
assert (
result.get("service_date", "") == ""
or result.get("service_date") is not None
)
def test_extract_fixed_width_multiple_records(
self, fixed_width_field_map: FieldMap, fixed_width_text: str
) -> None:
lines = fixed_width_text.strip().splitlines()
results = [fixed_width_field_map.extract(line) for line in lines]
assert len(results) == 3
claim_ids = [r["claim_id"].strip() for r in results]
assert claim_ids == ["CLM001", "CLM002", "CLM003"]
# ── extract() — delimited mode ────────────────────────────────────────────
def test_extract_delimited_basic(self, delimited_field_map: FieldMap) -> None:
line = "CLM001|P001|20240115|75.00"
result = delimited_field_map.extract(line)
assert result["claim_id"] == "CLM001"
assert result["person_id"] == "P001"
assert result["paid_amount"] == "75.00"
def test_extract_delimited_multiple_rows(
self, delimited_field_map: FieldMap, pipe_delimited_text: str
) -> None:
data_lines = pipe_delimited_text.strip().splitlines()[1:] # skip header
results = [delimited_field_map.extract(line) for line in data_lines]
assert len(results) == 2
assert results[0]["claim_id"] == "CLM001"
assert results[1]["claim_id"] == "CLM002"
def test_extract_delimited_returns_dict(
self, delimited_field_map: FieldMap
) -> None:
line = "CLM001|P001|20240115|75.00"
result = delimited_field_map.extract(line)
assert isinstance(result, dict)
# ── from_ruler() ──────────────────────────────────────────────────────────
def test_from_ruler_basic(self) -> None:
ruler = " Obs claim_id paid_amount diag_code"
fm = FieldMap.from_ruler(ruler)
assert isinstance(fm, FieldMap)
assert "claim_id" in fm.positions or len(fm.positions) > 0
def test_from_ruler_returns_field_map(self) -> None:
ruler = " Obs id amount"
fm = FieldMap.from_ruler(ruler)
assert isinstance(fm, FieldMap)
def test_from_ruler_positions_are_tuples(self) -> None:
ruler = " Obs claim_id paid_amount"
fm = FieldMap.from_ruler(ruler)
for name, pos in fm.positions.items():
assert isinstance(pos, tuple), f"{name} position is not a tuple"
assert len(pos) == 2, f"{name} position tuple should have 2 elements"
start, end = pos
assert start >= 0
assert end > start
def test_from_ruler_non_overlapping_positions(self) -> None:
ruler = " Obs claim_id paid_amount diag_code"
fm = FieldMap.from_ruler(ruler)
positions = sorted(fm.positions.values(), key=lambda p: p[0])
for i in range(len(positions) - 1):
_, end_a = positions[i]
start_b, _ = positions[i + 1]
assert end_a <= start_b, (
f"Overlapping positions detected: {positions[i]} and {positions[i + 1]}"
)
def test_from_ruler_column_names_stripped(self) -> None:
ruler = " Obs claim_id paid_amount"
fm = FieldMap.from_ruler(ruler)
for name in fm.positions:
assert name == name.strip(), f"Column name {name!r} has whitespace"
def test_from_ruler_with_sieve_integration(self) -> None:
"""FieldMap.from_ruler result can be used in a Sieve."""
ruler = " Obs claim_id paid_amount"
fm = FieldMap.from_ruler(ruler)
s = Sieve(name="dynamic", data=re.compile(r"^\s+\d"), fields=fm)
assert s.fields is fm
# ── Integration: classify + sift + extract ────────────────────────────────────
class TestSieveIntegration:
def test_full_pipeline_sas_listing(self, sas_listing_text: str) -> None:
"""Classify, sift, and extract a complete SAS listing sample."""
sieve = Sieve(
name="sas",
head=re.compile(r"^\s*(The SAS System|\x0c)"),
rule=re.compile(r"^\s+Obs\s+"),
data=re.compile(r"^\s+\d+\s+\w"),
skip=re.compile(r"^\s*(Total|NOTE:|$)"),
)
field_map = FieldMap(
positions={
"obs": (0, 5),
"claim_id": (5, 15),
"paid_amount": (15, 30),
"diag_code": (30, 40),
}
)
sieve = Sieve(
name="sas",
head=re.compile(r"^\s*(The SAS System|\x0c)"),
rule=re.compile(r"^\s+Obs\s+"),
data=re.compile(r"^\s+\d+\s+\w"),
skip=re.compile(r"^\s*(Total|NOTE:|$)"),
fields=field_map,
)
lines = sas_listing_text.splitlines()
tagged = list(sift(iter(lines), sieve))
data_records = [field_map.extract(line) for z, line in tagged if z == Zone.DATA]
assert len(data_records) == 3
def test_pipe_delimited_full_pipeline(self, pipe_delimited_text: str) -> None:
"""Classify and extract from a pipe-delimited file."""
sieve = Sieve(
name="pipe",
rule=re.compile(r"^claim_id\|"),
data=re.compile(r"^CLM"),
)
fm = FieldMap(
delimiter="|", indices={"claim_id": 0, "person_id": 1, "paid_amount": 3}
)
lines = pipe_delimited_text.strip().splitlines()
results = []
for zone, line in sift(iter(lines), sieve):
if zone == Zone.DATA:
results.append(fm.extract(line))
assert len(results) == 2
assert results[0]["claim_id"] == "CLM001"
assert results[1]["claim_id"] == "CLM002"
def test_classify_priority_order(self) -> None:
"""Priority: head > skip > rule > data."""
# A line matching all four — head should win
s = Sieve(
name="priority",
head=re.compile(r"^A"),
rule=re.compile(r"^A"),
data=re.compile(r"^A"),
skip=re.compile(r"^A"),
)
assert classify("AAAA", s) == Zone.HEAD
# Without head — skip should win
s2 = Sieve(
name="priority2",
rule=re.compile(r"^A"),
data=re.compile(r"^A"),
skip=re.compile(r"^A"),
)
assert classify("AAAA", s2) == Zone.SKIP
# Without skip — rule should win
s3 = Sieve(
name="priority3",
rule=re.compile(r"^A"),
data=re.compile(r"^A"),
)
assert classify("AAAA", s3) == Zone.RULE