hccinfhir 0.1.1__py3-none-any.whl → 0.1.3__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.
- hccinfhir/__init__.py +47 -1
- hccinfhir/data/ra_eligible_cpt_hcpcs_2026.csv +5130 -5107
- hccinfhir/data/ra_hierarchies_2026.csv +12 -0
- hccinfhir/extractor.py +2 -2
- hccinfhir/extractor_837.py +95 -28
- hccinfhir/filter.py +19 -21
- hccinfhir/hccinfhir.py +2 -1
- hccinfhir/sample_utils.py +252 -0
- hccinfhir/samples.py +252 -0
- {hccinfhir-0.1.1.dist-info → hccinfhir-0.1.3.dist-info}/METADATA +32 -3
- {hccinfhir-0.1.1.dist-info → hccinfhir-0.1.3.dist-info}/RECORD +13 -11
- {hccinfhir-0.1.1.dist-info → hccinfhir-0.1.3.dist-info}/WHEEL +0 -0
- {hccinfhir-0.1.1.dist-info → hccinfhir-0.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -330,6 +330,12 @@ cc_parent,cc_child,model_domain,model_version,model_fullname
|
|
|
330
330
|
19,21,CMS-HCC,V28,V28115H1
|
|
331
331
|
19,22,CMS-HCC,V28,V28115H1
|
|
332
332
|
19,23,CMS-HCC,V28,V28115H1
|
|
333
|
+
191,180,CMS-HCC,V28,V28115H1
|
|
334
|
+
191,181,CMS-HCC,V28,V28115H1
|
|
335
|
+
191,182,CMS-HCC,V28,V28115H1
|
|
336
|
+
191,192,CMS-HCC,V28,V28115H1
|
|
337
|
+
191,253,CMS-HCC,V28,V28115H1
|
|
338
|
+
191,254,CMS-HCC,V28,V28115H1
|
|
333
339
|
195,196,CMS-HCC,V28,V28115H1
|
|
334
340
|
20,21,CMS-HCC,V28,V28115H1
|
|
335
341
|
20,22,CMS-HCC,V28,V28115H1
|
|
@@ -340,6 +346,12 @@ cc_parent,cc_child,model_domain,model_version,model_fullname
|
|
|
340
346
|
211,213,CMS-HCC,V28,V28115H1
|
|
341
347
|
212,213,CMS-HCC,V28,V28115H1
|
|
342
348
|
22,23,CMS-HCC,V28,V28115H1
|
|
349
|
+
221,222,CMS-HCC,V28,V28115H1
|
|
350
|
+
221,223,CMS-HCC,V28,V28115H1
|
|
351
|
+
221,224,CMS-HCC,V28,V28115H1
|
|
352
|
+
221,225,CMS-HCC,V28,V28115H1
|
|
353
|
+
221,226,CMS-HCC,V28,V28115H1
|
|
354
|
+
221,227,CMS-HCC,V28,V28115H1
|
|
343
355
|
223,224,CMS-HCC,V28,V28115H1
|
|
344
356
|
223,225,CMS-HCC,V28,V28115H1
|
|
345
357
|
223,226,CMS-HCC,V28,V28115H1
|
hccinfhir/extractor.py
CHANGED
|
@@ -36,11 +36,11 @@ def extract_sld(
|
|
|
36
36
|
raise ValueError(f'Format must be either "837" or "fhir", got {format}')
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def extract_sld_list(data: Union[List[str], List[dict]],
|
|
39
|
+
def extract_sld_list(data: Union[List[str], List[dict]],
|
|
40
|
+
format: Literal["837", "fhir"] = "fhir") -> List[ServiceLevelData]:
|
|
40
41
|
"""Extract SLDs from a list of FHIR EOBs"""
|
|
41
42
|
output = []
|
|
42
43
|
for item in data:
|
|
43
|
-
|
|
44
44
|
try:
|
|
45
45
|
output.extend(extract_sld(item, format))
|
|
46
46
|
except TypeError as e:
|
hccinfhir/extractor_837.py
CHANGED
|
@@ -38,9 +38,11 @@ def parse_amount(amount_str: str) -> Optional[float]:
|
|
|
38
38
|
except (ValueError, TypeError):
|
|
39
39
|
return None
|
|
40
40
|
|
|
41
|
-
def get_segment_value(segment: List[str],
|
|
41
|
+
def get_segment_value(segment: List[str],
|
|
42
|
+
index: int,
|
|
43
|
+
default: Optional[str] = None) -> Optional[str]:
|
|
42
44
|
"""Safely get value from segment at given index"""
|
|
43
|
-
return segment[index] if len(segment) > index else
|
|
45
|
+
return segment[index] if len(segment) > index else default
|
|
44
46
|
|
|
45
47
|
def parse_diagnosis_codes(segment: List[str]) -> Dict[str, str]:
|
|
46
48
|
"""Extract diagnosis codes from HI segment"""
|
|
@@ -49,7 +51,11 @@ def parse_diagnosis_codes(segment: List[str]) -> Dict[str, str]:
|
|
|
49
51
|
if ':' not in element:
|
|
50
52
|
continue
|
|
51
53
|
qualifier, code = element.split(':')[:2]
|
|
52
|
-
if qualifier in
|
|
54
|
+
if qualifier in {'ABK', 'ABF', 'ABJ'}: # ICD-10 qualifiers
|
|
55
|
+
# ABK: Primary Diagnosis
|
|
56
|
+
# ABF: Secondary Diagnosis
|
|
57
|
+
# ABJ: Admitting Diagnosis
|
|
58
|
+
# NOTE: In Risk Adjustment, we do not differentiate between primary and secondary diagnoses
|
|
53
59
|
dx_lookup[str(pos)] = code
|
|
54
60
|
return dx_lookup
|
|
55
61
|
|
|
@@ -61,17 +67,35 @@ def process_service_line(segments: List[List[str]], start_index: int) -> tuple[O
|
|
|
61
67
|
for seg in segments[start_index:]:
|
|
62
68
|
if seg[0] in ['LX', 'CLM', 'SE']:
|
|
63
69
|
break
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
if len(seg) > 3:
|
|
71
|
+
if seg[0] == 'LIN' and seg[2] == 'N4':
|
|
72
|
+
ndc = seg[3]
|
|
73
|
+
elif (seg[0] == 'DTP' and
|
|
74
|
+
seg[1] in {'472', '434'} and
|
|
75
|
+
seg[2].endswith('D8')):
|
|
76
|
+
# 472: Service Date
|
|
77
|
+
# 434: From Date in 837I
|
|
78
|
+
# These are not included currently: 435: To Date in 837I, 096 Discharge Date
|
|
79
|
+
if seg[3]:
|
|
80
|
+
service_date = parse_date(seg[3][:8] if len(seg[3]) >= 8 else seg[3])
|
|
68
81
|
if ndc and service_date:
|
|
69
82
|
break
|
|
70
83
|
|
|
71
84
|
return ndc, service_date
|
|
72
85
|
|
|
73
86
|
def extract_sld_837(content: str) -> List[ServiceLevelData]:
|
|
74
|
-
"""Extract service level data from 837 Professional or Institutional claims
|
|
87
|
+
"""Extract service level data from 837 Professional or Institutional claims
|
|
88
|
+
|
|
89
|
+
Structure:
|
|
90
|
+
Billing Provider (2000A)
|
|
91
|
+
└── Subscriber (2000B)
|
|
92
|
+
└── Patient (2000C) [if needed]
|
|
93
|
+
└── Claim (2300)
|
|
94
|
+
├── Service Line 1 (2400)
|
|
95
|
+
├── Service Line 2 (2400)
|
|
96
|
+
└── Service Line N (2400)
|
|
97
|
+
|
|
98
|
+
"""
|
|
75
99
|
if not content:
|
|
76
100
|
raise ValueError("Input X12 data cannot be empty")
|
|
77
101
|
|
|
@@ -100,7 +124,7 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
|
|
|
100
124
|
seg_id = segment[0]
|
|
101
125
|
|
|
102
126
|
# Process NM1 segments (Provider and Patient info)
|
|
103
|
-
if seg_id == 'NM1':
|
|
127
|
+
if seg_id == 'NM1' and len(segment) > 1:
|
|
104
128
|
if segment[1] == 'IL': # Subscriber/Patient
|
|
105
129
|
current_data.patient_id = get_segment_value(segment, 9)
|
|
106
130
|
in_claim_loop = False
|
|
@@ -112,7 +136,7 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
|
|
|
112
136
|
current_data.billing_provider_npi = get_segment_value(segment, 9)
|
|
113
137
|
|
|
114
138
|
# Process Provider Specialty
|
|
115
|
-
elif seg_id == 'PRV' and segment[1] == 'PE' and in_rendering_provider_loop:
|
|
139
|
+
elif seg_id == 'PRV' and len(segment) > 1 and segment[1] == 'PE' and in_rendering_provider_loop:
|
|
116
140
|
current_data.provider_specialty = get_segment_value(segment, 3)
|
|
117
141
|
|
|
118
142
|
# Process Claim Information
|
|
@@ -122,29 +146,72 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
|
|
|
122
146
|
current_data.claim_id = segment[1] if len(segment) > 1 else None
|
|
123
147
|
|
|
124
148
|
# Parse facility and service type for institutional claims
|
|
125
|
-
if claim_type == "837I" and len(segment) > 5 and ':' in segment[5]:
|
|
126
|
-
current_data.facility_type = segment[5][0]
|
|
149
|
+
if claim_type == "837I" and len(segment) > 5 and segment[5] and ':' in segment[5]:
|
|
150
|
+
current_data.facility_type = segment[5][0] if segment[5] else None
|
|
127
151
|
current_data.service_type = segment[5][1] if len(segment[5]) > 1 else None
|
|
128
152
|
|
|
129
153
|
# Process Diagnosis Codes
|
|
130
154
|
elif seg_id == 'HI' and in_claim_loop:
|
|
131
|
-
|
|
155
|
+
# In 837I, there can be multiple HI segments in the claim
|
|
156
|
+
# Also, in 837I, diagnosis position does not matter
|
|
157
|
+
# We will use continuous numbering for diagnosis codes
|
|
158
|
+
# use the last dx_lookup position as the starting position, and update
|
|
159
|
+
hi_segment = parse_diagnosis_codes(segment)
|
|
160
|
+
hi_segment_realigned = {
|
|
161
|
+
str(int(pos) + len(current_data.dx_lookup)): code
|
|
162
|
+
for pos, code in hi_segment.items()
|
|
163
|
+
}
|
|
164
|
+
current_data.dx_lookup.update(hi_segment_realigned)
|
|
132
165
|
|
|
133
166
|
# Process Service Lines
|
|
167
|
+
#
|
|
168
|
+
# SV1 (Professional Services):
|
|
169
|
+
# SV101 (Required) - Procedure Code Composite: HC qualifier + 5-digit HCPCS code, supports up to 4 HCPCS modifiers
|
|
170
|
+
# SV102 (Required) - Charge Amount: Format 99999999.99
|
|
171
|
+
# SV103 (Required) - Unit Type: F2 (International Unit) or UN (Units)
|
|
172
|
+
# SV104 (Required) - Unit Count: Format 9999.99 (decimals allowed)
|
|
173
|
+
# SV105 (Situational) - Place of Service Code: Required for First Steps claims
|
|
174
|
+
# SV107 (Situational) - Diagnosis Code Pointer: Links to HI segment in 2300 loop, valid values 1-8
|
|
175
|
+
#
|
|
176
|
+
# SV2 (Institutional Services):
|
|
177
|
+
# SV201 (Required) - Revenue Code: Facility-specific revenue code for service rendered
|
|
178
|
+
# SV202 (Required) - Procedure Code Composite: HC qualifier + 5-digit HCPCS code, supports up to 4 HCPCS modifiers
|
|
179
|
+
# SV203 (Required) - Charge Amount: Format 99999999.99
|
|
180
|
+
# SV204 (Required) - Unit Type: DA (Days) or UN (Units)
|
|
181
|
+
# SV205 (Required) - Unit Count: Format 9999999.999 (whole numbers only - fractional quantities not recognized)
|
|
182
|
+
# NOTE: Diagnosis Code Pointer is not supported for SV2
|
|
183
|
+
#
|
|
134
184
|
elif seg_id in ['SV1', 'SV2'] and in_claim_loop:
|
|
135
|
-
# Parse procedure info
|
|
136
|
-
proc_info = segment[1].split(':')
|
|
137
|
-
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
138
|
-
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
139
185
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if
|
|
147
|
-
|
|
186
|
+
linked_diagnoses = []
|
|
187
|
+
|
|
188
|
+
if seg_id == 'SV1':
|
|
189
|
+
# SV1 Professional Service: SV101=procedure, SV104=quantity, SV106=place_of_service
|
|
190
|
+
proc_info = get_segment_value(segment, 1, '').split(':')
|
|
191
|
+
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
192
|
+
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
193
|
+
quantity = parse_amount(get_segment_value(segment, 4))
|
|
194
|
+
place_of_service = get_segment_value(segment, 5)
|
|
195
|
+
# Get diagnosis pointers and linked diagnoses
|
|
196
|
+
dx_pointers = get_segment_value(segment, 7, '')
|
|
197
|
+
linked_diagnoses = [
|
|
198
|
+
current_data.dx_lookup[pointer]
|
|
199
|
+
for pointer in (dx_pointers.split(':') if dx_pointers else [])
|
|
200
|
+
if pointer in current_data.dx_lookup
|
|
201
|
+
]
|
|
202
|
+
else:
|
|
203
|
+
# SV2 Institutional Service: SV201=revenue, SV202=procedure, SV205=quantity
|
|
204
|
+
# Revenue code in SV201
|
|
205
|
+
revenue_code = get_segment_value(segment, 1)
|
|
206
|
+
# Procedure code in SV202
|
|
207
|
+
proc_info = get_segment_value(segment, 2, '').split(':')
|
|
208
|
+
procedure_code = proc_info[1] if len(proc_info) > 1 else None
|
|
209
|
+
modifiers = proc_info[2:] if len(proc_info) > 2 else []
|
|
210
|
+
# Quantity in SV205
|
|
211
|
+
quantity = parse_amount(get_segment_value(segment, 5))
|
|
212
|
+
place_of_service = None # Not applicable for institutional
|
|
213
|
+
# linked diagnoses are not supported for SV2
|
|
214
|
+
|
|
148
215
|
|
|
149
216
|
# Get service line details
|
|
150
217
|
ndc, service_date = process_service_line(segments, i)
|
|
@@ -154,7 +221,7 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
|
|
|
154
221
|
claim_id=current_data.claim_id,
|
|
155
222
|
procedure_code=procedure_code,
|
|
156
223
|
linked_diagnosis_codes=linked_diagnoses,
|
|
157
|
-
claim_diagnosis_codes=list(current_data.dx_lookup.values()),
|
|
224
|
+
claim_diagnosis_codes=list(current_data.dx_lookup.values()), # this is used for risk adjustment
|
|
158
225
|
claim_type=current_data.claim_type,
|
|
159
226
|
provider_specialty=current_data.provider_specialty,
|
|
160
227
|
performing_provider_npi=current_data.performing_provider_npi,
|
|
@@ -163,8 +230,8 @@ def extract_sld_837(content: str) -> List[ServiceLevelData]:
|
|
|
163
230
|
facility_type=current_data.facility_type,
|
|
164
231
|
service_type=current_data.service_type,
|
|
165
232
|
service_date=service_date,
|
|
166
|
-
place_of_service=
|
|
167
|
-
quantity=
|
|
233
|
+
place_of_service=place_of_service,
|
|
234
|
+
quantity=quantity,
|
|
168
235
|
modifiers=modifiers,
|
|
169
236
|
ndc=ndc,
|
|
170
237
|
allowed_amount=None
|
hccinfhir/filter.py
CHANGED
|
@@ -3,39 +3,37 @@ from hccinfhir.datamodels import ServiceLevelData
|
|
|
3
3
|
from hccinfhir.utils import load_proc_filtering
|
|
4
4
|
|
|
5
5
|
# use import importlib.resources to load the professional_cpt_fn file as a list of strings
|
|
6
|
-
professional_cpt_default_fn = '
|
|
6
|
+
professional_cpt_default_fn = 'ra_eligible_cpt_hcpcs_2025.csv'
|
|
7
7
|
professional_cpt_default = load_proc_filtering(professional_cpt_default_fn)
|
|
8
8
|
|
|
9
9
|
def apply_filter(
|
|
10
10
|
data: List[ServiceLevelData],
|
|
11
11
|
inpatient_tob: Set[str] = {'11X', '41X'},
|
|
12
|
-
outpatient_tob: Set[str] = {'12X', '13X', '43X', '71X', '73X', '76X', '77X', '85X'},
|
|
12
|
+
outpatient_tob: Set[str] = {'12X', '13X', '43X', '71X', '73X', '76X', '77X', '85X', '87X'},
|
|
13
13
|
professional_cpt: Set[str] = professional_cpt_default
|
|
14
14
|
) -> List[ServiceLevelData]:
|
|
15
15
|
# tob (Type of Bill) Filter is based on:
|
|
16
16
|
# https://www.hhs.gov/guidance/sites/default/files/hhs-guidance-documents/2012181486-wq-092916_ra_webinar_slides_5cr_092816.pdf
|
|
17
|
-
# https://www.hhs.gov/guidance/sites/default/files/hhs-guidance-documents/
|
|
17
|
+
# https://www.hhs.gov/guidance/sites/default/files/hhs-guidance-documents/FinalEncounterDataDiagnosisFilteringLogic.pdf
|
|
18
|
+
# https://www.cms.gov/files/document/encounterdatasystemedit20495andedit01415andtob87x07162021.pdf for 87X
|
|
19
|
+
# NOTE: If no facility_type or service_type, then the claim is professional, in our implementation.
|
|
20
|
+
# NOTE: The original CMS logic is for the "record" level, not the service level.
|
|
21
|
+
# Thus, when preparing the service level data, put all diagnosis codes into the diagnosis field.
|
|
18
22
|
|
|
19
|
-
# Break down the inpatient ToB into facility and service types
|
|
20
|
-
inpatient_facility_types = {tob[0] for tob in inpatient_tob}
|
|
21
|
-
inpatient_service_types = {tob[1] for tob in inpatient_tob}
|
|
22
|
-
|
|
23
|
-
# Break down the outpatient ToB into facility and service types
|
|
24
|
-
outpatient_facility_types = {tob[0] for tob in outpatient_tob}
|
|
25
|
-
outpatient_service_types = {tob[1] for tob in outpatient_tob}
|
|
26
|
-
|
|
27
|
-
# If ServiceLevelData has a facility_type and service_type, then filter the data based on the facility_type and service_type
|
|
28
|
-
# If not, then filter the data based on the CPT code
|
|
29
23
|
filtered_data = []
|
|
30
24
|
for item in data:
|
|
31
|
-
if item.facility_type
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
item_tob = '?' if item.facility_type is None else item.facility_type
|
|
26
|
+
item_tob += '?' if item.service_type is None else item.service_type
|
|
27
|
+
item_tob += 'X'
|
|
28
|
+
|
|
29
|
+
if '?' in item_tob: # professional claims
|
|
30
|
+
if item.procedure_code in professional_cpt:
|
|
37
31
|
filtered_data.append(item)
|
|
38
32
|
else:
|
|
39
|
-
if
|
|
40
|
-
filtered_data.append(item)
|
|
33
|
+
if item_tob in inpatient_tob:
|
|
34
|
+
filtered_data.append(item)
|
|
35
|
+
elif item_tob in outpatient_tob:
|
|
36
|
+
if item.procedure_code in professional_cpt:
|
|
37
|
+
filtered_data.append(item)
|
|
38
|
+
|
|
41
39
|
return filtered_data
|
hccinfhir/hccinfhir.py
CHANGED
|
@@ -81,8 +81,9 @@ class HCCInFHIR:
|
|
|
81
81
|
|
|
82
82
|
# Extract and filter service level data
|
|
83
83
|
sld_list = extract_sld_list(eob_list)
|
|
84
|
+
|
|
84
85
|
if self.filter_claims:
|
|
85
|
-
sld_list = apply_filter(sld_list, self.professional_cpt)
|
|
86
|
+
sld_list = apply_filter(sld_list, professional_cpt=self.professional_cpt)
|
|
86
87
|
|
|
87
88
|
# Calculate RAF score
|
|
88
89
|
unique_dx_codes = self._get_unique_diagnosis_codes(sld_list)
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sample Data Module for HCCInFHIR
|
|
3
|
+
|
|
4
|
+
This module provides easy access to sample data files for testing and demonstration purposes.
|
|
5
|
+
End users can call functions to retrieve sample EOB (Explanation of Benefits) and 837 claim data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.resources
|
|
9
|
+
import json
|
|
10
|
+
from typing import List, Dict, Any, Union, Optional
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SampleData:
|
|
15
|
+
"""
|
|
16
|
+
A class that provides access to sample data files included with the HCCInFHIR package.
|
|
17
|
+
|
|
18
|
+
This class allows end users to easily retrieve sample EOB and 837 claim data
|
|
19
|
+
for testing, development, and demonstration purposes.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def get_eob_sample(case_number: int = 1) -> Dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
Retrieve a specific EOB sample by case number.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
case_number: The case number (1, 2, or 3). Default is 1.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A dictionary containing the EOB data
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If case_number is not 1, 2, or 3
|
|
35
|
+
FileNotFoundError: If the sample file cannot be found
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> sample_data = SampleData.get_eob_sample(1)
|
|
39
|
+
>>> print(sample_data['resourceType'])
|
|
40
|
+
'ExplanationOfBenefit'
|
|
41
|
+
"""
|
|
42
|
+
if case_number not in [1, 2, 3]:
|
|
43
|
+
raise ValueError("case_number must be 1, 2, or 3")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
with importlib.resources.open_text('hccinfhir.samples', f'sample_eob_{case_number}.json') as f:
|
|
47
|
+
return json.load(f)
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
raise FileNotFoundError(f"Sample EOB case {case_number} not found")
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def get_eob_sample_list(limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
53
|
+
"""
|
|
54
|
+
Retrieve a list of EOB samples from the large sample file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
limit: Maximum number of samples to return. If None, returns all 200 samples.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A list of EOB data dictionaries
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
FileNotFoundError: If the sample file cannot be found
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> # Get first 10 samples
|
|
67
|
+
>>> samples = SampleData.get_eob_sample_list(limit=10)
|
|
68
|
+
>>> print(len(samples))
|
|
69
|
+
10
|
|
70
|
+
|
|
71
|
+
>>> # Get all 200 samples
|
|
72
|
+
>>> all_samples = SampleData.get_eob_sample_list()
|
|
73
|
+
>>> print(len(all_samples))
|
|
74
|
+
200
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
output = []
|
|
78
|
+
with importlib.resources.open_text('hccinfhir.samples', 'sample_eob_200.ndjson') as f:
|
|
79
|
+
for i, line in enumerate(f):
|
|
80
|
+
if limit is not None and i >= limit:
|
|
81
|
+
break
|
|
82
|
+
eob_data = json.loads(line)
|
|
83
|
+
output.append(eob_data)
|
|
84
|
+
return output
|
|
85
|
+
except FileNotFoundError:
|
|
86
|
+
raise FileNotFoundError("Sample EOB list file not found")
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def get_837_sample(case_number: int = 0) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Retrieve a specific 837 claim sample by case number.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
case_number: The case number (0 through 11). Default is 0.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A string containing the 837 X12 claim data
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If case_number is not between 0 and 11
|
|
101
|
+
FileNotFoundError: If the sample file cannot be found
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> sample_837 = SampleData.get_837_sample(0)
|
|
105
|
+
>>> print("ISA" in sample_837)
|
|
106
|
+
True
|
|
107
|
+
"""
|
|
108
|
+
if case_number < 0 or case_number > 11:
|
|
109
|
+
raise ValueError("case_number must be between 0 and 11")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
with importlib.resources.open_text('hccinfhir.samples', f'sample_837_{case_number}.txt') as f:
|
|
113
|
+
return f.read()
|
|
114
|
+
except FileNotFoundError:
|
|
115
|
+
raise FileNotFoundError(f"Sample 837 case {case_number} not found")
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def get_837_sample_list(case_numbers: Optional[List[int]] = None) -> List[str]:
|
|
119
|
+
"""
|
|
120
|
+
Retrieve multiple 837 claim samples.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
case_numbers: List of case numbers to retrieve. If None, returns all 12 samples.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
A list of 837 X12 claim data strings
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If any case_number is not between 0 and 11
|
|
130
|
+
FileNotFoundError: If any sample file cannot be found
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> # Get specific cases
|
|
134
|
+
>>> samples = SampleData.get_837_sample_list([0, 1, 2])
|
|
135
|
+
>>> print(len(samples))
|
|
136
|
+
3
|
|
137
|
+
|
|
138
|
+
>>> # Get all samples
|
|
139
|
+
>>> all_samples = SampleData.get_837_sample_list()
|
|
140
|
+
>>> print(len(all_samples))
|
|
141
|
+
12
|
|
142
|
+
"""
|
|
143
|
+
if case_numbers is None:
|
|
144
|
+
case_numbers = list(range(12)) # 0 through 11
|
|
145
|
+
|
|
146
|
+
# Validate case numbers
|
|
147
|
+
for case_num in case_numbers:
|
|
148
|
+
if case_num < 0 or case_num > 11:
|
|
149
|
+
raise ValueError(f"case_number {case_num} must be between 0 and 11")
|
|
150
|
+
|
|
151
|
+
output = []
|
|
152
|
+
for case_num in case_numbers:
|
|
153
|
+
try:
|
|
154
|
+
with importlib.resources.open_text('hccinfhir.samples', f'sample_837_{case_num}.txt') as f:
|
|
155
|
+
output.append(f.read())
|
|
156
|
+
except FileNotFoundError:
|
|
157
|
+
raise FileNotFoundError(f"Sample 837 case {case_num} not found")
|
|
158
|
+
|
|
159
|
+
return output
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def list_available_samples() -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Get information about all available sample data.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
A dictionary containing information about available samples
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> info = SampleData.list_available_samples()
|
|
171
|
+
>>> print(info['eob_samples'])
|
|
172
|
+
['sample_eob_1.json', 'sample_eob_2.json', 'sample_eob_3.json', 'sample_eob_200.ndjson']
|
|
173
|
+
"""
|
|
174
|
+
return {
|
|
175
|
+
"eob_samples": [
|
|
176
|
+
"sample_eob_1.json",
|
|
177
|
+
"sample_eob_2.json",
|
|
178
|
+
"sample_eob_3.json",
|
|
179
|
+
"sample_eob_200.ndjson"
|
|
180
|
+
],
|
|
181
|
+
"eob_case_numbers": [1, 2, 3],
|
|
182
|
+
"eob_list_size": 200,
|
|
183
|
+
"837_samples": [f"sample_837_{i}.txt" for i in range(12)],
|
|
184
|
+
"837_case_numbers": list(range(12)),
|
|
185
|
+
"description": {
|
|
186
|
+
"eob": "Explanation of Benefits (FHIR resources) for testing HCC calculations",
|
|
187
|
+
"837": "X12 837 claim data for testing claim processing"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Convenience functions for easy access
|
|
193
|
+
def get_eob_sample(case_number: int = 1) -> Dict[str, Any]:
|
|
194
|
+
"""
|
|
195
|
+
Convenience function to get an EOB sample.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
case_number: The case number (1, 2, or 3). Default is 1.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
A dictionary containing the EOB data
|
|
202
|
+
"""
|
|
203
|
+
return SampleData.get_eob_sample(case_number)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_eob_sample_list(limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
207
|
+
"""
|
|
208
|
+
Convenience function to get a list of EOB samples.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
limit: Maximum number of samples to return. If None, returns all 200 samples.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
A list of EOB data dictionaries
|
|
215
|
+
"""
|
|
216
|
+
return SampleData.get_eob_sample_list(limit)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_837_sample(case_number: int = 0) -> str:
|
|
220
|
+
"""
|
|
221
|
+
Convenience function to get an 837 claim sample.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
case_number: The case number (0 through 11). Default is 0.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
A string containing the 837 X12 claim data
|
|
228
|
+
"""
|
|
229
|
+
return SampleData.get_837_sample(case_number)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_837_sample_list(case_numbers: Optional[List[int]] = None) -> List[str]:
|
|
233
|
+
"""
|
|
234
|
+
Convenience function to get multiple 837 claim samples.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
case_numbers: List of case numbers to retrieve. If None, returns all 12 samples.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
A list of 837 X12 claim data strings
|
|
241
|
+
"""
|
|
242
|
+
return SampleData.get_837_sample_list(case_numbers)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def list_available_samples() -> Dict[str, Any]:
|
|
246
|
+
"""
|
|
247
|
+
Convenience function to get information about available samples.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
A dictionary containing information about available samples
|
|
251
|
+
"""
|
|
252
|
+
return SampleData.list_available_samples()
|