aimedicalcoding 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aimedicalcoding/__init__.py +29 -0
- aimedicalcoding/__main__.py +313 -0
- aimedicalcoding/adapters/__init__.py +3 -0
- aimedicalcoding/adapters/base.py +25 -0
- aimedicalcoding/adapters/ccda/__init__.py +3 -0
- aimedicalcoding/adapters/ccda/adapter.py +294 -0
- aimedicalcoding/adapters/ccda/codes.py +198 -0
- aimedicalcoding/adapters/ccda/narrative.py +121 -0
- aimedicalcoding/adapters/ccda/sections.py +44 -0
- aimedicalcoding/adapters/custom_json/__init__.py +3 -0
- aimedicalcoding/adapters/custom_json/adapter.py +30 -0
- aimedicalcoding/adapters/fhir/__init__.py +3 -0
- aimedicalcoding/adapters/fhir/adapter.py +326 -0
- aimedicalcoding/adapters/fhir/codes.py +211 -0
- aimedicalcoding/adapters/fhir/mapping.py +68 -0
- aimedicalcoding/api.py +163 -0
- aimedicalcoding/core/__init__.py +34 -0
- aimedicalcoding/core/checkdigits.py +103 -0
- aimedicalcoding/core/models.py +149 -0
- aimedicalcoding/core/system_registry.py +185 -0
- aimedicalcoding/pipeline/__init__.py +3 -0
- aimedicalcoding/pipeline/orchestrator.py +33 -0
- aimedicalcoding/recovery/__init__.py +3 -0
- aimedicalcoding/recovery/engine.py +16 -0
- aimedicalcoding/serialize.py +257 -0
- aimedicalcoding/systems/__init__.py +3 -0
- aimedicalcoding/systems/base.py +43 -0
- aimedicalcoding/systems/cvx/__init__.py +15 -0
- aimedicalcoding/systems/cvx/handler.py +55 -0
- aimedicalcoding/systems/cvx/normalize.py +75 -0
- aimedicalcoding/systems/cvx/patterns.py +134 -0
- aimedicalcoding/systems/icd10cm/__init__.py +16 -0
- aimedicalcoding/systems/icd10cm/handler.py +55 -0
- aimedicalcoding/systems/icd10cm/icdformat.py +43 -0
- aimedicalcoding/systems/icd10cm/patterns.py +133 -0
- aimedicalcoding/systems/loinc/__init__.py +14 -0
- aimedicalcoding/systems/loinc/axes.py +199 -0
- aimedicalcoding/systems/loinc/handler.py +80 -0
- aimedicalcoding/systems/loinc/normalize.py +81 -0
- aimedicalcoding/systems/loinc/patterns.py +166 -0
- aimedicalcoding/systems/rxnorm/__init__.py +13 -0
- aimedicalcoding/systems/rxnorm/handler.py +55 -0
- aimedicalcoding/systems/rxnorm/normalize.py +88 -0
- aimedicalcoding/systems/rxnorm/patterns.py +143 -0
- aimedicalcoding/systems/snomed/__init__.py +15 -0
- aimedicalcoding/systems/snomed/handler.py +60 -0
- aimedicalcoding/systems/snomed/hierarchy.py +96 -0
- aimedicalcoding/systems/snomed/patterns.py +166 -0
- aimedicalcoding/systems/snomed/qualifiers.py +90 -0
- aimedicalcoding/terminology/__init__.py +3 -0
- aimedicalcoding/terminology/service.py +31 -0
- aimedicalcoding-0.3.1.dist-info/METADATA +276 -0
- aimedicalcoding-0.3.1.dist-info/RECORD +57 -0
- aimedicalcoding-0.3.1.dist-info/WHEEL +5 -0
- aimedicalcoding-0.3.1.dist-info/entry_points.txt +2 -0
- aimedicalcoding-0.3.1.dist-info/licenses/LICENSE +21 -0
- aimedicalcoding-0.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Per-pattern SNOMED gap classification + parsers.
|
|
2
|
+
|
|
3
|
+
Key difference from LOINC: SNOMED is the *concept* (answer). Depending on the
|
|
4
|
+
slot it lives either in the observation **value** (problem/reaction/social/…) or
|
|
5
|
+
in the node's **code** (procedure/allergen substance). We read the right slot per
|
|
6
|
+
context, then classify.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from ...core.checkdigits import is_valid_sctid
|
|
15
|
+
from ...core.models import ClinicalStatement, CodingCandidate, Gap, System
|
|
16
|
+
from ...core.system_registry import HL7_STRUCTURAL
|
|
17
|
+
from . import qualifiers as Q
|
|
18
|
+
from .hierarchy import SNOMED_CONTEXTS, hierarchy_for, resolve_hierarchy
|
|
19
|
+
|
|
20
|
+
# contexts where the SNOMED concept is the observation VALUE (answer slot)
|
|
21
|
+
VALUE_SLOT_CONTEXTS = {
|
|
22
|
+
"problem", "family_history", "allergy_reaction",
|
|
23
|
+
"social_history", "result_organism", "functional_status",
|
|
24
|
+
}
|
|
25
|
+
# the rest (procedure, allergy_substance, body_site, specimen) carry it on the CODE
|
|
26
|
+
|
|
27
|
+
STRUCTURAL = HL7_STRUCTURAL | {System.NULLFLAVOR}
|
|
28
|
+
ICD_SYSTEMS = {System.ICD10CM, System.ICD9CM, System.ICD10PCS}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SnomedGapPattern(str, Enum):
|
|
32
|
+
LOCAL_CODE_ONLY = "LOCAL_CODE_ONLY" # a local/other clinical code (incl. CPT), no SNOMED
|
|
33
|
+
ICD_ONLY = "ICD_ONLY" # ICD-9/10 only -> ICD↔SNOMED crosswalk
|
|
34
|
+
NULLFLAVOR = "NULLFLAVOR"
|
|
35
|
+
DISPLAY_ONLY = "DISPLAY_ONLY"
|
|
36
|
+
NARRATIVE_ONLY = "NARRATIVE_ONLY"
|
|
37
|
+
BAD_CODE = "BAD_CODE" # SCTID-shaped but Verhoeff fails
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_snomed_expected(stmt: ClinicalStatement) -> bool:
|
|
41
|
+
return (
|
|
42
|
+
stmt.context in SNOMED_CONTEXTS
|
|
43
|
+
and (stmt.raw_text is not None or stmt.value is not None or stmt.codings)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def concept_coding(stmt: ClinicalStatement) -> Optional[CodingCandidate]:
|
|
48
|
+
"""The coding that represents the clinical concept for this slot.
|
|
49
|
+
|
|
50
|
+
For value-slot contexts, scan all value codings (root + translations) and
|
|
51
|
+
prefer a SNOMED coding (so a SNOMED translation is found even when another
|
|
52
|
+
system is primary); else fall back to the first real coding present.
|
|
53
|
+
"""
|
|
54
|
+
if stmt.context in VALUE_SLOT_CONTEXTS:
|
|
55
|
+
codings = stmt.value.codings if stmt.value else []
|
|
56
|
+
snomed = next((c for c in codings
|
|
57
|
+
if c.system == System.SNOMEDCT and c.code), None)
|
|
58
|
+
if snomed:
|
|
59
|
+
return snomed
|
|
60
|
+
other = next((c for c in codings
|
|
61
|
+
if c.code and c.system not in STRUCTURAL), None)
|
|
62
|
+
return other or (stmt.value.coded_value if stmt.value else None)
|
|
63
|
+
# code-slot: first non-structural coding with a code
|
|
64
|
+
return next((c for c in stmt.codings
|
|
65
|
+
if c.code and c.system not in STRUCTURAL), None)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def concept_term(stmt: ClinicalStatement) -> Optional[str]:
|
|
69
|
+
"""The human label of the concept (value display for value-slots, code
|
|
70
|
+
display for code-slots), falling back to recovered value originalText."""
|
|
71
|
+
cc = concept_coding(stmt)
|
|
72
|
+
if cc and cc.display:
|
|
73
|
+
return cc.display
|
|
74
|
+
if stmt.context in VALUE_SLOT_CONTEXTS and stmt.value:
|
|
75
|
+
return stmt.value.original_text or stmt.value.raw
|
|
76
|
+
return stmt.raw_text
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def concept_labels(stmt: ClinicalStatement) -> tuple[Optional[str], Optional[str]]:
|
|
80
|
+
"""(display_name, original_text) for the concept's own slot."""
|
|
81
|
+
if stmt.context in VALUE_SLOT_CONTEXTS:
|
|
82
|
+
cc = stmt.value.coded_value if stmt.value else None
|
|
83
|
+
display = cc.display if (cc and cc.code) else None
|
|
84
|
+
ot = stmt.value.original_text if stmt.value else None
|
|
85
|
+
return display, ot
|
|
86
|
+
# code-slot (procedure / allergen substance): the node's own code labels
|
|
87
|
+
cc = concept_coding(stmt)
|
|
88
|
+
return (cc.display if cc else stmt.display_name), stmt.original_text
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _has_nullflavor(stmt: ClinicalStatement) -> bool:
|
|
92
|
+
if any(c.system == System.NULLFLAVOR for c in stmt.codings):
|
|
93
|
+
return True
|
|
94
|
+
cv = stmt.value.coded_value if stmt.value else None
|
|
95
|
+
return bool(cv and cv.system == System.NULLFLAVOR)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def classify(stmt: ClinicalStatement) -> Optional[SnomedGapPattern]:
|
|
99
|
+
if not is_snomed_expected(stmt):
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
cc = concept_coding(stmt)
|
|
103
|
+
|
|
104
|
+
if cc and cc.system == System.SNOMEDCT and cc.code:
|
|
105
|
+
# valid SNOMED present -> no gap (active/hierarchy checks need the DB)
|
|
106
|
+
return None if is_valid_sctid(cc.code) else SnomedGapPattern.BAD_CODE
|
|
107
|
+
|
|
108
|
+
if cc and cc.code and cc.system in ICD_SYSTEMS:
|
|
109
|
+
return SnomedGapPattern.ICD_ONLY
|
|
110
|
+
if cc and cc.code and cc.system not in STRUCTURAL:
|
|
111
|
+
return SnomedGapPattern.LOCAL_CODE_ONLY # CPT / local / other
|
|
112
|
+
|
|
113
|
+
# no usable concept code
|
|
114
|
+
if _has_nullflavor(stmt):
|
|
115
|
+
return SnomedGapPattern.NULLFLAVOR
|
|
116
|
+
if stmt.raw_text:
|
|
117
|
+
if "/text" in (stmt.source_ref or ""):
|
|
118
|
+
return SnomedGapPattern.NARRATIVE_ONLY
|
|
119
|
+
return SnomedGapPattern.DISPLAY_ONLY
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# --------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def _base(stmt, pattern, label, ecl, term) -> Gap:
|
|
126
|
+
return Gap(
|
|
127
|
+
type=pattern.value,
|
|
128
|
+
match_key=Q.build_match_key(stmt, label, term),
|
|
129
|
+
evidence=Q.evidence_packet(stmt, label, ecl, term),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_gap(stmt: ClinicalStatement, pattern: SnomedGapPattern) -> Gap:
|
|
134
|
+
term = concept_term(stmt)
|
|
135
|
+
# semantic tag in the term (e.g. "... (procedure)") overrides the section
|
|
136
|
+
# default so a Problem-List procedure/situation isn't forced to Clinical finding.
|
|
137
|
+
label, ecl = resolve_hierarchy(stmt.context, term) or ("?", "?")
|
|
138
|
+
gap = _base(stmt, pattern, label, ecl, term)
|
|
139
|
+
# concept labels come from the value slot for value-slot contexts
|
|
140
|
+
dn, ot = concept_labels(stmt)
|
|
141
|
+
gap.evidence["display_name"] = dn
|
|
142
|
+
gap.evidence["original_text"] = ot
|
|
143
|
+
cc = concept_coding(stmt)
|
|
144
|
+
|
|
145
|
+
if pattern == SnomedGapPattern.LOCAL_CODE_ONLY and cc:
|
|
146
|
+
gap.evidence["local_code"] = cc.code
|
|
147
|
+
gap.evidence["local_system"] = cc.raw_system
|
|
148
|
+
gap.note = ("Crosswalk (system, code) → SNOMED first (e.g. CPT/local map); "
|
|
149
|
+
"else hierarchy-constrained text search, then LLM.")
|
|
150
|
+
elif pattern == SnomedGapPattern.ICD_ONLY and cc:
|
|
151
|
+
gap.evidence["icd_code"] = cc.code
|
|
152
|
+
gap.evidence["icd_system"] = (cc.system.value if cc.system else None)
|
|
153
|
+
gap.note = ("Use the official ICD↔SNOMED map (NLM/UMLS) — deterministic; "
|
|
154
|
+
"LLM only to choose among multiple map targets.")
|
|
155
|
+
elif pattern == SnomedGapPattern.BAD_CODE and cc:
|
|
156
|
+
gap.evidence["malformed_code"] = cc.code
|
|
157
|
+
gap.note = "SCTID-shaped but Verhoeff check failed; resolve via text + hierarchy."
|
|
158
|
+
elif pattern == SnomedGapPattern.NULLFLAVOR:
|
|
159
|
+
nf = next((c for c in stmt.codings if c.system == System.NULLFLAVOR), None)
|
|
160
|
+
gap.evidence["null_flavor"] = nf.raw_system if nf else None
|
|
161
|
+
gap.note = "Label recovered from originalText; hierarchy-constrained text search."
|
|
162
|
+
elif pattern == SnomedGapPattern.DISPLAY_ONLY:
|
|
163
|
+
gap.note = "Display text only; hierarchy-constrained text search."
|
|
164
|
+
elif pattern == SnomedGapPattern.NARRATIVE_ONLY:
|
|
165
|
+
gap.note = "Narrative-table text; resolve via hierarchy-constrained search (low priority)."
|
|
166
|
+
return gap
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""SNOMED disambiguation: term normalization, qualifier extraction, match key.
|
|
2
|
+
|
|
3
|
+
SNOMED's analog of LOINC's axes is: hierarchy (from section) + qualifiers
|
|
4
|
+
(laterality, severity, clinical course, context/negation) parsed from the text.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from ...core.models import ClinicalStatement
|
|
13
|
+
|
|
14
|
+
_PUNCT = re.compile(r"[^\w\s]")
|
|
15
|
+
_WS = re.compile(r"\s+")
|
|
16
|
+
|
|
17
|
+
_LATERALITY = ["bilateral", "left", "right"]
|
|
18
|
+
_SEVERITY = ["mild", "moderate", "severe"]
|
|
19
|
+
_COURSE = ["acute", "chronic", "acute on chronic"]
|
|
20
|
+
_CONTEXT = {
|
|
21
|
+
"family history": "family-history",
|
|
22
|
+
"history of": "history-of",
|
|
23
|
+
"no known": "absent",
|
|
24
|
+
"denies": "absent",
|
|
25
|
+
"negative for": "absent",
|
|
26
|
+
"suspected": "suspected",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_term(text: Optional[str]) -> Optional[str]:
|
|
31
|
+
if not text:
|
|
32
|
+
return None
|
|
33
|
+
t = _PUNCT.sub(" ", text.lower())
|
|
34
|
+
t = _WS.sub(" ", t).strip()
|
|
35
|
+
return t or None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _find(text: str, options: list[str]) -> Optional[str]:
|
|
39
|
+
for o in options:
|
|
40
|
+
if re.search(rf"\b{re.escape(o)}\b", text):
|
|
41
|
+
return o
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extract_qualifiers(stmt: ClinicalStatement, term: Optional[str] = None) -> dict:
|
|
46
|
+
text = ((term or stmt.raw_text or "")).lower()
|
|
47
|
+
ctx = None
|
|
48
|
+
for kw, val in _CONTEXT.items():
|
|
49
|
+
if kw in text:
|
|
50
|
+
ctx = val
|
|
51
|
+
break
|
|
52
|
+
return {
|
|
53
|
+
"laterality": _find(text, _LATERALITY),
|
|
54
|
+
"severity": _find(text, _SEVERITY),
|
|
55
|
+
"course": _find(text, _COURSE),
|
|
56
|
+
"negation_context": ctx,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_match_key(stmt: ClinicalStatement, hierarchy_label: str,
|
|
61
|
+
term: Optional[str] = None) -> str:
|
|
62
|
+
"""component | hierarchy | laterality | severity | course | context."""
|
|
63
|
+
term = term or stmt.raw_text
|
|
64
|
+
q = extract_qualifiers(stmt, term)
|
|
65
|
+
parts = [
|
|
66
|
+
f"component:{normalize_term(term) or '?'}",
|
|
67
|
+
f"hierarchy:{hierarchy_label or '?'}",
|
|
68
|
+
f"laterality:{q['laterality'] or '-'}",
|
|
69
|
+
f"severity:{q['severity'] or '-'}",
|
|
70
|
+
f"course:{q['course'] or '-'}",
|
|
71
|
+
f"context:{q['negation_context'] or 'current'}",
|
|
72
|
+
]
|
|
73
|
+
return " | ".join(parts)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def evidence_packet(stmt: ClinicalStatement, hierarchy_label: str, ecl: str,
|
|
77
|
+
term: Optional[str] = None) -> dict:
|
|
78
|
+
term = term or stmt.raw_text
|
|
79
|
+
q = extract_qualifiers(stmt, term)
|
|
80
|
+
return {
|
|
81
|
+
"term": term,
|
|
82
|
+
"term_normalized": normalize_term(term),
|
|
83
|
+
"target_hierarchy": hierarchy_label,
|
|
84
|
+
"ecl": ecl,
|
|
85
|
+
"laterality": q["laterality"],
|
|
86
|
+
"severity": q["severity"],
|
|
87
|
+
"course": q["course"],
|
|
88
|
+
"context": q["negation_context"] or "current",
|
|
89
|
+
"section_context": stmt.context,
|
|
90
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Terminology DB service — PLACEHOLDER.
|
|
2
|
+
|
|
3
|
+
Will wrap the local terminology database: exact lookup, existence/deprecation,
|
|
4
|
+
free-text candidate search (returning a constrained set), axis/attribute
|
|
5
|
+
retrieval, crosswalks. Loaders for Loinc.csv / SNOMED RF2 / RxNorm RRF / ICD
|
|
6
|
+
files will live under terminology/loaders/.
|
|
7
|
+
See ARCHITECTURE.md §5.4.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from ..core.models import System
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TerminologyService:
|
|
18
|
+
def lookup(self, system: System, code: str) -> Optional[dict[str, Any]]:
|
|
19
|
+
raise NotImplementedError("DB layer is a placeholder (parsing-first phase).")
|
|
20
|
+
|
|
21
|
+
def is_deprecated(self, system: System, code: str) -> bool:
|
|
22
|
+
raise NotImplementedError("DB layer is a placeholder (parsing-first phase).")
|
|
23
|
+
|
|
24
|
+
def search_candidates(
|
|
25
|
+
self, system: System, match_key: str, limit: int = 10
|
|
26
|
+
) -> list[dict[str, Any]]:
|
|
27
|
+
"""Free-text + axis-filtered candidate search for the recovery layer."""
|
|
28
|
+
raise NotImplementedError("DB layer is a placeholder (parsing-first phase).")
|
|
29
|
+
|
|
30
|
+
def crosswalk(self, from_system: System, code: str, to_system: System) -> list[str]:
|
|
31
|
+
raise NotImplementedError("DB layer is a placeholder (parsing-first phase).")
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aimedicalcoding
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
|
|
5
|
+
Author: Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: loinc snomed rxnorm icd-10-cm cvx ccda fhir clinical-terminology medical-coding interoperability
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: lxml>=4.9
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
24
|
+
Requires-Dist: build; extra == "dev"
|
|
25
|
+
Requires-Dist: twine; extra == "dev"
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: classifier
|
|
28
|
+
Dynamic: description
|
|
29
|
+
Dynamic: description-content-type
|
|
30
|
+
Dynamic: keywords
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: provides-extra
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
Dynamic: summary
|
|
37
|
+
|
|
38
|
+
# aimedicalcoding
|
|
39
|
+
|
|
40
|
+
**Find every missing or broken standard clinical code in a CCDA or FHIR document — with the evidence needed to fix it.**
|
|
41
|
+
|
|
42
|
+
`aimedicalcoding` parses a clinical document (CCDA XML or FHIR R4 Bundle JSON),
|
|
43
|
+
inspects every coded clinical item — labs, vitals, problems, medications,
|
|
44
|
+
immunizations, allergies, procedures, social history — and reports the ones
|
|
45
|
+
whose standard terminology code (**LOINC · SNOMED CT · RxNorm · ICD-10-CM ·
|
|
46
|
+
CVX**) is missing, local-only, crosswalkable, or malformed. Each finding (a
|
|
47
|
+
*gap*) is emitted as a self-contained JSON record carrying the classification,
|
|
48
|
+
the document facts, and a ready-made recovery plan (search terms, axis filters,
|
|
49
|
+
crosswalk candidates, check-digit corrections).
|
|
50
|
+
|
|
51
|
+
Items that are already correctly coded produce no output — the result is a
|
|
52
|
+
work list, not an inventory.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install aimedicalcoding
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python 3.10+. The only runtime dependency is `lxml`.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick start (CLI)
|
|
67
|
+
|
|
68
|
+
The input format is **auto-detected** — the same command works for CCDA XML
|
|
69
|
+
and FHIR JSON, and the output schema is identical for both.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# human-readable gap report (card view)
|
|
73
|
+
aimedicalcoding patient.xml
|
|
74
|
+
|
|
75
|
+
# FHIR R4 Bundle — same flags, same output
|
|
76
|
+
aimedicalcoding bundle.json --table
|
|
77
|
+
|
|
78
|
+
# counts only
|
|
79
|
+
aimedicalcoding patient.xml --summary --table
|
|
80
|
+
|
|
81
|
+
# machine-readable JSON, written to a file as UTF-8
|
|
82
|
+
aimedicalcoding patient.xml --out gaps.json
|
|
83
|
+
|
|
84
|
+
# restrict to one terminology and/or certain contexts
|
|
85
|
+
aimedicalcoding patient.xml --system loinc --contexts result,vital
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If the entry point is not on your PATH, `python -m aimedicalcoding <file> [flags]`
|
|
89
|
+
is equivalent.
|
|
90
|
+
|
|
91
|
+
### CLI flags
|
|
92
|
+
|
|
93
|
+
| Flag | Meaning |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `--system` | `loinc` \| `snomed` \| `rxnorm` \| `icd10cm` \| `cvx` \| `all` (default `all`) |
|
|
96
|
+
| `--contexts` | comma-separated contexts to include, e.g. `result,vital,problem` |
|
|
97
|
+
| `--table` | compact one-line-per-gap table |
|
|
98
|
+
| `--json` | print the JSON report to stdout |
|
|
99
|
+
| `-o, --out FILE` | write the JSON report to FILE as UTF-8 (implies `--json`) |
|
|
100
|
+
| `--summary` | counts only |
|
|
101
|
+
| `--include-narrative` | also report narrative-table-only items (CCDA; off by default) |
|
|
102
|
+
| `--apply-dict` | expand lab abbreviations (BUN → urea nitrogen) in search terms |
|
|
103
|
+
| `--json-full` | dump the complete internal representation (debugging) |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Quick start (Python)
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from aimedicalcoding import extract_gaps
|
|
111
|
+
|
|
112
|
+
result = extract_gaps("patient.xml") # CCDA — auto-detected
|
|
113
|
+
result = extract_gaps("bundle.json") # FHIR R4 Bundle — auto-detected
|
|
114
|
+
result = extract_gaps(raw_bytes, # bytes / str / path / file-like
|
|
115
|
+
systems=("loinc", "snomed", "rxnorm", "icd10cm", "cvx"))
|
|
116
|
+
|
|
117
|
+
len(result) # number of gaps
|
|
118
|
+
result.by_target # {"LOINC": 17, "SNOMED-CT": 30, "RxNorm": 4, ...}
|
|
119
|
+
result.by_pattern # {"LOCAL_CODE_ONLY": 15, "NULLFLAVOR": 32, ...}
|
|
120
|
+
result.gaps # list[dict] — one record per (item, missing terminology)
|
|
121
|
+
result.to_json() # the same JSON the CLI emits
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Lower-level access — parse to the intermediate representation without gap
|
|
125
|
+
analysis:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from aimedicalcoding.pipeline import parse_document
|
|
129
|
+
|
|
130
|
+
statements = parse_document(open("bundle.json", "rb").read())
|
|
131
|
+
for s in statements:
|
|
132
|
+
print(s.context, s.raw_text, [(c.system, c.code) for c in s.codings])
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## How it works
|
|
138
|
+
|
|
139
|
+
The library is built on two independent axes that meet at a format-agnostic
|
|
140
|
+
intermediate representation (IR):
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
FORMAT axis (adapters) TERMINOLOGY axis (extractors)
|
|
144
|
+
CCDA XML ──┐ LOINC SNOMED CT
|
|
145
|
+
FHIR R4 ───┤──► ClinicalStatement ──► RxNorm ICD-10-CM CVX
|
|
146
|
+
│ (the IR)
|
|
147
|
+
(auto-detected) (identical for every input format)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
1. **Detect** — XML with a `<ClinicalDocument>` root → CCDA adapter; JSON with
|
|
151
|
+
`"resourceType": "Bundle"` → FHIR adapter.
|
|
152
|
+
2. **Parse to IR** — the adapter walks the document and emits one
|
|
153
|
+
`ClinicalStatement` per clinical item: every coding found on the item (root
|
|
154
|
+
code *plus* all translations / `coding[]` entries, each canonicalized by
|
|
155
|
+
system), the human labels, the value (quantity or coded answer), specimen,
|
|
156
|
+
panel membership, timestamps.
|
|
157
|
+
3. **Extract gaps** — each terminology extractor examines the statements
|
|
158
|
+
relevant to it (labs/vitals for LOINC, problems/procedures/reactions for
|
|
159
|
+
SNOMED, medications for RxNorm, diagnoses for ICD-10-CM, immunizations for
|
|
160
|
+
CVX), validates any code present (check digits, format), and classifies
|
|
161
|
+
everything else into a gap pattern.
|
|
162
|
+
4. **Serialize** — one JSON record per (item × missing terminology), identical
|
|
163
|
+
schema for CCDA and FHIR input.
|
|
164
|
+
|
|
165
|
+
### What each adapter understands
|
|
166
|
+
|
|
167
|
+
**CCDA**: document/section/entry structure, `<code>` + `<translation>`
|
|
168
|
+
scan-and-identify, `nullFlavor`, `<originalText>` reference resolution into the
|
|
169
|
+
narrative, organizer → panel linkage, specimen, interpretation and reference
|
|
170
|
+
ranges, narrative-table fallback for sections without structured entries.
|
|
171
|
+
|
|
172
|
+
**FHIR R4 Bundle**: routing by resource type + `Observation.category` (with
|
|
173
|
+
section-tag fallback), `CodeableConcept.coding[]` as primary + translations,
|
|
174
|
+
`valueQuantity` / `valueCodeableConcept` / `valueString` / `dataAbsentReason`,
|
|
175
|
+
`hasMember` / `DiagnosticReport.result` panel linkage (children inherit context
|
|
176
|
+
and specimen), `medicationReference → Medication.code` and
|
|
177
|
+
`specimen → Specimen.type` resolution across the Bundle, per-reaction allergy
|
|
178
|
+
statements, and normalization of `"unknown"` null-marker strings.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Gap patterns
|
|
183
|
+
|
|
184
|
+
Each record's `pattern` says *why* the code is missing — which determines how
|
|
185
|
+
to fix it:
|
|
186
|
+
|
|
187
|
+
| Pattern | What's wrong | Recovery route |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `LOCAL_CODE_ONLY` | a local/in-house/CPT code, no standard code | crosswalk local → target, else text search |
|
|
190
|
+
| `NULLFLAVOR` | code explicitly declared unknown (`nullFlavor`, `"unknown"`, `dataAbsentReason`) | resolve the recovered label |
|
|
191
|
+
| `DISPLAY_ONLY` | only a display name, no code and no absence marker | resolve the label |
|
|
192
|
+
| `NARRATIVE_ONLY` | exists only as narrative table text (CCDA, opt-in) | text search, low priority |
|
|
193
|
+
| `BAD_CODE` | right shape, fails validation (LOINC Luhn · SNOMED Verhoeff · ICD format) | check-digit / format correction (suggested fix included) |
|
|
194
|
+
| `ICD_ONLY` | ICD present, SNOMED missing | official ICD ↔ SNOMED map |
|
|
195
|
+
| `SNOMED_ONLY` | SNOMED present, ICD-10-CM missing | official SNOMED → ICD-10-CM map |
|
|
196
|
+
| `NDC_ONLY` | NDC present, RxNorm/CVX missing | NDC ↔ RxNorm / CDC NDC ↔ CVX table |
|
|
197
|
+
| `RXNORM_ONLY` | RxNorm vaccine product, CVX missing | CVX ↔ RxNorm map |
|
|
198
|
+
| `MULTUM_ONLY` / `FDB_ONLY` / `MEDISPAN_ONLY` | proprietary drug vocabulary, RxNorm missing | vendor ↔ RxNorm via UMLS |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Output
|
|
203
|
+
|
|
204
|
+
The JSON envelope:
|
|
205
|
+
|
|
206
|
+
```jsonc
|
|
207
|
+
{
|
|
208
|
+
"source": "patient.xml",
|
|
209
|
+
"source_format": "ccda", // or "fhir" — auto-detected
|
|
210
|
+
"targets_checked": ["LOINC", "SNOMED-CT", "RxNorm", "ICD-10-CM", "CVX"],
|
|
211
|
+
"gap_count": 4,
|
|
212
|
+
"by_pattern": { "LOCAL_CODE_ONLY": 1, "NULLFLAVOR": 3 },
|
|
213
|
+
"by_target": { "LOINC": 1, "SNOMED-CT": 3 },
|
|
214
|
+
"gaps": [ ... ]
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Each gap record separates four concerns — the classification, the facts, the
|
|
219
|
+
plan, and the (pending) answer:
|
|
220
|
+
|
|
221
|
+
```jsonc
|
|
222
|
+
{
|
|
223
|
+
"id": 1,
|
|
224
|
+
"target": { "system": "LOINC", "slot": "code" }, // what's missing, and in which slot
|
|
225
|
+
"pattern": "LOCAL_CODE_ONLY", // why
|
|
226
|
+
"section": { "code": "30954-2", "name": "Results", "system": "LOINC" },
|
|
227
|
+
|
|
228
|
+
"observation": { // the facts, verbatim
|
|
229
|
+
"code": {
|
|
230
|
+
"display_text": "LIPID PANEL",
|
|
231
|
+
"original_text": "LIPID PANEL",
|
|
232
|
+
"codings": [
|
|
233
|
+
{ "code": null, "system": "LOINC", "system_raw": "http://loinc.org" },
|
|
234
|
+
{ "code": "80061", "system": "CPT", "system_raw": "http://www.ama-assn.org/go/cpt" }
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
"value": null,
|
|
238
|
+
"qualifiers": { "specimen": { "value": "Bld", "confidence": "low" }, "...": "..." }
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
"recovery": { // the plan
|
|
242
|
+
"search_terms": ["LIPID PANEL", "lipid panel"],
|
|
243
|
+
"match_key": "component:LIPID PANEL | specimen:Bld | property:? | scale:? | method:-",
|
|
244
|
+
"crosswalk": { "from_system": "http://www.ama-assn.org/go/cpt", "from_code": "80061" },
|
|
245
|
+
"suggested_action": "Try (source_system, local_code) crosswalk FIRST; ..."
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
"provenance": { "location": { "type": "fhirpath", "path": "..." } },
|
|
249
|
+
"resolution": { "status": "pending", "code": null, "...": "..." } // filled by your recovery step
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Reading it: this lab was coded with CPT `80061` but its LOINC slot is empty
|
|
254
|
+
(`code: null` — the source wrote a null marker). The record hands you the CPT
|
|
255
|
+
crosswalk candidate, the text to search with, and the LOINC axis hints
|
|
256
|
+
(specimen/property/scale) to constrain candidates — everything a
|
|
257
|
+
crosswalk-database or LLM step needs to fill `resolution`.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Scope and status
|
|
262
|
+
|
|
263
|
+
- Emits **gap detections + recovery evidence**. It does not itself query
|
|
264
|
+
terminology databases or LLMs — `resolution` is intentionally left `pending`
|
|
265
|
+
for your downstream step.
|
|
266
|
+
- Terminologies checked today: LOINC, SNOMED CT, RxNorm, ICD-10-CM, CVX.
|
|
267
|
+
CPT / HCPCS / ICD-10-PCS are planned.
|
|
268
|
+
- Validation included: LOINC Luhn check digits, SNOMED Verhoeff check digits
|
|
269
|
+
(incl. long-format extension SCTIDs), ICD-10-CM format rules, CVX shape.
|
|
270
|
+
- Code-system identification covers HL7 OIDs, FHIR URIs, `urn:oid:` forms and
|
|
271
|
+
free-text names for 20+ vocabularies (Epic/local OIDs are recognized as
|
|
272
|
+
local codes, HL7 structural vocabularies are excluded from gap detection).
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT — © 2026 Trove Health.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
aimedicalcoding/__init__.py,sha256=viWad5P5w8ZOXDVhZZ2kXMpTTkUt-f4A28IZqDfHJm8,1182
|
|
2
|
+
aimedicalcoding/__main__.py,sha256=swkEmrnXebaTGwsIKonb6XfrAi_Movq0uKbU91wb8oE,12586
|
|
3
|
+
aimedicalcoding/api.py,sha256=bADfs8Nm0bARYDxDcLb2ML3Mn2yNkgehhCKzznhwEpA,6128
|
|
4
|
+
aimedicalcoding/serialize.py,sha256=3BaJIJj32tYGhcciSwBnwq4lPcs6nBNeofvw84TD4To,10644
|
|
5
|
+
aimedicalcoding/adapters/__init__.py,sha256=XKnZJyHCIkjLTKHKUfalztZt87KYRNRyOY4zGRxDmgc,64
|
|
6
|
+
aimedicalcoding/adapters/base.py,sha256=gMfYxK3nho2LjL7h1Y14HrTZ0KqRDAGeQicBEBKo5ug,790
|
|
7
|
+
aimedicalcoding/adapters/ccda/__init__.py,sha256=PbdC_mmRW5BrxKsWS4JlE9H5psPAvhRprmZfOQ3Gzo8,63
|
|
8
|
+
aimedicalcoding/adapters/ccda/adapter.py,sha256=J7xcjZF4KVqxAVoZAuWGhflHxJ7qkS6wicGLx9BWH2A,12687
|
|
9
|
+
aimedicalcoding/adapters/ccda/codes.py,sha256=gYDZP5jVWR7gUrUzP-jB9j62U_9JDdqWiC-k3FH3Kfo,7125
|
|
10
|
+
aimedicalcoding/adapters/ccda/narrative.py,sha256=7tC5TCtj3WbmeWXmbXPMLRby0foE9Vn5xBlYEuXsAoI,4162
|
|
11
|
+
aimedicalcoding/adapters/ccda/sections.py,sha256=TnKXVUtS6XsxhZZYEMAwv3YciXsmIcUIJeGJoFwkZ3M,2145
|
|
12
|
+
aimedicalcoding/adapters/custom_json/__init__.py,sha256=QPsWZfo1hSTCOp64UiAbmfjtIULKDyjrlSUDVH9YYVc,75
|
|
13
|
+
aimedicalcoding/adapters/custom_json/adapter.py,sha256=8cVT8dFeB1YlKJUmDCGr28I-xmo3LGfF5_kPfJLATWg,1017
|
|
14
|
+
aimedicalcoding/adapters/fhir/__init__.py,sha256=RDoieLHyrnWqf6dqs0UVwQ-JY8ukm5Ds9kxrGsXsJBY,63
|
|
15
|
+
aimedicalcoding/adapters/fhir/adapter.py,sha256=8b68P2hNQEKrmyHmE2_EHo7M5gQSYnBFir3yvHuEbIY,14955
|
|
16
|
+
aimedicalcoding/adapters/fhir/codes.py,sha256=w-eDA5OMJS2duBIHwI01phwZBhjPDmFrMOBXnYK_g28,8368
|
|
17
|
+
aimedicalcoding/adapters/fhir/mapping.py,sha256=dtESD28QZ3q1ze_E77yYkTWxEOg-WMC1TfRHr-hy0u4,3056
|
|
18
|
+
aimedicalcoding/core/__init__.py,sha256=cMtmrbXfZir5v-9_6bzf94wdmyKVFReJvZ4QKGcPAgw,694
|
|
19
|
+
aimedicalcoding/core/checkdigits.py,sha256=54vcbScOxG21ngGK7tsaS7d0ZjD-LppUuXvkxBJjO4s,3615
|
|
20
|
+
aimedicalcoding/core/models.py,sha256=VlRYtkNJQeF_vdc9TVT9BP4OU3JaKbNxwVfASdp1Pdw,6654
|
|
21
|
+
aimedicalcoding/core/system_registry.py,sha256=vgtUzsW8006ohxYR5bRh04C4InPzxGDrDQ6QXDOYmNs,8024
|
|
22
|
+
aimedicalcoding/pipeline/__init__.py,sha256=KGE1IpfLT62r9Iv1MGEhrutYRO6_q4lKv36vOnIjS3I,108
|
|
23
|
+
aimedicalcoding/pipeline/orchestrator.py,sha256=p-PbGhM5KXWW9f-Ocy8Ds-FMglV--9MirSB7QsOFriQ,1136
|
|
24
|
+
aimedicalcoding/recovery/__init__.py,sha256=kDE5TkBDET4ZyQ4aNsDYCnXUGhZ7PuNnG6gfkmgyt6I,68
|
|
25
|
+
aimedicalcoding/recovery/engine.py,sha256=-Ec8H6n2RN2ZfzgKkao091E5QBujN3nXiFXH_a4W8u4,620
|
|
26
|
+
aimedicalcoding/systems/__init__.py,sha256=clo-oaRc880EXmUrAtdGQ29Fam88r7Ad3joi4MyjLgI,74
|
|
27
|
+
aimedicalcoding/systems/base.py,sha256=-ZugvfiyOLU7NXogAB1bujoaY_s6JbH5EbX3cRXjg18,1419
|
|
28
|
+
aimedicalcoding/systems/cvx/__init__.py,sha256=7y8Tf7hUR1E478oD7blqrGo-h82A89foK0V6eKcgKak,710
|
|
29
|
+
aimedicalcoding/systems/cvx/handler.py,sha256=JOobyneuFF3vJXxK-aF6WEpxE7pEHOhq8zwfmSdfEQM,1969
|
|
30
|
+
aimedicalcoding/systems/cvx/normalize.py,sha256=FrHn8s3Sajdo0DX0DGJzbC6IyxQWOSyuDomLBdNOpQk,2343
|
|
31
|
+
aimedicalcoding/systems/cvx/patterns.py,sha256=y5n8_noaWxetMRugH_AHTeZi76tCda4oYVPW2Y9DQ-8,5391
|
|
32
|
+
aimedicalcoding/systems/icd10cm/__init__.py,sha256=bAlE4-1qctHJKiut_uQsEkOu0MLxfGTAEL4nimyHV8s,647
|
|
33
|
+
aimedicalcoding/systems/icd10cm/handler.py,sha256=S6vs634eGEiaJguKFQldQRAXZ56QEai-iGTiwivpwZI,1999
|
|
34
|
+
aimedicalcoding/systems/icd10cm/icdformat.py,sha256=t81aq7YiaKzfuYtjSk8GQFcd3XXAc5790M-A-FKEyGQ,1422
|
|
35
|
+
aimedicalcoding/systems/icd10cm/patterns.py,sha256=JgjmpT5AGJoyQF6VFueIUolqJzLzLgEtS3mwKDX1s80,5446
|
|
36
|
+
aimedicalcoding/systems/loinc/__init__.py,sha256=OimAW2Mz1u6P0v1-SRN_rYYpZoltJprjkxg3mR4VTFE,573
|
|
37
|
+
aimedicalcoding/systems/loinc/axes.py,sha256=YD8m2tvlHp0xQDuWDgDU1OjhpkXhpnPPX8f9YYYDV5c,8715
|
|
38
|
+
aimedicalcoding/systems/loinc/handler.py,sha256=2x1VI1YqxIg0pa4vapSWC533pJ8AK8lyBvFI6mXnhEo,3207
|
|
39
|
+
aimedicalcoding/systems/loinc/normalize.py,sha256=M6KGYuvSgIipMgsP4I7LW49aR7Owpw_kOS5Z6UEdmWE,2545
|
|
40
|
+
aimedicalcoding/systems/loinc/patterns.py,sha256=dG8_Cf9hq2xdQFYLfhWvF28V1Oyo9bVSqFodw9pZeoU,6714
|
|
41
|
+
aimedicalcoding/systems/rxnorm/__init__.py,sha256=JOz1CgTTgFRDAW82HYzWYdcc1vRNPW-DXlF52613Al4,611
|
|
42
|
+
aimedicalcoding/systems/rxnorm/handler.py,sha256=FpBYBhovnHfTyXMMiXFHKgJFb_OvHLaMGOpWW7SkZaM,1991
|
|
43
|
+
aimedicalcoding/systems/rxnorm/normalize.py,sha256=UJDu50yMKbLpmasI8oTlpH7GEQOXBZEGzDg5qjytK9c,3034
|
|
44
|
+
aimedicalcoding/systems/rxnorm/patterns.py,sha256=cCJ-27q3_1ITaGHxwrFXDMNFK8uO7GSYR8KjrfUdAkI,6064
|
|
45
|
+
aimedicalcoding/systems/snomed/__init__.py,sha256=I0X2Oc5-jZ4mHKdvSN4xlxucwYwHUmDirvp7X5TeMxI,715
|
|
46
|
+
aimedicalcoding/systems/snomed/handler.py,sha256=NI4zpIBvnysd_hKPjVNyyMWulZwhuBw9yZa8HfZn9ks,2208
|
|
47
|
+
aimedicalcoding/systems/snomed/hierarchy.py,sha256=vNFMCXmOlvWt0WZqeaXLZNf9uV1e8OPQDSQzxY1Ohss,3954
|
|
48
|
+
aimedicalcoding/systems/snomed/patterns.py,sha256=YU6ZpHo-Kw7BFsG5QZJ2AW2nGIYH7qxehyZiMxvsjMw,7341
|
|
49
|
+
aimedicalcoding/systems/snomed/qualifiers.py,sha256=zz_CCx7vW7cAz_dc7aWPtibtiMC7-oLQPtE7sUdXwOw,2815
|
|
50
|
+
aimedicalcoding/terminology/__init__.py,sha256=KVoIA1CCQn_v-1elbVEqDhy1j3oTPuxoQk9uRKTUv1k,77
|
|
51
|
+
aimedicalcoding/terminology/service.py,sha256=9EllY7NhWKFAgu6akhLEG4oMmqjCqKfrAe93KCT8I0Q,1289
|
|
52
|
+
aimedicalcoding-0.3.1.dist-info/licenses/LICENSE,sha256=K6yywBLgHo248RJhXjwoW1n9bDoYgyItdlBixb6iwGY,1069
|
|
53
|
+
aimedicalcoding-0.3.1.dist-info/METADATA,sha256=1DEr1j7M3HO9-U96MGdDQrPM47lKmw2_Ze7G43VUjg4,11418
|
|
54
|
+
aimedicalcoding-0.3.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
55
|
+
aimedicalcoding-0.3.1.dist-info/entry_points.txt,sha256=qYa-Olxsrca0nMczzXgmvRh1KKgI2xBBLbsM-IGst1g,66
|
|
56
|
+
aimedicalcoding-0.3.1.dist-info/top_level.txt,sha256=Oyr7k6Ihz10OEQ5Cw_uOCPFbOE14KCcMeTtjbX4MHi0,16
|
|
57
|
+
aimedicalcoding-0.3.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trove Health
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aimedicalcoding
|