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.
Files changed (80) hide show
  1. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot.py +5 -3
  2. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Preprocessor_lib.py +31 -38
  3. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_UI.py +51 -28
  4. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/__init__.py +1 -1
  5. medicafe-0.250818.0/MediBot/update_medicafe.py +245 -0
  6. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/__init__.py +1 -1
  7. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/__init__.py +1 -1
  8. {medicafe-0.250816.0 → medicafe-0.250818.0}/PKG-INFO +1 -1
  9. {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/PKG-INFO +1 -1
  10. {medicafe-0.250816.0 → medicafe-0.250818.0}/setup.py +1 -1
  11. medicafe-0.250816.0/MediBot/update_medicafe.py +0 -770
  12. {medicafe-0.250816.0 → medicafe-0.250818.0}/LICENSE +0 -0
  13. {medicafe-0.250816.0 → medicafe-0.250818.0}/MANIFEST.in +0 -0
  14. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot.bat +0 -0
  15. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Charges.py +0 -0
  16. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Crosswalk_Library.py +0 -0
  17. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
  18. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Post.py +0 -0
  19. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_Preprocessor.py +0 -0
  20. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_dataformat_library.py +0 -0
  21. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_docx_decoder.py +0 -0
  22. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/MediBot_smart_import.py +0 -0
  23. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/get_medicafe_version.py +0 -0
  24. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediBot/update_json.py +0 -0
  25. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/MediLink_ConfigLoader.py +0 -0
  26. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/__main__.py +0 -0
  27. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/api_core.py +0 -0
  28. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/api_core_backup.py +0 -0
  29. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/api_factory.py +0 -0
  30. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/api_utils.py +0 -0
  31. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/core_utils.py +0 -0
  32. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/graphql_utils.py +0 -0
  33. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/logging_config.py +0 -0
  34. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/logging_demo.py +0 -0
  35. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/migration_helpers.py +0 -0
  36. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/smart_import.py +0 -0
  37. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediCafe/submission_index.py +0 -0
  38. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/InsuranceTypeService.py +0 -0
  39. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_837p_cob_library.py +0 -0
  40. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_837p_encoder.py +0 -0
  41. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_837p_encoder_library.py +0 -0
  42. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_837p_utilities.py +0 -0
  43. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_API_Generator.py +0 -0
  44. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Azure.py +0 -0
  45. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_ClaimStatus.py +0 -0
  46. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_DataMgmt.py +0 -0
  47. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Decoder.py +0 -0
  48. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Deductible.py +0 -0
  49. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Deductible_Validator.py +0 -0
  50. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Display_Utils.py +0 -0
  51. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Down.py +0 -0
  52. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Gmail.py +0 -0
  53. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Mailer.py +0 -0
  54. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Parser.py +0 -0
  55. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_PatientProcessor.py +0 -0
  56. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Scan.py +0 -0
  57. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Scheduler.py +0 -0
  58. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_UI.py +0 -0
  59. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_Up.py +0 -0
  60. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_insurance_utils.py +0 -0
  61. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_main.py +0 -0
  62. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/MediLink_smart_import.py +0 -0
  63. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/Soumit_api.py +0 -0
  64. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/gmail_http_utils.py +0 -0
  65. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/gmail_oauth_utils.py +0 -0
  66. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/insurance_type_integration_test.py +0 -0
  67. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/openssl.cnf +0 -0
  68. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/test.py +0 -0
  69. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/test_cob_library.py +0 -0
  70. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/test_timing.py +0 -0
  71. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/test_validation.py +0 -0
  72. {medicafe-0.250816.0 → medicafe-0.250818.0}/MediLink/webapp.html +0 -0
  73. {medicafe-0.250816.0 → medicafe-0.250818.0}/README.md +0 -0
  74. {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/SOURCES.txt +0 -0
  75. {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/dependency_links.txt +0 -0
  76. {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/entry_points.txt +0 -0
  77. {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/not-zip-safe +0 -0
  78. {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/requires.txt +0 -0
  79. {medicafe-0.250816.0 → medicafe-0.250818.0}/medicafe.egg-info/top_level.txt +0 -0
  80. {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
- "NOTE: 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:",
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
- "NOTE: The following NEW patient(s) will be automatically entered into Medisoft:",
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
- error_msg = "CRITICAL: MAINS file not found at: {}. This file is required for insurance name to Medisoft ID mapping.".format(mains_path)
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
- error_msg = "CRITICAL: MAINS file not found: {}. This file is required for insurance name to Medisoft ID mapping.".format(mains_path)
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
- print("{0:03d}: {3} (ID: {2}) {1} ".format(index+1, patient_name, patient_id, formatted_date))
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
- display_menu_header("MEDICARE Patient Selection for Today's Data Entry")
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
- display_menu_header("PRIVATE Patient Selection for Today's Data Entry")
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
- print("-" * 60)
366
- proceed = input("\nDo you want to proceed with the selected patients? (yes/no): ").lower().strip() in ['yes', 'y']
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
- display_menu_header("Patient Selection for Today's Data Entry")
370
- selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping)
371
- print("-" * 60)
372
-
373
- while True:
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
- selection = input("\nEnter the number(s) of the patients you wish to proceed with\n(e.g., 1, 3, 5): ").strip()
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
- return proceed, selected_patient_ids, selected_indices, fixed_values
502
-
503
- return handle_user_interaction(interaction_mode, error_message)
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
@@ -19,7 +19,7 @@ Smart Import Integration:
19
19
  medibot_main = get_components('medibot_main')
20
20
  """
21
21
 
22
- __version__ = "0.250816.0"
22
+ __version__ = "0.250818.0"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25
 
@@ -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()
@@ -27,7 +27,7 @@ Smart Import System:
27
27
  api_suite = get_api_access()
28
28
  """
29
29
 
30
- __version__ = "0.250816.0"
30
+ __version__ = "0.250818.0"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
@@ -22,7 +22,7 @@ Smart Import Integration:
22
22
  datamgmt = get_components('medilink_datamgmt')
23
23
  """
24
24
 
25
- __version__ = "0.250816.0"
25
+ __version__ = "0.250818.0"
26
26
  __author__ = "Daniel Vidaud"
27
27
  __email__ = "daniel@personalizedtransformation.com"
28
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: medicafe
3
- Version: 0.250816.0
3
+ Version: 0.250818.0
4
4
  Summary: MediCafe
5
5
  Home-page: https://github.com/katanada2/MediCafe
6
6
  Author: Daniel Vidaud
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: medicafe
3
- Version: 0.250816.0
3
+ Version: 0.250818.0
4
4
  Summary: MediCafe
5
5
  Home-page: https://github.com/katanada2/MediCafe
6
6
  Author: Daniel Vidaud
@@ -54,7 +54,7 @@ if long_description_text is None:
54
54
 
55
55
  setup(
56
56
  name='medicafe',
57
- version="0.250816.0",
57
+ version="0.250818.0",
58
58
  description='MediCafe',
59
59
  long_description=long_description_text,
60
60
  long_description_content_type='text/markdown',