hccinfhir 0.2.3__py3-none-any.whl → 0.2.5__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/datamodels.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from pydantic import BaseModel, Field
2
- from typing import List, Optional, Literal, Dict, Set, TypedDict, Union
2
+ from typing import List, Optional, Literal, Dict, Set, TypedDict, Union, Any
3
+ from datetime import date
3
4
 
4
5
  # Define Model Name literal type
5
6
  ModelName = Literal[
@@ -203,6 +204,21 @@ class HCPCoveragePeriod(BaseModel):
203
204
  hcp_status: Optional[str] = None
204
205
  aid_codes: Optional[str] = None # REF*CE composite
205
206
 
207
+ def model_dump_with_dates(self, **kwargs) -> Dict[str, Any]:
208
+ """Return dict with date fields as date objects instead of strings.
209
+
210
+ Args:
211
+ **kwargs: Additional arguments passed to model_dump()
212
+
213
+ Returns:
214
+ Dict with start_date and end_date as date objects (if present)
215
+ """
216
+ data = self.model_dump(**kwargs)
217
+ for field in ('start_date', 'end_date'):
218
+ if data.get(field):
219
+ data[field] = date.fromisoformat(data[field])
220
+ return data
221
+
206
222
 
207
223
  class EnrollmentData(BaseModel):
208
224
  """
@@ -287,7 +303,8 @@ class EnrollmentData(BaseModel):
287
303
  # HCP (Health Care Plan) Info
288
304
  hcp_code: Current HCP code (HD04 first part)
289
305
  hcp_status: Current HCP status (HD04 second part)
290
- amount: Premium or cost share amount
306
+ amount_qualifier: AMT qualifier code (e.g., 'D' = premium, 'C1' = copay)
307
+ amount: Premium or cost share amount (numeric)
291
308
 
292
309
  # HCP History (multiple coverage periods)
293
310
  hcp_history: List of historical HCP coverage periods
@@ -367,7 +384,51 @@ class EnrollmentData(BaseModel):
367
384
  # HCP Info
368
385
  hcp_code: Optional[str] = None
369
386
  hcp_status: Optional[str] = None
370
- amount: Optional[str] = None
387
+ amount_qualifier: Optional[str] = None
388
+ amount: Optional[float] = None
371
389
 
372
390
  # HCP History
373
- hcp_history: List[HCPCoveragePeriod] = []
391
+ hcp_history: List[HCPCoveragePeriod] = []
392
+
393
+ def model_dump_with_dates(self, **kwargs) -> Dict[str, Any]:
394
+ """Return dict with date fields as date objects instead of strings.
395
+
396
+ Converts all YYYY-MM-DD string date fields to date objects.
397
+ Also converts dates in nested hcp_history items.
398
+
399
+ Args:
400
+ **kwargs: Additional arguments passed to model_dump()
401
+
402
+ Returns:
403
+ Dict with date fields as date objects (if present)
404
+
405
+ Example:
406
+ >>> enrollment = extract_enrollment_834(content)[0]
407
+ >>> data = enrollment.model_dump_with_dates()
408
+ >>> isinstance(data['dob'], date) # True
409
+ """
410
+ data = self.model_dump(**kwargs)
411
+
412
+ # EnrollmentData date fields
413
+ date_fields = (
414
+ 'report_date',
415
+ 'dob',
416
+ 'death_date',
417
+ 'coverage_start_date',
418
+ 'coverage_end_date',
419
+ 'fame_card_issue_date',
420
+ 'fame_redetermination_date',
421
+ 'fame_death_date',
422
+ )
423
+ for field in date_fields:
424
+ if data.get(field):
425
+ data[field] = date.fromisoformat(data[field])
426
+
427
+ # Convert dates in hcp_history items
428
+ if data.get('hcp_history'):
429
+ for hcp in data['hcp_history']:
430
+ for field in ('start_date', 'end_date'):
431
+ if hcp.get(field):
432
+ hcp[field] = date.fromisoformat(hcp[field])
433
+
434
+ return data
@@ -21,6 +21,21 @@ from hccinfhir.constants import (
21
21
  map_medicare_status_to_dual_code,
22
22
  map_aid_code_to_dual_status,
23
23
  )
24
+ from hccinfhir.utils import load_race_ethnicity
25
+
26
+ # Load race/ethnicity mapping at module level
27
+ _RACE_ETHNICITY_MAPPING: Optional[Dict[str, str]] = None
28
+
29
+
30
+ def _get_race_ethnicity_mapping() -> Dict[str, str]:
31
+ """Lazy load race/ethnicity mapping"""
32
+ global _RACE_ETHNICITY_MAPPING
33
+ if _RACE_ETHNICITY_MAPPING is None:
34
+ try:
35
+ _RACE_ETHNICITY_MAPPING = load_race_ethnicity()
36
+ except (FileNotFoundError, RuntimeError):
37
+ _RACE_ETHNICITY_MAPPING = {}
38
+ return _RACE_ETHNICITY_MAPPING
24
39
 
25
40
  # Constants
26
41
  TRANSACTION_TYPES = {"005010X220A1": "834"}
@@ -114,7 +129,8 @@ class MemberContext(BaseModel):
114
129
  # HCP Info
115
130
  hcp_code: Optional[str] = None
116
131
  hcp_status: Optional[str] = None
117
- amount: Optional[str] = None
132
+ amount_qualifier: Optional[str] = None
133
+ amount: Optional[float] = None
118
134
 
119
135
  # HCP History
120
136
  hcp_history: List[HCPContext] = []
@@ -206,6 +222,35 @@ def contains_any_keyword(text: str, keywords: set) -> bool:
206
222
  return any(kw in text_upper for kw in keywords)
207
223
 
208
224
 
225
+ def parse_race_code(raw_value: Optional[str]) -> Optional[str]:
226
+ """Parse race code from DMG05 and translate to human-readable name.
227
+
228
+ Handles formats like:
229
+ - ":RET:2135-2" -> "Hispanic or Latino"
230
+ - "2135-2" -> "Hispanic or Latino"
231
+ - "2106-3" -> "White"
232
+
233
+ Args:
234
+ raw_value: Raw race value from DMG segment
235
+
236
+ Returns:
237
+ Human-readable race/ethnicity name, or original value if not found
238
+ """
239
+ if not raw_value:
240
+ return None
241
+
242
+ # Extract code from formats like ":RET:2135-2" or "2135-2"
243
+ code = raw_value
244
+ if ':' in raw_value:
245
+ parts = raw_value.split(':')
246
+ # Take the last non-empty part which should be the code
247
+ code = next((p for p in reversed(parts) if p), raw_value)
248
+
249
+ # Look up in mapping
250
+ mapping = _get_race_ethnicity_mapping()
251
+ return mapping.get(code, raw_value)
252
+
253
+
209
254
  # ============================================================================
210
255
  # Dual Eligibility Logic
211
256
  # ============================================================================
@@ -248,7 +293,7 @@ def parse_ref_23(value: str, member: MemberContext) -> None:
248
293
  member.cin_check_digit = get_composite_part(value, 0)
249
294
  card_date = get_composite_part(value, 1)
250
295
  if card_date and len(card_date) >= 8:
251
- member.fame_card_issue_date = card_date[:8]
296
+ member.fame_card_issue_date = parse_date(card_date[:8])
252
297
 
253
298
 
254
299
  def parse_ref_3h(value: str, member: MemberContext) -> None:
@@ -277,7 +322,7 @@ def parse_ref_dx(value: str, member: MemberContext) -> None:
277
322
  member.carrier_code = strip_leading_zeros(carrier)
278
323
  policy_start = get_composite_part(value, 2)
279
324
  if policy_start and len(policy_start) >= 8:
280
- member.coverage_start_date = policy_start[:8]
325
+ member.coverage_start_date = parse_date(policy_start[:8])
281
326
 
282
327
 
283
328
  def parse_ref_17(value: str, member: MemberContext) -> None:
@@ -495,7 +540,8 @@ def _finalize_member(member: MemberContext, source: str, report_date: str) -> En
495
540
  res_zip_deliv_code=member.res_zip_deliv_code,
496
541
  orec=member.orec, crec=member.crec, snp=member.snp,
497
542
  low_income=member.low_income, lti=member.lti, new_enrollee=new_enrollee,
498
- hcp_code=member.hcp_code, hcp_status=member.hcp_status, amount=member.amount,
543
+ hcp_code=member.hcp_code, hcp_status=member.hcp_status,
544
+ amount_qualifier=member.amount_qualifier, amount=member.amount,
499
545
  hcp_history=hcp_history
500
546
  )
501
547
 
@@ -573,8 +619,8 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
573
619
  # Strip state suffix from city if embedded
574
620
  if city and state and city.upper().endswith(' ' + state.upper()):
575
621
  city = city[:-len(state)-1].strip()
576
- member.city = city.lower() if city else None
577
- member.state = state.lower() if state else None
622
+ member.city = city
623
+ member.state = state
578
624
  member.zip = get_segment_value(segment, 3)
579
625
  # County code
580
626
  if len(segment) > 6 and segment[5] == 'CY' and not member.fame_county_id:
@@ -594,7 +640,7 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
594
640
  sex = get_segment_value(segment, 3)
595
641
  if sex in X12_SEX_CODE_MAPPING:
596
642
  member.sex = X12_SEX_CODE_MAPPING[sex]
597
- member.race = get_segment_value(segment, 5)
643
+ member.race = parse_race_code(get_segment_value(segment, 5))
598
644
  death_str = get_segment_value(segment, 6)
599
645
  if death_str and len(death_str) >= 8:
600
646
  member.death_date = parse_date(death_str[:8])
@@ -638,7 +684,13 @@ def parse_834_enrollment(segments: List[List[str]], source: str = None, report_d
638
684
 
639
685
  # AMT - Amount
640
686
  elif seg_id == 'AMT' and len(segment) >= 3:
641
- member.amount = get_segment_value(segment, 2)
687
+ member.amount_qualifier = get_segment_value(segment, 1)
688
+ amt_val = get_segment_value(segment, 2)
689
+ if amt_val:
690
+ try:
691
+ member.amount = float(amt_val)
692
+ except ValueError:
693
+ pass
642
694
 
643
695
  # Finalize last member
644
696
  if member.member_id or member.has_medicare or member.has_medicaid:
@@ -696,7 +748,7 @@ def extract_enrollment_834(content: str) -> List[EnrollmentData]:
696
748
  source = get_segment_value(segment, 2)
697
749
  gs_date = get_segment_value(segment, 4)
698
750
  if gs_date and len(gs_date) >= 8:
699
- report_date = f"{gs_date[:4]}-{gs_date[4:6]}-{gs_date[6:8]}"
751
+ report_date = parse_date(gs_date[:8])
700
752
  if len(segment) > 8 and segment[8] not in TRANSACTION_TYPES:
701
753
  raise ValueError("Invalid or unsupported 834 format")
702
754
  break
hccinfhir/utils.py CHANGED
@@ -247,6 +247,46 @@ def load_coefficients(file_path: str) -> Dict[Tuple[str, ModelName], float]:
247
247
  return coefficients
248
248
 
249
249
 
250
+ def load_race_ethnicity(file_path: str = "ph_race_and_ethnicity_cdc_v1.3.csv") -> Dict[str, str]:
251
+ """
252
+ Load CDC race and ethnicity codes from CSV file.
253
+ Expected format: Concept Code,Hierarchical Property,Concept Name,...
254
+
255
+ Args:
256
+ file_path: Filename or path to the CSV file
257
+
258
+ Returns:
259
+ Dictionary mapping concept code to concept name
260
+
261
+ Raises:
262
+ FileNotFoundError: If file cannot be found
263
+ RuntimeError: If file cannot be loaded or parsed
264
+ """
265
+ mapping: Dict[str, str] = {}
266
+
267
+ try:
268
+ resolved_path = resolve_data_file(file_path)
269
+ with open(resolved_path, "r", encoding="utf-8", errors="replace") as file:
270
+ content = file.read()
271
+ except FileNotFoundError as e:
272
+ raise FileNotFoundError(f"Could not load race/ethnicity mapping: {e}")
273
+ except Exception as e:
274
+ raise RuntimeError(f"Error loading race/ethnicity file '{file_path}': {e}")
275
+
276
+ for line in content.splitlines()[1:]: # Skip header
277
+ try:
278
+ parts = line.split(',')
279
+ if len(parts) >= 3:
280
+ concept_code = parts[0].strip()
281
+ concept_name = parts[2].strip()
282
+ if concept_code and concept_name:
283
+ mapping[concept_code] = concept_name
284
+ except (ValueError, IndexError):
285
+ continue # Skip malformed lines
286
+
287
+ return mapping
288
+
289
+
250
290
  def load_labels(file_path: str) -> Dict[Tuple[str, ModelName], str]:
251
291
  """
252
292
  Load HCC labels from a CSV file.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hccinfhir
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: HCC Algorithm for FHIR Resources
5
5
  Project-URL: Homepage, https://github.com/mimilabs/hccinfhir
6
6
  Project-URL: Issues, https://github.com/mimilabs/hccinfhir/issues
@@ -95,6 +95,7 @@ print(f"HCCs: {result.hcc_list}")
95
95
  - **Use Case**: Extract dual eligibility status, detect Medicaid coverage loss
96
96
  - **Features**: California DHCS aid code mapping, Medicare status codes, coverage tracking
97
97
  - **Output**: Demographics with accurate dual eligibility for risk calculations
98
+ - **Architecture**: See [834 Parsing Documentation](./README_PARSING834.md) for transaction structure and parsing logic
98
99
 
99
100
  ### 3. **FHIR ExplanationOfBenefit Resources**
100
101
  - **Input**: FHIR EOB from CMS Blue Button 2.0 / BCDA API
@@ -1073,6 +1074,7 @@ Apache License 2.0. See [LICENSE](LICENSE) for details.
1073
1074
  ## 📞 Support
1074
1075
 
1075
1076
  - **Claude Code Documentation**: [CLAUDE.md](./CLAUDE.md) - Comprehensive developer guide
1077
+ - **834 Parsing Architecture**: [README_PARSING834.md](./README_PARSING834.md) - X12 834 transaction structure and parsing logic
1076
1078
  - **Issues**: [GitHub Issues](https://github.com/mimilabs/hccinfhir/issues)
1077
1079
 
1078
1080
  ## 👥 Contributors
@@ -1,9 +1,9 @@
1
1
  hccinfhir/__init__.py,sha256=3aFYtjTklZJg3wIlnMJNgfDBaDCfKXVlYsacdsZ9L4I,1113
2
2
  hccinfhir/constants.py,sha256=C4Vyjtzgyd4Jm2I2X6cTYQZLe-jAMC8boUcy-7OXQDQ,8473
3
- hccinfhir/datamodels.py,sha256=xGh9E5RVi4vONhtIZw2XiaFwVLc5UK027trY31YMUWc,15457
3
+ hccinfhir/datamodels.py,sha256=u-KICO7ODOo8GCsZ0JmyhPauL-irQ9aVjNaiGiVHdks,17592
4
4
  hccinfhir/defaults.py,sha256=aKdXPhf9bYUzpGvXM1GIXZaKxqkKInt3v9meLB9fWog,1394
5
5
  hccinfhir/extractor.py,sha256=xL9c2VT-e2I7_c8N8j4Og42UEgVuCzyn9WFp3ntM5Ro,1822
6
- hccinfhir/extractor_834.py,sha256=zH2nOUJvIJvbDLf6HJWmwCw2yAjT-6RCJyuH4kmIKIQ,27862
6
+ hccinfhir/extractor_834.py,sha256=H5756zwjkZN9nXlGHr_V63VgNjQEjIB7nqaoBYazAR8,29526
7
7
  hccinfhir/extractor_837.py,sha256=fGsvBTWIj9dsHLGGR67AdlYDSsFi5qnSVlTgwkL1f-E,15334
8
8
  hccinfhir/extractor_fhir.py,sha256=wUN3vTm1oTZ-KvfcDebnpQMxAC-7YlRKv12Wrv3p85A,8490
9
9
  hccinfhir/filter.py,sha256=j_yD2g6RBXVUV9trKkWzsQ35x3fRvfKUPvEXKUefI64,2007
@@ -15,10 +15,11 @@ hccinfhir/model_dx_to_cc.py,sha256=Yjc6xKI-jMXsbOzS_chc4NI15Bwagb7BwZZ8cKQaTbk,1
15
15
  hccinfhir/model_hierarchies.py,sha256=cboUnSHZZfOxA8QZKV4QIE-32duElssML32OqYT-65g,1542
16
16
  hccinfhir/model_interactions.py,sha256=g6jK27Xu8RQUHS3lk4sk2v6w6wqd52mdbGn0BsnR7Pk,21394
17
17
  hccinfhir/samples.py,sha256=2VSWS81cv9EnaHqK7sd6CjwG6FUI9E--5wHgD000REI,9952
18
- hccinfhir/utils.py,sha256=hQgHjuOcEQcnxemTZwqFBHWvLC5-C1Gup9cDXEYlZjE,10770
18
+ hccinfhir/utils.py,sha256=WQ2atW0CrdX7sAz_YRLeY4JD-CuH0o-WRusQ_xVVfgY,12152
19
19
  hccinfhir/data/__init__.py,sha256=SGiSkpGrnxbvtEFMMlk82NFHOE50hFXcgKwKUSuVZUg,45
20
20
  hccinfhir/data/hcc_is_chronic.csv,sha256=Bwd-RND6SdEsKP-assoBaXnjUJAuDXhSkwWlymux72Y,19701
21
21
  hccinfhir/data/hcc_is_chronic_without_esrd_model.csv,sha256=eVVI4_8mQNkiBiNO3kattfT_zfcV18XgmiltdzZEXSo,17720
22
+ hccinfhir/data/ph_race_and_ethnicity_cdc_v1.3.csv,sha256=5tw_ATN1mQWVUIahXZyIa5GOX-977PzfhNlGvm43tD8,146970
22
23
  hccinfhir/data/ra_coefficients_2025.csv,sha256=I0S2hoJlfig-D0oSFxy0b3Piv7m9AzOGo2CwR6bcQ9w,215191
23
24
  hccinfhir/data/ra_coefficients_2026.csv,sha256=0gfjGgVdIEWkBO01NaAbTLMzHCYINA0rf_zl8ojngCY,288060
24
25
  hccinfhir/data/ra_dx_to_cc_2025.csv,sha256=4N7vF6VZndkl7d3Fo0cGsbAPAZdCjAizSH8BOKsZNAo,1618924
@@ -54,7 +55,7 @@ hccinfhir/sample_files/sample_eob_1.json,sha256=_NGSVR2ysFpx-DcTvyga6dFCzhQ8Vi9f
54
55
  hccinfhir/sample_files/sample_eob_2.json,sha256=FcnJcx0ApOczxjJ_uxVLzCep9THfNf4xs9Yf7hxk8e4,1769
55
56
  hccinfhir/sample_files/sample_eob_200.ndjson,sha256=CxpjeQ1DCMUzZILaM68UEhfxO0p45YGhDDoCZeq8PxU,1917986
56
57
  hccinfhir/sample_files/sample_eob_3.json,sha256=4BW4wOMBEEU9RDfJR15rBEvk0KNHyuMEh3e055y87Hc,2306
57
- hccinfhir-0.2.3.dist-info/METADATA,sha256=YHHcOAObdo2gWJtPmP6y05-EXeXHpuE40W1pdUXlydw,37132
58
- hccinfhir-0.2.3.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
59
- hccinfhir-0.2.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
60
- hccinfhir-0.2.3.dist-info/RECORD,,
58
+ hccinfhir-0.2.5.dist-info/METADATA,sha256=fjqK-VAKUQ_zblUqz4p8qddjSZje16tYVnPGEYyzw4Q,37381
59
+ hccinfhir-0.2.5.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
60
+ hccinfhir-0.2.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
+ hccinfhir-0.2.5.dist-info/RECORD,,