medicafe 0.250814.4__tar.gz → 0.250816.0__tar.gz
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.
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot.py +69 -37
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_Preprocessor_lib.py +97 -7
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_UI.py +73 -9
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/__init__.py +1 -1
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/MediLink_ConfigLoader.py +60 -13
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/__init__.py +5 -2
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/smart_import.py +12 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/__init__.py +2 -1
- {medicafe-0.250814.4 → medicafe-0.250816.0}/PKG-INFO +1 -1
- {medicafe-0.250814.4 → medicafe-0.250816.0}/medicafe.egg-info/PKG-INFO +1 -1
- {medicafe-0.250814.4 → medicafe-0.250816.0}/setup.py +1 -1
- {medicafe-0.250814.4 → medicafe-0.250816.0}/LICENSE +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MANIFEST.in +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot.bat +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_Crosswalk_Library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/MediBot_smart_import.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/get_medicafe_version.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/update_json.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediBot/update_medicafe.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/__main__.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/api_core.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/api_core_backup.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/api_factory.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/api_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/core_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/graphql_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/logging_config.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/logging_demo.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/migration_helpers.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediCafe/submission_index.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/InsuranceTypeService.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_837p_encoder.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_837p_encoder_library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_ClaimStatus.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_DataMgmt.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Deductible.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Deductible_Validator.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Display_Utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Gmail.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_PatientProcessor.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_UI.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_Up.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_insurance_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_main.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/MediLink_smart_import.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/gmail_http_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/gmail_oauth_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/insurance_type_integration_test.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/openssl.cnf +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/test.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/test_cob_library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/test_timing.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/test_validation.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/MediLink/webapp.html +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/README.md +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/medicafe.egg-info/SOURCES.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/medicafe.egg-info/entry_points.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250816.0}/setup.cfg +0 -0
@@ -11,6 +11,7 @@ try:
|
|
11
11
|
except ImportError:
|
12
12
|
msvcrt = None # Not available on non-Windows systems
|
13
13
|
from collections import OrderedDict
|
14
|
+
from datetime import datetime # Added for primary surgery date logic
|
14
15
|
|
15
16
|
# ============================================================================
|
16
17
|
# MINIMAL PROTECTION: Import State Validation
|
@@ -171,6 +172,68 @@ def identify_field(header, field_mapping):
|
|
171
172
|
# Add this print to a function that is calling identify_field
|
172
173
|
#print("Warning: No matching field found for CSV header '{}'".format(header))
|
173
174
|
|
175
|
+
def create_patient_entries_from_row(row, reverse_mapping):
|
176
|
+
"""
|
177
|
+
Helper function to create patient entries from a row with surgery date handling.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
row: The CSV row containing patient data
|
181
|
+
reverse_mapping: The reverse mapping for field lookups
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
list: List of tuples (surgery_date, patient_name, patient_id, diagnosis_code, row)
|
185
|
+
"""
|
186
|
+
patient_id = row.get(reverse_mapping['Patient ID #2'])
|
187
|
+
patient_name = row.get(reverse_mapping['Patient Name'])
|
188
|
+
|
189
|
+
# Get all surgery dates for this patient
|
190
|
+
all_surgery_dates = row.get('_all_surgery_dates', [row.get('Surgery Date')])
|
191
|
+
surgery_date_to_diagnosis = row.get('_surgery_date_to_diagnosis', {})
|
192
|
+
|
193
|
+
patient_entries = []
|
194
|
+
|
195
|
+
# Sort surgery dates chronologically to ensure proper ordering
|
196
|
+
sorted_surgery_dates = []
|
197
|
+
for surgery_date in all_surgery_dates:
|
198
|
+
try:
|
199
|
+
if hasattr(surgery_date, 'strftime'):
|
200
|
+
# Already a datetime object
|
201
|
+
sorted_surgery_dates.append(surgery_date)
|
202
|
+
elif isinstance(surgery_date, str):
|
203
|
+
# Convert string to datetime for sorting
|
204
|
+
surgery_date_dt = datetime.strptime(surgery_date, '%m-%d-%Y')
|
205
|
+
sorted_surgery_dates.append(surgery_date_dt)
|
206
|
+
else:
|
207
|
+
# Fallback - use as is
|
208
|
+
sorted_surgery_dates.append(surgery_date)
|
209
|
+
except (ValueError, TypeError):
|
210
|
+
# If parsing fails, use the original value
|
211
|
+
sorted_surgery_dates.append(surgery_date)
|
212
|
+
|
213
|
+
# Sort the dates chronologically
|
214
|
+
sorted_surgery_dates.sort()
|
215
|
+
|
216
|
+
# Create entries for each surgery date in chronological order
|
217
|
+
# The enhanced table display will group by patient_id and show dashed lines for secondary dates
|
218
|
+
for surgery_date in sorted_surgery_dates:
|
219
|
+
try:
|
220
|
+
if hasattr(surgery_date, 'strftime'):
|
221
|
+
surgery_date_str = surgery_date.strftime('%m-%d-%Y')
|
222
|
+
elif isinstance(surgery_date, str):
|
223
|
+
surgery_date_str = surgery_date
|
224
|
+
else:
|
225
|
+
surgery_date_str = str(surgery_date)
|
226
|
+
except Exception:
|
227
|
+
surgery_date_str = str(surgery_date)
|
228
|
+
|
229
|
+
# Get the diagnosis code for this surgery date
|
230
|
+
diagnosis_code = surgery_date_to_diagnosis.get(surgery_date_str, 'N/A')
|
231
|
+
|
232
|
+
# Add entry for this surgery date
|
233
|
+
patient_entries.append((surgery_date, patient_name, patient_id, diagnosis_code, row))
|
234
|
+
|
235
|
+
return patient_entries
|
236
|
+
|
174
237
|
# Global flag to control AHK execution method - set to True to use optimized stdin method
|
175
238
|
USE_AHK_STDIN_OPTIMIZATION = True
|
176
239
|
|
@@ -695,23 +758,9 @@ if __name__ == "__main__":
|
|
695
758
|
if patient_row is None:
|
696
759
|
raise ValueError("Patient row not found for patient ID: {}".format(patient_id))
|
697
760
|
|
698
|
-
#
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
# Create entries for each surgery date with its specific diagnosis code
|
703
|
-
for surgery_date in all_surgery_dates:
|
704
|
-
# Convert surgery_date to string format for consistent lookup (XP SP3 + Py3.4.4 compatible)
|
705
|
-
try:
|
706
|
-
if hasattr(surgery_date, 'strftime'):
|
707
|
-
surgery_date_str = surgery_date.strftime('%m-%d-%Y')
|
708
|
-
else:
|
709
|
-
surgery_date_str = str(surgery_date)
|
710
|
-
except Exception:
|
711
|
-
surgery_date_str = str(surgery_date)
|
712
|
-
|
713
|
-
diagnosis_code = surgery_date_to_diagnosis.get(surgery_date_str, 'N/A')
|
714
|
-
patient_info.append((surgery_date, patient_name, patient_id, diagnosis_code, patient_row))
|
761
|
+
# Use helper function to create patient entries
|
762
|
+
patient_entries = create_patient_entries_from_row(patient_row, reverse_mapping)
|
763
|
+
patient_info.extend(patient_entries)
|
715
764
|
|
716
765
|
except Exception as e:
|
717
766
|
MediLink_ConfigLoader.log("Warning: Error retrieving data for patient ID '{}': {}".format(patient_id, e), level="WARNING")
|
@@ -733,26 +782,9 @@ if __name__ == "__main__":
|
|
733
782
|
# Collect surgery dates and patient info for NEW patients
|
734
783
|
new_patient_info = []
|
735
784
|
for row in csv_data:
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
# Get all surgery dates for this patient
|
740
|
-
all_surgery_dates = row.get('_all_surgery_dates', [row.get('Surgery Date')])
|
741
|
-
surgery_date_to_diagnosis = row.get('_surgery_date_to_diagnosis', {})
|
742
|
-
|
743
|
-
# Create entries for each surgery date with its specific diagnosis code
|
744
|
-
for surgery_date in all_surgery_dates:
|
745
|
-
# Convert surgery_date to string format for consistent lookup (XP SP3 + Py3.4.4 compatible)
|
746
|
-
try:
|
747
|
-
if hasattr(surgery_date, 'strftime'):
|
748
|
-
surgery_date_str = surgery_date.strftime('%m-%d-%Y')
|
749
|
-
else:
|
750
|
-
surgery_date_str = str(surgery_date)
|
751
|
-
except Exception:
|
752
|
-
surgery_date_str = str(surgery_date)
|
753
|
-
|
754
|
-
diagnosis_code = surgery_date_to_diagnosis.get(surgery_date_str, 'N/A')
|
755
|
-
new_patient_info.append((surgery_date, patient_name, patient_id, diagnosis_code, row))
|
785
|
+
# Use helper function to create patient entries
|
786
|
+
patient_entries = create_patient_entries_from_row(row, reverse_mapping)
|
787
|
+
new_patient_info.extend(patient_entries)
|
756
788
|
|
757
789
|
# Display new patients table using the enhanced display function
|
758
790
|
MediBot_UI.display_enhanced_patient_table(
|
@@ -531,17 +531,41 @@ def sort_and_deduplicate(csv_data):
|
|
531
531
|
existing_row = unique_patients[patient_id]
|
532
532
|
existing_date = existing_row['Surgery Date']
|
533
533
|
|
534
|
+
# Ensure both dates are comparable by converting to datetime objects
|
535
|
+
def normalize_date_for_comparison(date_value):
|
536
|
+
if isinstance(date_value, datetime):
|
537
|
+
return date_value
|
538
|
+
elif isinstance(date_value, str) and date_value.strip():
|
539
|
+
try:
|
540
|
+
# Try to parse the string as a date
|
541
|
+
return datetime.strptime(date_value, '%m/%d/%Y')
|
542
|
+
except ValueError:
|
543
|
+
try:
|
544
|
+
return datetime.strptime(date_value, '%m-%d-%Y')
|
545
|
+
except ValueError:
|
546
|
+
# If parsing fails, return minimum datetime
|
547
|
+
return datetime.min
|
548
|
+
else:
|
549
|
+
# Empty or invalid values get minimum datetime
|
550
|
+
return datetime.min
|
551
|
+
|
552
|
+
normalized_surgery_date = normalize_date_for_comparison(surgery_date)
|
553
|
+
normalized_existing_date = normalize_date_for_comparison(existing_date)
|
554
|
+
|
534
555
|
# Keep the most current demographic data (later surgery date takes precedence)
|
535
|
-
if
|
556
|
+
if normalized_surgery_date > normalized_existing_date:
|
536
557
|
# Store the old row's surgery date before replacing
|
537
558
|
old_date = existing_row['Surgery Date']
|
538
|
-
|
559
|
+
# Add the old date to the list if it's not already there
|
560
|
+
if old_date not in patient_surgery_dates[patient_id]:
|
561
|
+
patient_surgery_dates[patient_id].append(old_date)
|
539
562
|
# Replace with newer row (better demographics)
|
540
563
|
unique_patients[patient_id] = row
|
541
|
-
#
|
542
|
-
|
564
|
+
# Add the new surgery date to the list if it's not already there
|
565
|
+
if surgery_date not in patient_surgery_dates[patient_id]:
|
566
|
+
patient_surgery_dates[patient_id].append(surgery_date)
|
543
567
|
else:
|
544
|
-
# Add this surgery date to the list for this patient
|
568
|
+
# Add this surgery date to the list for this patient if it's not already there
|
545
569
|
if surgery_date not in patient_surgery_dates[patient_id]:
|
546
570
|
patient_surgery_dates[patient_id].append(surgery_date)
|
547
571
|
|
@@ -558,11 +582,77 @@ def sort_and_deduplicate(csv_data):
|
|
558
582
|
else:
|
559
583
|
surgery_date_strings.append(str(date) if date else 'MISSING')
|
560
584
|
|
561
|
-
|
585
|
+
# Remove duplicates and sort
|
586
|
+
unique_surgery_dates = list(set(surgery_date_strings))
|
587
|
+
sorted_surgery_dates = sorted(unique_surgery_dates, key=lambda x: datetime.strptime(x, '%m-%d-%Y') if x != 'MISSING' else datetime.min)
|
588
|
+
row['_all_surgery_dates'] = sorted_surgery_dates
|
562
589
|
row['_primary_surgery_date'] = row['Surgery Date'] # Keep track of which date has the demographics
|
590
|
+
# Compute and store earliest surgery date for emission sort
|
591
|
+
earliest_dt = None
|
592
|
+
earliest_str = None
|
593
|
+
for d in sorted_surgery_dates:
|
594
|
+
if d and d != 'MISSING':
|
595
|
+
try:
|
596
|
+
earliest_dt = datetime.strptime(d, '%m-%d-%Y')
|
597
|
+
earliest_str = d
|
598
|
+
break
|
599
|
+
except Exception:
|
600
|
+
pass
|
601
|
+
# Fallback to demographics date if earliest could not be determined
|
602
|
+
if earliest_str is None:
|
603
|
+
try:
|
604
|
+
sd = row.get('Surgery Date')
|
605
|
+
if isinstance(sd, datetime) and sd != datetime.min:
|
606
|
+
earliest_dt = sd
|
607
|
+
earliest_str = sd.strftime('%m-%d-%Y')
|
608
|
+
elif isinstance(sd, str) and sd.strip():
|
609
|
+
try:
|
610
|
+
earliest_dt = datetime.strptime(sd, '%m/%d/%Y')
|
611
|
+
except Exception:
|
612
|
+
try:
|
613
|
+
earliest_dt = datetime.strptime(sd, '%m-%d-%Y')
|
614
|
+
except Exception:
|
615
|
+
earliest_dt = None
|
616
|
+
earliest_str = sd
|
617
|
+
except Exception:
|
618
|
+
earliest_dt = None
|
619
|
+
earliest_str = None
|
620
|
+
row['_earliest_surgery_date'] = earliest_str
|
621
|
+
|
622
|
+
|
563
623
|
|
564
624
|
# Convert the unique_patients dictionary back to a list and sort it
|
565
|
-
|
625
|
+
# Use the same normalization function for consistent sorting
|
626
|
+
def sort_key(row):
|
627
|
+
# Prefer earliest surgery date across all known dates for the patient
|
628
|
+
earliest = row.get('_earliest_surgery_date')
|
629
|
+
if isinstance(earliest, str) and earliest and earliest != 'MISSING':
|
630
|
+
try:
|
631
|
+
normalized_date = datetime.strptime(earliest, '%m-%d-%Y')
|
632
|
+
except Exception:
|
633
|
+
normalized_date = datetime.min
|
634
|
+
else:
|
635
|
+
# Fallback to the single Surgery Date field
|
636
|
+
surgery_date = row.get('Surgery Date')
|
637
|
+
if isinstance(surgery_date, datetime):
|
638
|
+
normalized_date = surgery_date
|
639
|
+
elif isinstance(surgery_date, str) and surgery_date.strip():
|
640
|
+
try:
|
641
|
+
normalized_date = datetime.strptime(surgery_date, '%m/%d/%Y')
|
642
|
+
except ValueError:
|
643
|
+
try:
|
644
|
+
normalized_date = datetime.strptime(surgery_date, '%m-%d-%Y')
|
645
|
+
except ValueError:
|
646
|
+
normalized_date = datetime.min
|
647
|
+
else:
|
648
|
+
normalized_date = datetime.min
|
649
|
+
# Tie-break per requirement: last name (case-insensitive), then first name, then patient id
|
650
|
+
last_name = ((row.get('Patient Last') or '')).strip().upper()
|
651
|
+
first_name = ((row.get('Patient First') or '')).strip().upper()
|
652
|
+
patient_id_tiebreak = str(row.get('Patient ID') or '')
|
653
|
+
return (normalized_date, last_name, first_name, patient_id_tiebreak)
|
654
|
+
|
655
|
+
csv_data[:] = sorted(unique_patients.values(), key=sort_key) # TODO Does this need to be sorted twice? once before and once after?
|
566
656
|
|
567
657
|
# TODO: Consider adding an option in the config to sort based on Surgery Schedules when available.
|
568
658
|
# If no schedule is available, the current sorting strategy will be used.
|
@@ -48,26 +48,29 @@ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
|
|
48
48
|
for surgery_date, patient_name, patient_id, diagnosis_code, patient_row in patient_info:
|
49
49
|
# Normalize date into comparable key and display string
|
50
50
|
display_date = None
|
51
|
-
|
51
|
+
current_date_dt = None
|
52
52
|
try:
|
53
53
|
if hasattr(surgery_date, 'strftime'):
|
54
54
|
display_date = surgery_date.strftime('%m-%d')
|
55
|
-
|
55
|
+
current_date_dt = surgery_date
|
56
56
|
elif isinstance(surgery_date, str):
|
57
57
|
# Date strings may be MM-DD-YYYY or already MM-DD
|
58
58
|
parts = surgery_date.split('-') if surgery_date else []
|
59
59
|
if len(parts) == 3 and all(parts):
|
60
60
|
display_date = "{}-{}".format(parts[0], parts[1])
|
61
|
+
try:
|
62
|
+
current_date_dt = datetime.strptime(surgery_date, '%m-%d-%Y')
|
63
|
+
except Exception:
|
64
|
+
current_date_dt = None
|
61
65
|
else:
|
62
66
|
display_date = surgery_date or 'Unknown Date'
|
63
|
-
|
64
|
-
sort_key = surgery_date or ''
|
67
|
+
current_date_dt = None
|
65
68
|
else:
|
66
69
|
display_date = str(surgery_date) if surgery_date is not None else 'Unknown Date'
|
67
|
-
|
70
|
+
current_date_dt = None
|
68
71
|
except Exception:
|
69
72
|
display_date = str(surgery_date) if surgery_date is not None else 'Unknown Date'
|
70
|
-
|
73
|
+
current_date_dt = None
|
71
74
|
|
72
75
|
# Normalize diagnosis display: only show "-Not Found-" when explicitly flagged as N/A
|
73
76
|
# XP SP3 + Py3.4.4 compatible error handling
|
@@ -90,11 +93,72 @@ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
|
|
90
93
|
# Fallback logging if string formatting fails
|
91
94
|
MediLink_ConfigLoader.log("Error converting diagnosis code to string", level="WARNING")
|
92
95
|
display_diagnosis = "-Not Found-"
|
96
|
+
|
97
|
+
# Grouping: place all dates for a patient together under their earliest date
|
98
|
+
primary_date_dt = None
|
99
|
+
within_index = 0
|
100
|
+
last_name_key = ''
|
101
|
+
first_name_key = ''
|
102
|
+
try:
|
103
|
+
all_dates = []
|
104
|
+
if patient_row is not None:
|
105
|
+
raw_dates = patient_row.get('_all_surgery_dates', [])
|
106
|
+
# Convert to datetime list and find primary
|
107
|
+
for d in raw_dates:
|
108
|
+
try:
|
109
|
+
if hasattr(d, 'strftime'):
|
110
|
+
all_dates.append(d)
|
111
|
+
elif isinstance(d, str):
|
112
|
+
all_dates.append(datetime.strptime(d, '%m-%d-%Y'))
|
113
|
+
except Exception:
|
114
|
+
pass
|
115
|
+
if all_dates:
|
116
|
+
all_dates.sort()
|
117
|
+
primary_date_dt = all_dates[0]
|
118
|
+
# Determine within-patient index of current date
|
119
|
+
if current_date_dt is not None:
|
120
|
+
# Find matching index by exact date
|
121
|
+
for idx, ad in enumerate(all_dates):
|
122
|
+
if current_date_dt == ad:
|
123
|
+
within_index = idx
|
124
|
+
break
|
125
|
+
# Prefer explicit last/first from row for sorting
|
126
|
+
try:
|
127
|
+
ln = patient_row.get('Patient Last')
|
128
|
+
fn = patient_row.get('Patient First')
|
129
|
+
if isinstance(ln, str):
|
130
|
+
last_name_key = ln.strip().upper()
|
131
|
+
if isinstance(fn, str):
|
132
|
+
first_name_key = fn.strip().upper()
|
133
|
+
except Exception:
|
134
|
+
pass
|
135
|
+
except Exception:
|
136
|
+
primary_date_dt = None
|
137
|
+
within_index = 0
|
138
|
+
|
139
|
+
# Fallbacks if parsing failed
|
140
|
+
if primary_date_dt is None:
|
141
|
+
primary_date_dt = current_date_dt
|
142
|
+
# If last/first not available from row, parse from display name "LAST, FIRST ..."
|
143
|
+
if not last_name_key and isinstance(patient_name, str):
|
144
|
+
try:
|
145
|
+
parts = [p.strip() for p in patient_name.split(',')]
|
146
|
+
if len(parts) >= 1:
|
147
|
+
last_name_key = parts[0].upper()
|
148
|
+
if len(parts) >= 2:
|
149
|
+
first_name_key = parts[1].split()[0].upper() if parts[1] else ''
|
150
|
+
except Exception:
|
151
|
+
last_name_key = ''
|
152
|
+
first_name_key = ''
|
153
|
+
|
154
|
+
# Build composite sort key per requirement: by earliest date, then last name within date,
|
155
|
+
# while keeping same patient's additional dates directly under the first line
|
156
|
+
composite_sort_key = (primary_date_dt, last_name_key, first_name_key, str(patient_id or ''), within_index)
|
93
157
|
|
94
|
-
normalized_info.append((
|
158
|
+
normalized_info.append((composite_sort_key, display_date, str(patient_name or ''), str(patient_id or ''), display_diagnosis))
|
95
159
|
|
96
|
-
# Sort
|
97
|
-
normalized_info.sort(key=lambda x:
|
160
|
+
# Sort so that all entries for a patient are grouped under their earliest date
|
161
|
+
normalized_info.sort(key=lambda x: x[0])
|
98
162
|
|
99
163
|
# Calculate column widths for proper alignment
|
100
164
|
max_patient_id_len = max(len(pid) for _, _, _, pid, _ in normalized_info)
|
@@ -18,6 +18,32 @@ except ImportError:
|
|
18
18
|
# Fallback to local flag if centralized config is not available
|
19
19
|
PERFORMANCE_LOGGING = False
|
20
20
|
|
21
|
+
def get_default_config():
|
22
|
+
"""Provide a default configuration when config files are missing."""
|
23
|
+
return {
|
24
|
+
'MediLink_Config': {
|
25
|
+
'local_storage_path': '.',
|
26
|
+
'receiptsRoot': './receipts',
|
27
|
+
'api_endpoints': {
|
28
|
+
'base_url': 'https://api.example.com',
|
29
|
+
'timeout': 30
|
30
|
+
},
|
31
|
+
'logging': {
|
32
|
+
'level': 'INFO',
|
33
|
+
'console_output': True
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
def get_default_crosswalk():
|
39
|
+
"""Provide a default crosswalk when crosswalk files are missing."""
|
40
|
+
return {
|
41
|
+
'insurance_types': {},
|
42
|
+
'diagnosis_codes': {},
|
43
|
+
'procedure_codes': {},
|
44
|
+
'payer_mappings': {}
|
45
|
+
}
|
46
|
+
|
21
47
|
"""
|
22
48
|
This function should be generalizable to have a initialization script over all the Medi* functions
|
23
49
|
"""
|
@@ -62,6 +88,10 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
|
|
62
88
|
if PERFORMANCE_LOGGING:
|
63
89
|
print("Path resolution completed in {:.2f} seconds".format(path_check_end - path_check_start))
|
64
90
|
|
91
|
+
# Load configuration with graceful fallback
|
92
|
+
config = None
|
93
|
+
crosswalk = None
|
94
|
+
|
65
95
|
try:
|
66
96
|
config_load_start = time.time()
|
67
97
|
with open(config_path, 'r') as config_file:
|
@@ -88,30 +118,47 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
|
|
88
118
|
if PERFORMANCE_LOGGING:
|
89
119
|
print("Crosswalk file loading completed in {:.2f} seconds".format(crosswalk_load_end - crosswalk_load_start))
|
90
120
|
|
91
|
-
|
92
|
-
|
93
|
-
|
121
|
+
except FileNotFoundError:
|
122
|
+
# Graceful fallback to default configurations
|
123
|
+
print("Configuration files not found. Using default configurations.")
|
124
|
+
print("Config path: {}, Crosswalk path: {}".format(config_path, crosswalk_path))
|
125
|
+
print("To use custom configurations, create the 'json' directory and add config.json and crosswalk.json files.")
|
94
126
|
|
95
|
-
|
96
|
-
|
97
|
-
_CROSSWALK_CACHE = crosswalk
|
127
|
+
config = get_default_config()
|
128
|
+
crosswalk = get_default_crosswalk()
|
98
129
|
|
99
|
-
|
130
|
+
# Log the fallback for debugging
|
131
|
+
if PERFORMANCE_LOGGING:
|
132
|
+
print("Using default configurations due to missing files")
|
133
|
+
|
100
134
|
except ValueError as e:
|
101
135
|
if isinstance(e, UnicodeDecodeError):
|
102
136
|
print("Error decoding file: {}".format(e))
|
103
137
|
else:
|
104
138
|
print("Error parsing file: {}".format(e))
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
raise
|
139
|
+
print("Falling back to default configurations...")
|
140
|
+
config = get_default_config()
|
141
|
+
crosswalk = get_default_crosswalk()
|
109
142
|
except KeyError as e:
|
110
143
|
print("Critical configuration is missing: {}".format(e))
|
111
|
-
|
144
|
+
print("Falling back to default configurations...")
|
145
|
+
config = get_default_config()
|
146
|
+
crosswalk = get_default_crosswalk()
|
112
147
|
except Exception as e:
|
113
148
|
print("An unexpected error occurred while loading the configuration: {}".format(e))
|
114
|
-
|
149
|
+
print("Falling back to default configurations...")
|
150
|
+
config = get_default_config()
|
151
|
+
crosswalk = get_default_crosswalk()
|
152
|
+
|
153
|
+
config_end = time.time()
|
154
|
+
if PERFORMANCE_LOGGING:
|
155
|
+
print("Total configuration loading completed in {:.2f} seconds".format(config_end - config_start))
|
156
|
+
|
157
|
+
# Cache the loaded configuration
|
158
|
+
_CONFIG_CACHE = config
|
159
|
+
_CROSSWALK_CACHE = crosswalk
|
160
|
+
|
161
|
+
return config, crosswalk
|
115
162
|
|
116
163
|
def clear_config_cache():
|
117
164
|
"""Clear the configuration cache to force reloading on next call."""
|
@@ -27,7 +27,7 @@ Smart Import System:
|
|
27
27
|
api_suite = get_api_access()
|
28
28
|
"""
|
29
29
|
|
30
|
-
__version__ = "0.
|
30
|
+
__version__ = "0.250816.0"
|
31
31
|
__author__ = "Daniel Vidaud"
|
32
32
|
__email__ = "daniel@personalizedtransformation.com"
|
33
33
|
|
@@ -83,8 +83,11 @@ try:
|
|
83
83
|
from . import logging_config
|
84
84
|
from . import core_utils
|
85
85
|
__legacy_imports_available__ = True
|
86
|
-
except Exception:
|
86
|
+
except Exception as e:
|
87
87
|
__legacy_imports_available__ = False
|
88
|
+
# Don't fail the entire package import if legacy imports fail
|
89
|
+
print("Warning: Some legacy imports failed: {}".format(e))
|
90
|
+
print("This is expected when configuration files are missing. The package will continue to function.")
|
88
91
|
|
89
92
|
# Package information
|
90
93
|
__all__ = [
|
@@ -162,6 +162,18 @@ class ComponentRegistry:
|
|
162
162
|
module_path = component_mappings[name]
|
163
163
|
try:
|
164
164
|
module = importlib.import_module(module_path)
|
165
|
+
|
166
|
+
# Special handling for components that may fail due to missing config files
|
167
|
+
if name in ['api_core', 'logging_config', 'core_utils']:
|
168
|
+
try:
|
169
|
+
# Test if the module can initialize properly
|
170
|
+
if hasattr(module, 'load_configuration'):
|
171
|
+
# This will use our new graceful fallback
|
172
|
+
module.load_configuration()
|
173
|
+
except Exception as config_error:
|
174
|
+
# Log the config error but don't fail the import
|
175
|
+
warnings.warn("Component '{}' loaded but configuration failed: {}".format(name, config_error))
|
176
|
+
|
165
177
|
return module
|
166
178
|
except ImportError as e:
|
167
179
|
raise ImportError("Failed to load component '{}' from '{}': {}".format(name, module_path, e))
|
@@ -22,7 +22,7 @@ Smart Import Integration:
|
|
22
22
|
datamgmt = get_components('medilink_datamgmt')
|
23
23
|
"""
|
24
24
|
|
25
|
-
__version__ = "0.
|
25
|
+
__version__ = "0.250816.0"
|
26
26
|
__author__ = "Daniel Vidaud"
|
27
27
|
__email__ = "daniel@personalizedtransformation.com"
|
28
28
|
|
@@ -98,6 +98,7 @@ except Exception as e:
|
|
98
98
|
# Handle any other import errors
|
99
99
|
import sys
|
100
100
|
print("Warning: Unexpected error importing MediLink_insurance_utils: {}".format(e))
|
101
|
+
print("This may be due to missing configuration files. MediLink will continue to function.")
|
101
102
|
MediLink_insurance_utils = None
|
102
103
|
|
103
104
|
# Optional: Show guide on import (can be disabled)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|