medicafe 0.250816.0__tar.gz → 0.250818.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.250816.0 → medicafe-0.250818.0}/MediBot/MediBot.py +5 -3
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Preprocessor_lib.py +31 -38
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_UI.py +51 -28
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/__init__.py +1 -1
- medicafe-0.250818.0/MediBot/update_medicafe.py +245 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/__init__.py +1 -1
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/__init__.py +1 -1
- {medicafe-0.250816.0 → medicafe-0.250818.0}/PKG-INFO +1 -1
- {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/PKG-INFO +1 -1
- {medicafe-0.250816.0 → medicafe-0.250818.0}/setup.py +1 -1
- medicafe-0.250816.0/MediBot/update_medicafe.py +0 -770
- {medicafe-0.250816.0 → medicafe-0.250818.0}/LICENSE +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MANIFEST.in +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot.bat +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Crosswalk_Library.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_smart_import.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/get_medicafe_version.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/update_json.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/MediLink_ConfigLoader.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/__main__.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/api_core.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/api_core_backup.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/api_factory.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/api_utils.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/core_utils.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/graphql_utils.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/logging_config.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/logging_demo.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/migration_helpers.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/smart_import.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/submission_index.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/InsuranceTypeService.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_837p_encoder.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_837p_encoder_library.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_ClaimStatus.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_DataMgmt.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Deductible.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Deductible_Validator.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Display_Utils.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Gmail.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_PatientProcessor.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_UI.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Up.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_insurance_utils.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_main.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_smart_import.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/gmail_http_utils.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/gmail_oauth_utils.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/insurance_type_integration_test.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/openssl.cnf +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/test.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/test_cob_library.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/test_timing.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/test_validation.py +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/webapp.html +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/README.md +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/SOURCES.txt +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/entry_points.txt +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.250816.0 → medicafe-0.250818.0}/setup.cfg +0 -0
@@ -735,7 +735,7 @@ if __name__ == "__main__":
|
|
735
735
|
print(msg)
|
736
736
|
print("-" * 60)
|
737
737
|
|
738
|
-
proceed, selected_patient_ids, selected_indices, fixed_values = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
738
|
+
proceed, selected_patient_ids, selected_indices, fixed_values, is_medicare = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
739
739
|
|
740
740
|
if proceed:
|
741
741
|
# Filter csv_data for selected patients from Triage mode
|
@@ -767,9 +767,10 @@ if __name__ == "__main__":
|
|
767
767
|
patient_info.append(('Unknown Date', patient_name, patient_id, 'N/A', None)) # Append with 'Unknown Date' if there's an error
|
768
768
|
|
769
769
|
# Display existing patients table using the enhanced display function
|
770
|
+
patient_type = "MEDICARE" if is_medicare else "PRIVATE"
|
770
771
|
MediBot_UI.display_enhanced_patient_table(
|
771
772
|
patient_info,
|
772
|
-
"
|
773
|
+
"{} PATIENTS - EXISTING: The following patient(s) already EXIST in the system but may have new dates of service.\n Their diagnosis codes may need to be updated manually by the user to the following list:".format(patient_type),
|
773
774
|
show_line_numbers=False
|
774
775
|
)
|
775
776
|
|
@@ -787,9 +788,10 @@ if __name__ == "__main__":
|
|
787
788
|
new_patient_info.extend(patient_entries)
|
788
789
|
|
789
790
|
# Display new patients table using the enhanced display function
|
791
|
+
patient_type = "MEDICARE" if is_medicare else "PRIVATE"
|
790
792
|
MediBot_UI.display_enhanced_patient_table(
|
791
793
|
new_patient_info,
|
792
|
-
"
|
794
|
+
"{} PATIENTS - NEW: The following patient(s) will be automatically entered into Medisoft:".format(patient_type),
|
793
795
|
show_line_numbers=True
|
794
796
|
)
|
795
797
|
|
@@ -1378,6 +1378,35 @@ def map_payer_ids_to_insurance_ids(patient_id_to_insurance_id, payer_id_to_patie
|
|
1378
1378
|
}
|
1379
1379
|
return payer_id_to_details
|
1380
1380
|
|
1381
|
+
def _display_mains_file_error(mains_path):
|
1382
|
+
"""
|
1383
|
+
Helper function to display the critical MAINS file error message.
|
1384
|
+
|
1385
|
+
Args:
|
1386
|
+
mains_path (str): The path where the MAINS file was expected to be found.
|
1387
|
+
"""
|
1388
|
+
error_msg = "CRITICAL: MAINS file not found at: {}. This file is required for insurance name to Medisoft ID mapping.".format(mains_path)
|
1389
|
+
if hasattr(MediLink_ConfigLoader, 'log'):
|
1390
|
+
MediLink_ConfigLoader.log(error_msg, level="CRITICAL")
|
1391
|
+
print("\n" + "="*80)
|
1392
|
+
print("CRITICAL ERROR: MAINS FILE MISSING")
|
1393
|
+
print("="*80)
|
1394
|
+
print("\nThe MAINS file is required for the following critical functions:")
|
1395
|
+
print("* Mapping insurance company names to Medisoft IDs")
|
1396
|
+
print("* Converting insurance names to payer IDs for claim submission")
|
1397
|
+
print("* Creating properly formatted 837p claim files")
|
1398
|
+
print("\nWithout this file, claim submission will fail because:")
|
1399
|
+
print("* Insurance names cannot be converted to payer IDs")
|
1400
|
+
print("* 837p claim files cannot be generated")
|
1401
|
+
print("* Claims cannot be submitted to insurance companies")
|
1402
|
+
print("\nTO FIX THIS:")
|
1403
|
+
print("1. Ensure the MAINS file exists at: {}".format(mains_path))
|
1404
|
+
print("2. If the file is missing, llamar a Dani")
|
1405
|
+
print("3. The file should contain insurance company data from your Medisoft system")
|
1406
|
+
print("="*80)
|
1407
|
+
time.sleep(3) # 3 second pause to allow user to read critical error message
|
1408
|
+
|
1409
|
+
|
1381
1410
|
def load_insurance_data_from_mains(config):
|
1382
1411
|
"""
|
1383
1412
|
Loads insurance data from MAINS and creates a mapping from insurance names to their respective IDs.
|
@@ -1421,25 +1450,7 @@ def load_insurance_data_from_mains(config):
|
|
1421
1450
|
try:
|
1422
1451
|
# Check if MAINS file exists before attempting to read
|
1423
1452
|
if not os.path.exists(mains_path):
|
1424
|
-
|
1425
|
-
if hasattr(MediLink_ConfigLoader, 'log'):
|
1426
|
-
MediLink_ConfigLoader.log(error_msg, level="CRITICAL")
|
1427
|
-
print("\n" + "="*80)
|
1428
|
-
print("CRITICAL ERROR: MAINS FILE MISSING")
|
1429
|
-
print("="*80)
|
1430
|
-
print("\nThe MAINS file is required for the following critical functions:")
|
1431
|
-
print("* Mapping insurance company names to Medisoft IDs")
|
1432
|
-
print("* Converting insurance names to payer IDs for claim submission")
|
1433
|
-
print("* Creating properly formatted 837p claim files")
|
1434
|
-
print("\nWithout this file, claim submission will fail because:")
|
1435
|
-
print("* Insurance names cannot be converted to payer IDs")
|
1436
|
-
print("* 837p claim files cannot be generated")
|
1437
|
-
print("* Claims cannot be submitted to insurance companies")
|
1438
|
-
print("\nTO FIX THIS:")
|
1439
|
-
print("1. Ensure the MAINS file exists at: {}".format(mains_path))
|
1440
|
-
print("2. If the file is missing, llamar a Dani")
|
1441
|
-
print("3. The file should contain insurance company data from your Medisoft system")
|
1442
|
-
print("="*80)
|
1453
|
+
_display_mains_file_error(mains_path)
|
1443
1454
|
return insurance_to_id
|
1444
1455
|
|
1445
1456
|
# XP Compatibility: Check if MediLink_DataMgmt has the required function
|
@@ -1459,25 +1470,7 @@ def load_insurance_data_from_mains(config):
|
|
1459
1470
|
print("Successfully loaded {} insurance records from MAINS".format(len(insurance_to_id)))
|
1460
1471
|
|
1461
1472
|
except FileNotFoundError:
|
1462
|
-
|
1463
|
-
if hasattr(MediLink_ConfigLoader, 'log'):
|
1464
|
-
MediLink_ConfigLoader.log(error_msg, level="CRITICAL")
|
1465
|
-
print("\n" + "="*80)
|
1466
|
-
print("CRITICAL ERROR: MAINS FILE MISSING")
|
1467
|
-
print("="*80)
|
1468
|
-
print("\nThe MAINS file is required for the following critical functions:")
|
1469
|
-
print("* Mapping insurance company names to Medisoft IDs")
|
1470
|
-
print("* Converting insurance names to payer IDs for claim submission")
|
1471
|
-
print("* Creating properly formatted 837p claim files")
|
1472
|
-
print("\nWithout this file, claim submission will fail because:")
|
1473
|
-
print("* Insurance names cannot be converted to payer IDs")
|
1474
|
-
print("* 837p claim files cannot be generated")
|
1475
|
-
print("* Claims cannot be submitted to insurance companies")
|
1476
|
-
print("\nTO FIX THIS:")
|
1477
|
-
print("1. Ensure the MAINS file exists at: {}".format(mains_path))
|
1478
|
-
print("2. If the file is missing, llamar a Dani")
|
1479
|
-
print("3. The file should contain insurance company data from your Medisoft system")
|
1480
|
-
print("="*80)
|
1473
|
+
_display_mains_file_error(mains_path)
|
1481
1474
|
except Exception as e:
|
1482
1475
|
error_msg = "Error loading MAINS data: {}. Continuing without MAINS data.".format(str(e))
|
1483
1476
|
if hasattr(MediLink_ConfigLoader, 'log'):
|
@@ -316,6 +316,10 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
316
316
|
selected_patient_ids = []
|
317
317
|
selected_indices = []
|
318
318
|
|
319
|
+
# TODO: Future enhancement - make this configurable via config file
|
320
|
+
# Example: config.get('silent_initial_selection', True)
|
321
|
+
SILENT_INITIAL_SELECTION = True # Set to False to restore original interactive behavior
|
322
|
+
|
319
323
|
def display_menu_header(title):
|
320
324
|
print("\n" + "-" * 60)
|
321
325
|
print(title)
|
@@ -348,7 +352,10 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
348
352
|
formatted_date = surgery_date.strftime('%m-%d')
|
349
353
|
except Exception:
|
350
354
|
formatted_date = str(surgery_date)
|
351
|
-
|
355
|
+
|
356
|
+
# Only display if not in silent mode
|
357
|
+
if not SILENT_INITIAL_SELECTION:
|
358
|
+
print("{0:03d}: {3} (ID: {2}) {1} ".format(index+1, patient_name, patient_id, formatted_date))
|
352
359
|
|
353
360
|
displayed_indices.append(index)
|
354
361
|
displayed_patient_ids.append(patient_id)
|
@@ -356,23 +363,44 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
356
363
|
return displayed_indices, displayed_patient_ids
|
357
364
|
|
358
365
|
if proceed_as_medicare:
|
359
|
-
|
366
|
+
if not SILENT_INITIAL_SELECTION:
|
367
|
+
display_menu_header("MEDICARE Patient Selection for Today's Data Entry")
|
360
368
|
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping, medicare_filter=True)
|
361
369
|
else:
|
362
|
-
|
370
|
+
if not SILENT_INITIAL_SELECTION:
|
371
|
+
display_menu_header("PRIVATE Patient Selection for Today's Data Entry")
|
363
372
|
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping, exclude_medicare=True)
|
364
373
|
|
365
|
-
|
366
|
-
|
374
|
+
if not SILENT_INITIAL_SELECTION:
|
375
|
+
print("-" * 60)
|
376
|
+
proceed = input("\nDo you want to proceed with the selected patients? (yes/no): ").lower().strip() in ['yes', 'y']
|
377
|
+
else:
|
378
|
+
# Auto-confirm in silent mode
|
379
|
+
proceed = True
|
367
380
|
|
368
381
|
if not proceed:
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
382
|
+
if not SILENT_INITIAL_SELECTION:
|
383
|
+
display_menu_header("Patient Selection for Today's Data Entry")
|
384
|
+
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping)
|
385
|
+
print("-" * 60)
|
386
|
+
|
374
387
|
while True:
|
375
|
-
|
388
|
+
while True:
|
389
|
+
selection = input("\nEnter the number(s) of the patients you wish to proceed with\n(e.g., 1, 3, 5): ").strip()
|
390
|
+
if not selection:
|
391
|
+
print("Invalid entry. Please provide at least one number.")
|
392
|
+
continue
|
393
|
+
|
394
|
+
selection = selection.replace('.', ',') # Replace '.' with ',' in the user input just in case
|
395
|
+
selected_indices = [int(x.strip()) - 1 for x in selection.split(',') if x.strip().isdigit()]
|
396
|
+
|
397
|
+
if not selected_indices:
|
398
|
+
print("Invalid entry. Please provide at least one integer.")
|
399
|
+
continue
|
400
|
+
|
401
|
+
proceed = True
|
402
|
+
break
|
403
|
+
|
376
404
|
if not selection:
|
377
405
|
print("Invalid entry. Please provide at least one number.")
|
378
406
|
continue
|
@@ -386,20 +414,6 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
386
414
|
|
387
415
|
proceed = True
|
388
416
|
break
|
389
|
-
|
390
|
-
if not selection:
|
391
|
-
print("Invalid entry. Please provide at least one number.")
|
392
|
-
continue
|
393
|
-
|
394
|
-
selection = selection.replace('.', ',') # Replace '.' with ',' in the user input just in case
|
395
|
-
selected_indices = [int(x.strip()) - 1 for x in selection.split(',') if x.strip().isdigit()]
|
396
|
-
|
397
|
-
if not selected_indices:
|
398
|
-
print("Invalid entry. Please provide at least one integer.")
|
399
|
-
continue
|
400
|
-
|
401
|
-
proceed = True
|
402
|
-
break
|
403
417
|
|
404
418
|
patient_id_header = reverse_mapping['Patient ID #2']
|
405
419
|
selected_patient_ids = [csv_data[i][patient_id_header] for i in selected_indices if i < len(csv_data)]
|
@@ -498,6 +512,15 @@ def user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
|
498
512
|
fixed_values.update(medicare_added_fixed_values) # Add any medicare-specific fixed values from config
|
499
513
|
|
500
514
|
proceed, selected_patient_ids, selected_indices = display_patient_selection_menu(csv_data, reverse_mapping, response in ['yes', 'y'])
|
501
|
-
|
502
|
-
|
503
|
-
|
515
|
+
is_medicare = response in ['yes', 'y']
|
516
|
+
return proceed, selected_patient_ids, selected_indices, fixed_values, is_medicare
|
517
|
+
|
518
|
+
# For non-triage modes (error, normal), return a compatible structure
|
519
|
+
# The is_medicare value is not relevant in these modes, so we'll use a default
|
520
|
+
result = handle_user_interaction(interaction_mode, error_message)
|
521
|
+
if isinstance(result, int):
|
522
|
+
# This is a control value (-1, 1, -2), return with default values
|
523
|
+
return False, [], [], {}, False # proceed=False, empty lists, empty dict, is_medicare=False
|
524
|
+
else:
|
525
|
+
# Unexpected return type, handle gracefully
|
526
|
+
return False, [], [], {}, False
|
@@ -0,0 +1,245 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# update_medicafe.py
|
3
|
+
# Script Version: 2.0.0 (clean 3-try updater)
|
4
|
+
# Target environment: Windows XP SP3 + Python 3.4.4 (ASCII-only)
|
5
|
+
|
6
|
+
import sys, os, time, subprocess, platform
|
7
|
+
|
8
|
+
try:
|
9
|
+
import requests
|
10
|
+
except Exception:
|
11
|
+
requests = None
|
12
|
+
|
13
|
+
try:
|
14
|
+
import pkg_resources
|
15
|
+
except Exception:
|
16
|
+
pkg_resources = None
|
17
|
+
|
18
|
+
|
19
|
+
SCRIPT_NAME = "update_medicafe.py"
|
20
|
+
SCRIPT_VERSION = "2.0.0"
|
21
|
+
PACKAGE_NAME = "medicafe"
|
22
|
+
|
23
|
+
|
24
|
+
# ---------- UI helpers (ASCII-only) ----------
|
25
|
+
def _line(char, width):
|
26
|
+
try:
|
27
|
+
return char * width
|
28
|
+
except Exception:
|
29
|
+
return char * 60
|
30
|
+
|
31
|
+
|
32
|
+
def print_banner(title):
|
33
|
+
width = 60
|
34
|
+
print(_line("=", width))
|
35
|
+
print(title)
|
36
|
+
print(_line("=", width))
|
37
|
+
|
38
|
+
|
39
|
+
def print_section(title):
|
40
|
+
width = 60
|
41
|
+
print("\n" + _line("-", width))
|
42
|
+
print(title)
|
43
|
+
print(_line("-", width))
|
44
|
+
|
45
|
+
|
46
|
+
def print_status(kind, message):
|
47
|
+
label = "[{}]".format(kind)
|
48
|
+
print("{} {}".format(label, message))
|
49
|
+
|
50
|
+
|
51
|
+
# ---------- Version utilities ----------
|
52
|
+
def compare_versions(version1, version2):
|
53
|
+
try:
|
54
|
+
v1_parts = list(map(int, version1.split(".")))
|
55
|
+
v2_parts = list(map(int, version2.split(".")))
|
56
|
+
return (v1_parts > v2_parts) - (v1_parts < v2_parts)
|
57
|
+
except Exception:
|
58
|
+
# Fall back to string compare if unexpected formats
|
59
|
+
return (version1 > version2) - (version1 < version2)
|
60
|
+
|
61
|
+
|
62
|
+
def get_installed_version(package):
|
63
|
+
try:
|
64
|
+
proc = subprocess.Popen([sys.executable, '-m', 'pip', 'show', package],
|
65
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
66
|
+
out, err = proc.communicate()
|
67
|
+
if proc.returncode == 0:
|
68
|
+
for line in out.decode().splitlines():
|
69
|
+
if line.startswith("Version:"):
|
70
|
+
return line.split(":", 1)[1].strip()
|
71
|
+
except Exception:
|
72
|
+
pass
|
73
|
+
|
74
|
+
if pkg_resources:
|
75
|
+
try:
|
76
|
+
return pkg_resources.get_distribution(package).version
|
77
|
+
except Exception:
|
78
|
+
return None
|
79
|
+
return None
|
80
|
+
|
81
|
+
|
82
|
+
def get_latest_version(package, retries):
|
83
|
+
if not requests:
|
84
|
+
return None
|
85
|
+
|
86
|
+
headers = {
|
87
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
88
|
+
'Pragma': 'no-cache',
|
89
|
+
'Expires': '0',
|
90
|
+
'User-Agent': 'MediCafe-Updater/2.0.0'
|
91
|
+
}
|
92
|
+
|
93
|
+
last = None
|
94
|
+
for attempt in range(1, retries + 1):
|
95
|
+
try:
|
96
|
+
url = "https://pypi.org/pypi/{}/json?t={}".format(package, int(time.time()))
|
97
|
+
resp = requests.get(url, headers=headers, timeout=10)
|
98
|
+
resp.raise_for_status()
|
99
|
+
data = resp.json()
|
100
|
+
latest = data.get('info', {}).get('version')
|
101
|
+
if not latest:
|
102
|
+
raise Exception("Malformed PyPI response")
|
103
|
+
|
104
|
+
# Pragmatic double-fetch-if-equal to mitigate CDN staleness
|
105
|
+
if last and latest == last:
|
106
|
+
return latest
|
107
|
+
last = latest
|
108
|
+
if attempt == retries:
|
109
|
+
return latest
|
110
|
+
# If we just fetched same as before and it's equal to current installed, refetch once more quickly
|
111
|
+
time.sleep(1)
|
112
|
+
except Exception:
|
113
|
+
if attempt == retries:
|
114
|
+
return None
|
115
|
+
time.sleep(1)
|
116
|
+
|
117
|
+
return last
|
118
|
+
|
119
|
+
|
120
|
+
def check_internet_connection():
|
121
|
+
if not requests:
|
122
|
+
return False
|
123
|
+
try:
|
124
|
+
requests.get("http://www.google.com", timeout=5)
|
125
|
+
return True
|
126
|
+
except Exception:
|
127
|
+
return False
|
128
|
+
|
129
|
+
|
130
|
+
# ---------- Upgrade logic (3 attempts, minimal delays) ----------
|
131
|
+
def run_pip_install(args):
|
132
|
+
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
133
|
+
out, err = proc.communicate()
|
134
|
+
return proc.returncode, out.decode(), err.decode()
|
135
|
+
|
136
|
+
|
137
|
+
def verify_post_install(package, expected_version):
|
138
|
+
# Try quick reads with minimal backoff to avoid unnecessary slowness
|
139
|
+
for _ in range(3):
|
140
|
+
installed = get_installed_version(package)
|
141
|
+
if installed:
|
142
|
+
# Re-fetch latest once to avoid stale latest
|
143
|
+
latest_again = get_latest_version(package, retries=1) or expected_version
|
144
|
+
if compare_versions(installed, latest_again) >= 0:
|
145
|
+
return True, installed
|
146
|
+
time.sleep(1)
|
147
|
+
return False, get_installed_version(package)
|
148
|
+
|
149
|
+
|
150
|
+
def upgrade_package(package):
|
151
|
+
strategies = [
|
152
|
+
['install', '--upgrade', package, '--no-cache-dir', '--disable-pip-version-check'],
|
153
|
+
['install', '--upgrade', '--force-reinstall', package, '--no-cache-dir', '--disable-pip-version-check'],
|
154
|
+
['install', '--upgrade', '--force-reinstall', '--ignore-installed', '--user', package, '--no-cache-dir', '--disable-pip-version-check']
|
155
|
+
]
|
156
|
+
|
157
|
+
latest_before = get_latest_version(package, retries=2)
|
158
|
+
if not latest_before:
|
159
|
+
print_status('ERROR', 'Unable to determine latest version from PyPI')
|
160
|
+
return False
|
161
|
+
|
162
|
+
for idx, parts in enumerate(strategies):
|
163
|
+
attempt = idx + 1
|
164
|
+
print_section("Attempt {}/3".format(attempt))
|
165
|
+
cmd = [sys.executable, '-m', 'pip'] + parts
|
166
|
+
print_status('INFO', 'Running: {} -m pip {}'.format(sys.executable, ' '.join(parts)))
|
167
|
+
code, out, err = run_pip_install(cmd)
|
168
|
+
if code == 0:
|
169
|
+
ok, installed = verify_post_install(package, latest_before)
|
170
|
+
if ok:
|
171
|
+
print_status('SUCCESS', 'Installed version: {}'.format(installed))
|
172
|
+
return True
|
173
|
+
else:
|
174
|
+
print_status('WARNING', 'Install returned success but version not updated yet{}'.format(
|
175
|
+
'' if not installed else ' (detected {})'.format(installed)))
|
176
|
+
else:
|
177
|
+
# Show error output concisely
|
178
|
+
if err:
|
179
|
+
print(err.strip())
|
180
|
+
print_status('WARNING', 'pip returned non-zero exit code ({})'.format(code))
|
181
|
+
|
182
|
+
return False
|
183
|
+
|
184
|
+
|
185
|
+
# ---------- Main ----------
|
186
|
+
def main():
|
187
|
+
print_banner("MediCafe Updater ({} v{})".format(SCRIPT_NAME, SCRIPT_VERSION))
|
188
|
+
print_status('INFO', 'Python: {}'.format(sys.version.split(" ")[0]))
|
189
|
+
print_status('INFO', 'Platform: {}'.format(platform.platform()))
|
190
|
+
|
191
|
+
if not check_internet_connection():
|
192
|
+
print_section('Network check')
|
193
|
+
print_status('ERROR', 'No internet connection detected')
|
194
|
+
sys.exit(1)
|
195
|
+
|
196
|
+
print_section('Environment')
|
197
|
+
current = get_installed_version(PACKAGE_NAME)
|
198
|
+
if current:
|
199
|
+
print_status('INFO', 'Installed {}: {}'.format(PACKAGE_NAME, current))
|
200
|
+
else:
|
201
|
+
print_status('WARNING', '{} is not currently installed'.format(PACKAGE_NAME))
|
202
|
+
|
203
|
+
latest = get_latest_version(PACKAGE_NAME, retries=3)
|
204
|
+
if not latest:
|
205
|
+
print_status('ERROR', 'Could not fetch latest version information from PyPI')
|
206
|
+
sys.exit(1)
|
207
|
+
print_status('INFO', 'Latest {} on PyPI: {}'.format(PACKAGE_NAME, latest))
|
208
|
+
|
209
|
+
if current and compare_versions(latest, current) <= 0:
|
210
|
+
print_section('Status')
|
211
|
+
print_status('SUCCESS', 'Already up to date')
|
212
|
+
sys.exit(0)
|
213
|
+
|
214
|
+
print_section('Upgrade')
|
215
|
+
print_status('INFO', 'Upgrading {} to {} (up to 3 attempts)'.format(PACKAGE_NAME, latest))
|
216
|
+
success = upgrade_package(PACKAGE_NAME)
|
217
|
+
|
218
|
+
print_section('Result')
|
219
|
+
final_version = get_installed_version(PACKAGE_NAME)
|
220
|
+
if success:
|
221
|
+
print_status('SUCCESS', 'Update completed. {} is now at {}'.format(PACKAGE_NAME, final_version or '(unknown)'))
|
222
|
+
print_status('INFO', 'This updater script: v{}'.format(SCRIPT_VERSION))
|
223
|
+
sys.exit(0)
|
224
|
+
else:
|
225
|
+
print_status('ERROR', 'Update failed.')
|
226
|
+
if final_version and current and compare_versions(final_version, current) > 0:
|
227
|
+
print_status('WARNING', 'Partial success: detected {} after failures'.format(final_version))
|
228
|
+
print_status('INFO', 'This updater script: v{}'.format(SCRIPT_VERSION))
|
229
|
+
sys.exit(1)
|
230
|
+
|
231
|
+
|
232
|
+
if __name__ == '__main__':
|
233
|
+
# Optional quick mode: --check-only prints machine-friendly status
|
234
|
+
if len(sys.argv) > 1 and sys.argv[1] == '--check-only':
|
235
|
+
if not check_internet_connection():
|
236
|
+
print('ERROR')
|
237
|
+
sys.exit(1)
|
238
|
+
cur = get_installed_version(PACKAGE_NAME)
|
239
|
+
lat = get_latest_version(PACKAGE_NAME, retries=2)
|
240
|
+
if not cur or not lat:
|
241
|
+
print('ERROR')
|
242
|
+
sys.exit(1)
|
243
|
+
print('UPDATE_AVAILABLE:' + lat if compare_versions(lat, cur) > 0 else 'UP_TO_DATE')
|
244
|
+
sys.exit(0)
|
245
|
+
main()
|