""" Local ICD-10-CM search over the bundled CDC NCHS annual snapshot. The full official code set ships with the app (see ``access_core/monitoring/data/`` and ``manage.py load_icd10_codes`` for provenance) so the admin typeahead has no runtime dependency on an external API — searches run against Postgres and work offline, with no per-keystroke round trip to a third party. Codes are stored exactly as CDC publishes them (no dot, "E119"); results are returned in canonical dotted form ("E11.9") to match what reviewers enter and what the CMS boundary submits. """ import logging import re from access_core.monitoring.models import ICD10_CODE_RE, Icd10Code, canonical_icd10_code logger = logging.getLogger(__name__) class Icd10SearchError(Exception): """Raised when local ICD-10-CM search cannot produce results.""" CODE_LINE_RE = re.compile(r"^([A-Z0-9]{3,7})\s+(\S.*)$") def parse_icd10_code_file(text: str) -> list[tuple[str, str]]: """Parse a CDC NCHS ICD-10-CM code file into (code, description) tuples. The official format is one ``CODE Description`` pair per line, no header. Fails loudly (ValueError) on any malformed or duplicate line so a bad download can never silently corrupt the snapshot table. """ rows: list[tuple[str, str]] = [] seen: set[str] = set() for lineno, line in enumerate(text.splitlines(), start=1): if not line.strip(): continue match = CODE_LINE_RE.match(line) if not match: raise ValueError(f"line {lineno}: malformed line {line[:80]!r}") code, description = match.group(1), match.group(2).strip() if code in seen: raise ValueError(f"line {lineno}: duplicate code {code!r}") if not ICD10_CODE_RE.match(canonical_icd10_code(code)): raise ValueError(f"line {lineno}: invalid canonical form for {code!r}") seen.add(code) rows.append((code, description)) return rows def _normalize_terms(terms: str) -> list[str]: """Upper-case whitespace-separated words, dots stripped, <2-char dropped.""" raw = (terms or "").strip().upper().replace(".", "") return [word for word in re.split(r"\s+", raw) if len(word) >= 2] def search_icd10(terms: str, limit: int = 15) -> list[dict]: """Search the local ICD-10-CM snapshot by code prefix or description. Each normalized word (>= 2 chars) must match a code prefix or appear in the description; words are AND-ed together. Input is case-insensitive and dot-tolerant ("e119" == "E11.9"). Returns {"code": dotted, "name": str} ordered by code, capped at ``limit``. Empty for short or empty queries. Raises: Icd10SearchError on unexpected lookup failure (the admin endpoint maps it to a 502; the typeahead JS ignores it and manual entry works). """ words = _normalize_terms(terms) if not words: return [] from django.db.models import Q try: queryset = Icd10Code.objects.all() for word in words: queryset = queryset.filter( Q(code__startswith=word) | Q(description__icontains=word) ) rows = list( queryset.order_by("code").values_list("code", "description")[:limit] ) except Exception as exc: # The admin endpoint maps any lookup failure to a 502; the typeahead # JS ignores it and manual entry still works. raise Icd10SearchError(str(exc)) from exc return [ {"code": canonical_icd10_code(code), "name": description} for code, description in rows ]