medicafe 0.250814.3__py3-none-any.whl → 0.250816.0__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.
- MediBot/MediBot.py +182 -20
- MediBot/MediBot_Preprocessor_lib.py +164 -24
- MediBot/MediBot_UI.py +123 -36
- MediBot/__init__.py +1 -1
- MediBot/update_medicafe.py +98 -37
- MediCafe/MediLink_ConfigLoader.py +60 -13
- MediCafe/__init__.py +5 -2
- MediCafe/smart_import.py +12 -0
- MediLink/MediLink_837p_encoder_library.py +72 -65
- MediLink/MediLink_DataMgmt.py +11 -9
- MediLink/__init__.py +11 -2
- {medicafe-0.250814.3.dist-info → medicafe-0.250816.0.dist-info}/METADATA +1 -1
- {medicafe-0.250814.3.dist-info → medicafe-0.250816.0.dist-info}/RECORD +17 -17
- {medicafe-0.250814.3.dist-info → medicafe-0.250816.0.dist-info}/LICENSE +0 -0
- {medicafe-0.250814.3.dist-info → medicafe-0.250816.0.dist-info}/WHEEL +0 -0
- {medicafe-0.250814.3.dist-info → medicafe-0.250816.0.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250814.3.dist-info → medicafe-0.250816.0.dist-info}/top_level.txt +0 -0
MediBot/MediBot.py
CHANGED
@@ -11,6 +11,56 @@ 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
|
15
|
+
|
16
|
+
# ============================================================================
|
17
|
+
# MINIMAL PROTECTION: Import State Validation
|
18
|
+
# ============================================================================
|
19
|
+
|
20
|
+
def validate_critical_imports():
|
21
|
+
"""Validate that critical imports are in expected state before proceeding"""
|
22
|
+
critical_modules = {
|
23
|
+
'MediBot_Preprocessor': None,
|
24
|
+
'MediBot_Preprocessor_lib': None,
|
25
|
+
'MediBot_UI': None,
|
26
|
+
'MediBot_Crosswalk_Library': None
|
27
|
+
}
|
28
|
+
|
29
|
+
# Test imports and capture state
|
30
|
+
try:
|
31
|
+
print("Testing MediCafe.core_utils import...")
|
32
|
+
from MediCafe.core_utils import import_medibot_module_with_debug
|
33
|
+
print("MediCafe.core_utils import successful")
|
34
|
+
|
35
|
+
for module_name in critical_modules.keys():
|
36
|
+
print("Testing {} import...".format(module_name))
|
37
|
+
try:
|
38
|
+
module = import_medibot_module_with_debug(module_name)
|
39
|
+
critical_modules[module_name] = module
|
40
|
+
if module is None:
|
41
|
+
print(" WARNING: {} import returned None".format(module_name))
|
42
|
+
else:
|
43
|
+
print(" SUCCESS: {} import successful".format(module_name))
|
44
|
+
except Exception as e:
|
45
|
+
print(" ERROR: {} import failed with exception: {}".format(module_name, e))
|
46
|
+
critical_modules[module_name] = None
|
47
|
+
except Exception as e:
|
48
|
+
print("CRITICAL: Failed to import core utilities: {}".format(e))
|
49
|
+
return False, critical_modules
|
50
|
+
|
51
|
+
# Check for None imports (the specific failure pattern)
|
52
|
+
failed_imports = []
|
53
|
+
for module_name, module in critical_modules.items():
|
54
|
+
if module is None:
|
55
|
+
failed_imports.append(module_name)
|
56
|
+
|
57
|
+
if failed_imports:
|
58
|
+
print("CRITICAL: Import failures detected:")
|
59
|
+
for failed in failed_imports:
|
60
|
+
print(" - {}: Import returned None".format(failed))
|
61
|
+
return False, critical_modules
|
62
|
+
|
63
|
+
return True, critical_modules
|
14
64
|
|
15
65
|
# Use core utilities for standardized imports
|
16
66
|
from MediCafe.core_utils import (
|
@@ -122,6 +172,68 @@ def identify_field(header, field_mapping):
|
|
122
172
|
# Add this print to a function that is calling identify_field
|
123
173
|
#print("Warning: No matching field found for CSV header '{}'".format(header))
|
124
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
|
+
|
125
237
|
# Global flag to control AHK execution method - set to True to use optimized stdin method
|
126
238
|
USE_AHK_STDIN_OPTIMIZATION = True
|
127
239
|
|
@@ -512,6 +624,19 @@ if __name__ == "__main__":
|
|
512
624
|
try:
|
513
625
|
if PERFORMANCE_LOGGING:
|
514
626
|
print("Initializing. Loading configuration and preparing environment...")
|
627
|
+
|
628
|
+
# PROTECTION: Validate critical imports before proceeding
|
629
|
+
print("Validating critical imports...")
|
630
|
+
import_valid, import_state = validate_critical_imports()
|
631
|
+
if not import_valid:
|
632
|
+
print("CRITICAL: Import validation failed. Cannot continue.")
|
633
|
+
print("This indicates a fundamental system configuration issue.")
|
634
|
+
print("Please check:")
|
635
|
+
print(" 1. All MediBot modules exist in the MediBot directory")
|
636
|
+
print(" 2. Python path is correctly configured")
|
637
|
+
print(" 3. No syntax errors in MediBot modules")
|
638
|
+
sys.exit(1)
|
639
|
+
|
515
640
|
# Use MediCafe configuration system
|
516
641
|
try:
|
517
642
|
config_loader = get_config_loader_with_fallback()
|
@@ -553,6 +678,22 @@ if __name__ == "__main__":
|
|
553
678
|
print("Starting CSV preprocessing at: {}".format(time.strftime("%H:%M:%S")))
|
554
679
|
MediLink_ConfigLoader.log("Starting CSV preprocessing at: {}".format(time.strftime("%H:%M:%S")), level="INFO")
|
555
680
|
|
681
|
+
# PROTECTION: Validate MediBot_Preprocessor before calling preprocess_csv_data
|
682
|
+
if MediBot_Preprocessor is None:
|
683
|
+
print("CRITICAL: MediBot_Preprocessor is None when trying to call preprocess_csv_data")
|
684
|
+
print("This indicates the import failed silently during execution.")
|
685
|
+
print("Import state at failure:")
|
686
|
+
for module_name, module in import_state.items():
|
687
|
+
status = "None" if module is None else "OK"
|
688
|
+
print(" - {}: {}".format(module_name, status))
|
689
|
+
print("Please check for syntax errors or missing dependencies in MediBot modules.")
|
690
|
+
sys.exit(1)
|
691
|
+
|
692
|
+
if not hasattr(MediBot_Preprocessor, 'preprocess_csv_data'):
|
693
|
+
print("CRITICAL: MediBot_Preprocessor missing preprocess_csv_data function")
|
694
|
+
print("Available functions: {}".format([attr for attr in dir(MediBot_Preprocessor) if not attr.startswith('_')]))
|
695
|
+
sys.exit(1)
|
696
|
+
|
556
697
|
MediBot_Preprocessor.preprocess_csv_data(csv_data, e_state.crosswalk)
|
557
698
|
|
558
699
|
# TIMING: End CSV preprocessing timing
|
@@ -617,14 +758,9 @@ if __name__ == "__main__":
|
|
617
758
|
if patient_row is None:
|
618
759
|
raise ValueError("Patient row not found for patient ID: {}".format(patient_id))
|
619
760
|
|
620
|
-
#
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
# Create entries for each surgery date with its specific diagnosis code
|
625
|
-
for surgery_date in all_surgery_dates:
|
626
|
-
diagnosis_code = surgery_date_to_diagnosis.get(surgery_date, 'N/A')
|
627
|
-
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)
|
628
764
|
|
629
765
|
except Exception as e:
|
630
766
|
MediLink_ConfigLoader.log("Warning: Error retrieving data for patient ID '{}': {}".format(patient_id, e), level="WARNING")
|
@@ -646,17 +782,9 @@ if __name__ == "__main__":
|
|
646
782
|
# Collect surgery dates and patient info for NEW patients
|
647
783
|
new_patient_info = []
|
648
784
|
for row in csv_data:
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
# Get all surgery dates for this patient
|
653
|
-
all_surgery_dates = row.get('_all_surgery_dates', [row.get('Surgery Date')])
|
654
|
-
surgery_date_to_diagnosis = row.get('_surgery_date_to_diagnosis', {})
|
655
|
-
|
656
|
-
# Create entries for each surgery date with its specific diagnosis code
|
657
|
-
for surgery_date in all_surgery_dates:
|
658
|
-
diagnosis_code = surgery_date_to_diagnosis.get(surgery_date, 'N/A')
|
659
|
-
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)
|
660
788
|
|
661
789
|
# Display new patients table using the enhanced display function
|
662
790
|
MediBot_UI.display_enhanced_patient_table(
|
@@ -695,7 +823,41 @@ if __name__ == "__main__":
|
|
695
823
|
if e_state:
|
696
824
|
interaction_mode = 'error' # Switch to error mode
|
697
825
|
error_message = str(e) # Capture the error message
|
698
|
-
|
826
|
+
|
827
|
+
# ENHANCED ERROR DIAGNOSTICS
|
828
|
+
print("=" * 60)
|
829
|
+
print("MEDIBOT EXECUTION FAILURE")
|
830
|
+
print("=" * 60)
|
831
|
+
print("Error: {}".format(e))
|
832
|
+
print("Error type: {}".format(type(e).__name__))
|
833
|
+
|
834
|
+
# Check for the specific failure pattern
|
835
|
+
if "'NoneType' object has no attribute" in str(e):
|
836
|
+
print("DIAGNOSIS: This is the import failure pattern.")
|
837
|
+
print("A module import returned None, causing a method call to fail.")
|
838
|
+
print("This typically indicates:")
|
839
|
+
print(" 1. Syntax error in a MediBot module")
|
840
|
+
print(" 2. Missing dependency")
|
841
|
+
print(" 3. Import path issue")
|
842
|
+
print(" 4. Circular import problem")
|
843
|
+
|
844
|
+
# Show current import state
|
845
|
+
print("Current import state:")
|
846
|
+
try:
|
847
|
+
import_state = {
|
848
|
+
'MediBot_Preprocessor': MediBot_Preprocessor,
|
849
|
+
'MediBot_Preprocessor_lib': MediBot_Preprocessor_lib,
|
850
|
+
'MediBot_UI': MediBot_UI,
|
851
|
+
'MediBot_Crosswalk_Library': MediBot_Crosswalk_Library
|
852
|
+
}
|
853
|
+
for module_name, module in import_state.items():
|
854
|
+
status = "None" if module is None else "OK"
|
855
|
+
print(" - {}: {}".format(module_name, status))
|
856
|
+
except Exception as diag_e:
|
857
|
+
print(" - Unable to diagnose import state: {}".format(diag_e))
|
858
|
+
|
859
|
+
print("=" * 60)
|
860
|
+
|
699
861
|
# Handle the error by calling user interaction with the error information
|
700
862
|
if 'identified_fields' in locals():
|
701
863
|
_ = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
@@ -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.
|
@@ -1160,17 +1250,51 @@ def update_diagnosis_codes(csv_data):
|
|
1160
1250
|
MediLink_ConfigLoader.log("Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="DEBUG")
|
1161
1251
|
|
1162
1252
|
if surgery_date_str in all_patient_data[patient_id]:
|
1163
|
-
|
1253
|
+
diagnosis_data = all_patient_data[patient_id][surgery_date_str]
|
1254
|
+
# XP SP3 + Py3.4.4 compatible tuple unpacking with safety check
|
1255
|
+
try:
|
1256
|
+
if isinstance(diagnosis_data, (list, tuple)) and len(diagnosis_data) >= 3:
|
1257
|
+
diagnosis_code, left_or_right_eye, femto_yes_or_no = diagnosis_data
|
1258
|
+
else:
|
1259
|
+
# Handle case where diagnosis_data is not a proper tuple
|
1260
|
+
diagnosis_code = diagnosis_data if diagnosis_data else None
|
1261
|
+
left_or_right_eye = None
|
1262
|
+
femto_yes_or_no = None
|
1263
|
+
except Exception as e:
|
1264
|
+
MediLink_ConfigLoader.log("Error unpacking diagnosis data for Patient ID: {}, Surgery Date: {}: {}".format(
|
1265
|
+
patient_id, surgery_date_str, str(e)), level="WARNING")
|
1266
|
+
diagnosis_code = None
|
1267
|
+
left_or_right_eye = None
|
1268
|
+
femto_yes_or_no = None
|
1269
|
+
|
1164
1270
|
MediLink_ConfigLoader.log("Found diagnosis data for Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="DEBUG")
|
1165
1271
|
|
1166
|
-
|
1167
|
-
|
1168
|
-
if
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1272
|
+
# Convert diagnosis code to Medisoft shorthand format.
|
1273
|
+
# XP SP3 + Py3.4.4 compatible null check
|
1274
|
+
if diagnosis_code is None:
|
1275
|
+
medisoft_shorthand = 'N/A'
|
1276
|
+
MediLink_ConfigLoader.log("Diagnosis code is None for Patient ID: {}, Surgery Date: {}".format(
|
1277
|
+
patient_id, surgery_date_str), level="WARNING")
|
1278
|
+
else:
|
1279
|
+
medisoft_shorthand = diagnosis_to_medisoft.get(diagnosis_code, None)
|
1280
|
+
if medisoft_shorthand is None and diagnosis_code:
|
1281
|
+
# Use fallback logic for missing mapping (XP SP3 + Py3.4.4 compatible)
|
1282
|
+
try:
|
1283
|
+
defaulted_code = diagnosis_code.lstrip('H').lstrip('T8').replace('.', '')[-5:]
|
1284
|
+
# Basic validation: ensure code is not empty and has reasonable length
|
1285
|
+
if defaulted_code and len(defaulted_code) >= 3:
|
1286
|
+
medisoft_shorthand = defaulted_code
|
1287
|
+
MediLink_ConfigLoader.log("Missing diagnosis mapping for '{}', using fallback code '{}'".format(
|
1288
|
+
diagnosis_code, medisoft_shorthand), level="WARNING")
|
1289
|
+
else:
|
1290
|
+
medisoft_shorthand = 'N/A'
|
1291
|
+
MediLink_ConfigLoader.log("Fallback diagnosis code validation failed for '{}', using 'N/A'".format(
|
1292
|
+
diagnosis_code), level="WARNING")
|
1293
|
+
except Exception as e:
|
1294
|
+
medisoft_shorthand = 'N/A'
|
1295
|
+
MediLink_ConfigLoader.log("Error in fallback diagnosis code generation for '{}': {}".format(
|
1296
|
+
diagnosis_code, str(e)), level="WARNING")
|
1297
|
+
|
1174
1298
|
MediLink_ConfigLoader.log("Converted diagnosis code to Medisoft shorthand: {}".format(medisoft_shorthand), level="DEBUG")
|
1175
1299
|
|
1176
1300
|
surgery_date_to_diagnosis[surgery_date_str] = medisoft_shorthand
|
@@ -1488,12 +1612,28 @@ def capitalize_all_fields(csv_data):
|
|
1488
1612
|
Returns:
|
1489
1613
|
None: The function modifies the csv_data in place.
|
1490
1614
|
"""
|
1491
|
-
# PERFORMANCE FIX: Optimize uppercase conversion
|
1615
|
+
# PERFORMANCE FIX: Optimize uppercase conversion while preserving complex types
|
1492
1616
|
for row in csv_data:
|
1493
|
-
|
1494
|
-
row.
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1617
|
+
updated_row = {}
|
1618
|
+
for key, value in row.items():
|
1619
|
+
# Preserve internal/derived fields intact (e.g., `_all_surgery_dates`, `_surgery_date_to_diagnosis`)
|
1620
|
+
if isinstance(key, str) and key.startswith('_'):
|
1621
|
+
updated_row[key] = value
|
1622
|
+
continue
|
1623
|
+
# Uppercase plain strings
|
1624
|
+
if isinstance(value, str):
|
1625
|
+
updated_row[key] = value.upper()
|
1626
|
+
continue
|
1627
|
+
# Preserve complex containers; optionally uppercase their string contents
|
1628
|
+
if isinstance(value, list):
|
1629
|
+
updated_row[key] = [elem.upper() if isinstance(elem, str) else elem for elem in value]
|
1630
|
+
continue
|
1631
|
+
if isinstance(value, dict):
|
1632
|
+
updated_row[key] = {k: (v.upper() if isinstance(v, str) else v) for k, v in value.items()}
|
1633
|
+
continue
|
1634
|
+
# Leave datetimes as-is; coerce simple scalars to string upper for consistency
|
1635
|
+
if isinstance(value, datetime):
|
1636
|
+
updated_row[key] = value
|
1637
|
+
else:
|
1638
|
+
updated_row[key] = str(value).upper() if value is not None else value
|
1639
|
+
row.update(updated_row)
|
MediBot/MediBot_UI.py
CHANGED
@@ -43,64 +43,151 @@ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
|
|
43
43
|
print(title)
|
44
44
|
print()
|
45
45
|
|
46
|
-
#
|
47
|
-
|
46
|
+
# Normalize data to avoid None and unexpected container types in sort key
|
47
|
+
normalized_info = []
|
48
|
+
for surgery_date, patient_name, patient_id, diagnosis_code, patient_row in patient_info:
|
49
|
+
# Normalize date into comparable key and display string
|
50
|
+
display_date = None
|
51
|
+
current_date_dt = None
|
52
|
+
try:
|
53
|
+
if hasattr(surgery_date, 'strftime'):
|
54
|
+
display_date = surgery_date.strftime('%m-%d')
|
55
|
+
current_date_dt = surgery_date
|
56
|
+
elif isinstance(surgery_date, str):
|
57
|
+
# Date strings may be MM-DD-YYYY or already MM-DD
|
58
|
+
parts = surgery_date.split('-') if surgery_date else []
|
59
|
+
if len(parts) == 3 and all(parts):
|
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
|
65
|
+
else:
|
66
|
+
display_date = surgery_date or 'Unknown Date'
|
67
|
+
current_date_dt = None
|
68
|
+
else:
|
69
|
+
display_date = str(surgery_date) if surgery_date is not None else 'Unknown Date'
|
70
|
+
current_date_dt = None
|
71
|
+
except Exception:
|
72
|
+
display_date = str(surgery_date) if surgery_date is not None else 'Unknown Date'
|
73
|
+
current_date_dt = None
|
74
|
+
|
75
|
+
# Normalize diagnosis display: only show "-Not Found-" when explicitly flagged as N/A
|
76
|
+
# XP SP3 + Py3.4.4 compatible error handling
|
77
|
+
display_diagnosis = diagnosis_code
|
78
|
+
try:
|
79
|
+
if diagnosis_code == "N/A":
|
80
|
+
display_diagnosis = "-Not Found-"
|
81
|
+
elif diagnosis_code is None:
|
82
|
+
display_diagnosis = "-Not Found-"
|
83
|
+
elif isinstance(diagnosis_code, str) and diagnosis_code.strip() == "":
|
84
|
+
display_diagnosis = "-Not Found-"
|
85
|
+
else:
|
86
|
+
display_diagnosis = str(diagnosis_code)
|
87
|
+
except (TypeError, ValueError) as e:
|
88
|
+
# Log the specific error for debugging (ASCII-only compatible)
|
89
|
+
try:
|
90
|
+
error_msg = "Error converting diagnosis code to string: {}".format(str(e))
|
91
|
+
MediLink_ConfigLoader.log(error_msg, level="WARNING")
|
92
|
+
except Exception:
|
93
|
+
# Fallback logging if string formatting fails
|
94
|
+
MediLink_ConfigLoader.log("Error converting diagnosis code to string", level="WARNING")
|
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)
|
157
|
+
|
158
|
+
normalized_info.append((composite_sort_key, display_date, str(patient_name or ''), str(patient_id or ''), display_diagnosis))
|
159
|
+
|
160
|
+
# Sort so that all entries for a patient are grouped under their earliest date
|
161
|
+
normalized_info.sort(key=lambda x: x[0])
|
48
162
|
|
49
163
|
# Calculate column widths for proper alignment
|
50
|
-
max_patient_id_len = max(len(
|
51
|
-
max_patient_name_len = max(len(pname) for _,
|
52
|
-
max_diagnosis_len = max(len(dcode) for _, _, _,
|
164
|
+
max_patient_id_len = max(len(pid) for _, _, _, pid, _ in normalized_info)
|
165
|
+
max_patient_name_len = max(len(pname) for _, _, pname, _, _ in normalized_info)
|
166
|
+
max_diagnosis_len = max(len(dcode) for _, _, _, _, dcode in normalized_info)
|
53
167
|
|
54
168
|
# Ensure minimum widths for readability
|
55
169
|
max_patient_id_len = max(max_patient_id_len, 5) # 5-digit ID max
|
56
170
|
max_patient_name_len = max(max_patient_name_len, 12) # "Patient Name" header
|
57
171
|
max_diagnosis_len = max(max_diagnosis_len, 10) # "Diagnosis" header
|
58
172
|
|
59
|
-
# Print the sorted patient info with multiple surgery dates
|
60
173
|
current_patient = None
|
61
174
|
line_number = 1
|
62
175
|
|
63
|
-
for
|
64
|
-
# Format surgery_date safely whether it's a datetime/date or a string
|
65
|
-
try:
|
66
|
-
if isinstance(surgery_date, datetime):
|
67
|
-
formatted_date = surgery_date.strftime('%m-%d')
|
68
|
-
else:
|
69
|
-
# Handle string dates - this should be the Surgery Date Display field
|
70
|
-
formatted_date = str(surgery_date)
|
71
|
-
# If it's a date string like "12-25-2023", format it as "12-25"
|
72
|
-
if '-' in formatted_date and len(formatted_date.split('-')) == 3:
|
73
|
-
try:
|
74
|
-
parts = formatted_date.split('-')
|
75
|
-
formatted_date = "{}-{}".format(parts[0], parts[1])
|
76
|
-
except:
|
77
|
-
pass # Use original string if parsing fails
|
78
|
-
except Exception:
|
79
|
-
formatted_date = str(surgery_date)
|
80
|
-
|
81
|
-
# Transform diagnosis code display: show "-Not Found-" instead of "N/A"
|
82
|
-
display_diagnosis = "-Not Found-" if diagnosis_code == "N/A" else diagnosis_code
|
83
|
-
|
84
|
-
# Check if this is the same patient as the previous line
|
176
|
+
for sort_key, formatted_date, patient_name, patient_id, display_diagnosis in normalized_info:
|
85
177
|
if current_patient == patient_id:
|
86
|
-
|
87
|
-
# Use exact character count matching for dashes
|
88
|
-
patient_id_dashes = '-' * len(str(patient_id))
|
178
|
+
patient_id_dashes = '-' * len(patient_id)
|
89
179
|
patient_name_dashes = '-' * len(patient_name)
|
90
|
-
|
91
180
|
secondary_format = " {:<6} | {:<" + str(max_patient_id_len) + "} | {:<" + str(max_patient_name_len) + "} | {:<" + str(max_diagnosis_len) + "}"
|
92
181
|
print(secondary_format.format(formatted_date, patient_id_dashes, patient_name_dashes, display_diagnosis))
|
93
182
|
else:
|
94
|
-
# New patient - show full patient info
|
95
183
|
current_patient = patient_id
|
96
184
|
if show_line_numbers:
|
97
185
|
primary_format = "{:03d}: {:<6} | {:<" + str(max_patient_id_len) + "} | {:<" + str(max_patient_name_len) + "} | {:<" + str(max_diagnosis_len) + "}"
|
98
|
-
print(primary_format.format(line_number, formatted_date,
|
186
|
+
print(primary_format.format(line_number, formatted_date, patient_id, patient_name, display_diagnosis))
|
99
187
|
line_number += 1
|
100
188
|
else:
|
101
|
-
# For existing patients, don't show line numbers
|
102
189
|
primary_format = " {:<6} | {:<" + str(max_patient_id_len) + "} | {:<" + str(max_patient_name_len) + "} | {:<" + str(max_diagnosis_len) + "}"
|
103
|
-
print(primary_format.format(formatted_date,
|
190
|
+
print(primary_format.format(formatted_date, patient_id, patient_name, display_diagnosis))
|
104
191
|
|
105
192
|
# Function to check if a specific key is pressed
|
106
193
|
def _get_vk_codes():
|