medicafe 0.250810.7__py3-none-any.whl → 0.250811.1__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.bat CHANGED
@@ -469,68 +469,54 @@ if %errorlevel% neq 0 (
469
469
  echo [OK] Internet connection available
470
470
  echo.
471
471
 
472
- :: Step 5: Start update process
473
- echo [5/5] Starting update process...
472
+ :: Step 5: Start update process (self-update orchestrator)
473
+ echo [5/5] Preparing self-update...
474
474
  echo.
475
475
  echo ========================================
476
- echo UPDATE WINDOW
476
+ echo UPDATE PREPARATION
477
477
  echo ========================================
478
- echo The update process will open in a new window.
479
- echo Please wait for it to complete.
478
+ echo The application will close to allow safe replacement of files.
479
+ echo A separate updater will run and reopen MediBot when finished.
480
480
  echo.
481
481
  echo Update script: %upgrade_medicafe%
482
482
  echo.
483
483
 
484
- :: Execute the update script with better error handling
485
- if exist "%upgrade_medicafe_local%" (
486
- echo Starting local update script...
487
- start "MediCafe Update" cmd /v:on /c "python %upgrade_medicafe_local% %package_version% & echo. & echo Update process completed. Press any key to close... & pause >nul"
488
- if %errorlevel% equ 0 (
489
- echo %DATE% %TIME% Upgrade initiated successfully - local. >> "%temp_file%"
490
- echo [SUCCESS] Update process started successfully
491
- ) else (
492
- echo %DATE% %TIME% Update failed - local. >> "%temp_file%"
493
- echo [ERROR] Failed to start update process
494
- echo Please check the update window for error details.
495
- )
496
- ) else (
497
- if exist "%upgrade_medicafe_legacy%" (
498
- echo Starting legacy update script...
499
- start "MediCafe Update" cmd /v:on /c "python %upgrade_medicafe_legacy% %package_version% & echo. & echo Update process completed. Press any key to close... & pause >nul"
500
- if %errorlevel% equ 0 (
501
- echo %DATE% %TIME% Upgrade initiated successfully - legacy. >> "%temp_file%"
502
- echo [SUCCESS] Update process started successfully
503
- ) else (
504
- echo %DATE% %TIME% Update failed - legacy. >> "%temp_file%"
505
- echo [ERROR] Failed to start update process
506
- echo Please check the update window for error details.
507
- )
508
- ) else (
509
- echo [ERROR] Update script not found
510
- echo Expected locations:
511
- echo - Local: %upgrade_medicafe_local%
512
- echo - Legacy: %upgrade_medicafe_legacy%
513
- echo.
514
- echo Press Enter to return to main menu...
515
- pause >nul
516
- goto main_menu
517
- )
518
- )
519
-
520
- echo.
521
- echo ========================================
522
- echo UPDATE STATUS
523
- echo ========================================
524
- echo.
525
- echo The update process has been initiated in a new window.
526
- echo Please wait for it to complete before continuing.
527
- echo.
528
- echo If the update window closes quickly, there may be an error.
529
- echo Check the update window output for details.
530
- echo.
531
- echo Press Enter to return to main menu...
532
- pause >nul
533
- goto main_menu
484
+ :: Build a temporary updater to run after this window exits
485
+ set "_MEDIBOT_PATH=%~f0"
486
+ set "_UPD_RUNNER=%TEMP%\medicafe_update_runner.cmd"
487
+
488
+ (
489
+ echo @echo off
490
+ echo setlocal enabledelayedexpansion
491
+ echo set "UPD_PY=%upgrade_medicafe%"
492
+ echo set "MEDIBOT_PATH=%_MEDIBOT_PATH%"
493
+ echo rem Wait briefly to ensure the main window has exited and released file locks
494
+ echo ping 127.0.0.1 -n 3 ^>nul
495
+ echo echo Starting MediCafe updater...
496
+ echo where python ^>nul 2^>^&1
497
+ echo if errorlevel 1 (
498
+ echo echo [ERROR] Python not found in PATH. Aborting update.
499
+ echo goto :eof
500
+ echo )
501
+ echo python "%%UPD_PY%%"
502
+ echo set "RET=%%ERRORLEVEL%%"
503
+ echo echo Update process exited with code %%RET%%
504
+ echo rem Relaunch MediBot if available
505
+ echo if exist "%%MEDIBOT_PATH%%" start "MediBot" "%%MEDIBOT_PATH%%"
506
+ echo exit /b %%RET%%
507
+ ) > "%_UPD_RUNNER%"
508
+
509
+ if not exist "%_UPD_RUNNER%" (
510
+ echo [ERROR] Failed to create updater script at %_UPD_RUNNER%
511
+ echo Press Enter to return to main menu...
512
+ pause >nul
513
+ goto main_menu
514
+ )
515
+
516
+ echo.
517
+ echo Launching updater and closing this window...
518
+ start "MediCafe Update" "%_UPD_RUNNER%"
519
+ exit /b 0
534
520
 
535
521
  :: Download Carol's Emails
536
522
  :download_emails
MediBot/MediBot.py CHANGED
@@ -33,6 +33,12 @@ if MediBot_UI:
33
33
  app_control = getattr(MediBot_UI, 'app_control', None)
34
34
  manage_script_pause = getattr(MediBot_UI, 'manage_script_pause', None)
35
35
  user_interaction = getattr(MediBot_UI, 'user_interaction', None)
36
+ get_app_control = getattr(MediBot_UI, '_get_app_control', None)
37
+ def _ac():
38
+ try:
39
+ return get_app_control() if get_app_control else getattr(MediBot_UI, 'app_control', None)
40
+ except Exception:
41
+ return getattr(MediBot_UI, 'app_control', None)
36
42
  else:
37
43
  app_control = None
38
44
  manage_script_pause = None
@@ -345,7 +351,7 @@ def data_entry_loop(csv_data, field_mapping, reverse_mapping, fixed_values):
345
351
  manage_script_pause(csv_data, error_message, reverse_mapping)
346
352
  error_message = '' # Clear error message for the next iteration
347
353
 
348
- if app_control.get_pause_status():
354
+ if _ac() and _ac().get_pause_status():
349
355
  continue # Skip processing this row if the script is paused
350
356
 
351
357
  # I feel like this is overwriting what would have already been idenfitied in the mapping.
@@ -374,7 +380,8 @@ def data_entry_loop(csv_data, field_mapping, reverse_mapping, fixed_values):
374
380
 
375
381
  # Code to handle the end of a patient record
376
382
  # TODO One day this can just not pause...
377
- app_control.set_pause_status(True) # Pause at the end of processing each patient record
383
+ if _ac():
384
+ _ac().set_pause_status(True) # Pause at the end of processing each patient record
378
385
 
379
386
  # PERFORMANCE FIX: Explicit cleanup at end of patient processing
380
387
  # Clear global state to prevent accumulation over processing sessions
@@ -548,11 +555,11 @@ if __name__ == "__main__":
548
555
  csv_data = [row for index, row in enumerate(csv_data) if index in selected_indices]
549
556
 
550
557
  # Check if MAPAT_MED_PATH is missing or invalid
551
- if not app_control.get_mapat_med_path() or not os.path.exists(app_control.get_mapat_med_path()):
558
+ if (not _ac()) or (not _ac().get_mapat_med_path()) or (not os.path.exists(_ac().get_mapat_med_path())):
552
559
  print("Warning: MAPAT.MED PATH is missing or invalid. Please check the path configuration.")
553
560
 
554
561
  # Perform the existing patients check
555
- existing_patients, patients_to_process = MediBot_Preprocessor.check_existing_patients(selected_patient_ids, app_control.get_mapat_med_path())
562
+ existing_patients, patients_to_process = MediBot_Preprocessor.check_existing_patients(selected_patient_ids, _ac().get_mapat_med_path() if _ac() else '')
556
563
 
557
564
  if existing_patients:
558
565
  print("\nNOTE: The following patient(s) already EXIST in the system and \n will be excluded from processing:")
@@ -610,8 +617,9 @@ if __name__ == "__main__":
610
617
  print("\n*** Press [Enter] when ready to begin! ***")
611
618
  input()
612
619
  MediLink_ConfigLoader.log("Opening Medisoft...")
613
- open_medisoft(app_control.get_medisoft_shortcut())
614
- app_control.set_pause_status(True)
620
+ open_medisoft(_ac().get_medisoft_shortcut() if _ac() else '')
621
+ if _ac():
622
+ _ac().set_pause_status(True)
615
623
  _ = manage_script_pause(csv_data, error_message, reverse_mapping)
616
624
  data_entry_loop(csv_data, MediBot_Preprocessor_lib.field_mapping, reverse_mapping, fixed_values)
617
625
  cleanup()
@@ -31,6 +31,12 @@ MediLink_DataMgmt = import_medilink_module('MediLink_DataMgmt')
31
31
  MediBot_UI = import_medibot_module('MediBot_UI')
32
32
  if MediBot_UI:
33
33
  app_control = getattr(MediBot_UI, 'app_control', None)
34
+ get_app_control = getattr(MediBot_UI, '_get_app_control', None)
35
+ def _ac():
36
+ try:
37
+ return get_app_control() if get_app_control else getattr(MediBot_UI, 'app_control', None)
38
+ except Exception:
39
+ return getattr(MediBot_UI, 'app_control', None)
34
40
  else:
35
41
  app_control = None
36
42
 
@@ -1299,7 +1305,8 @@ def load_insurance_data_from_mapat(config, crosswalk):
1299
1305
  dict: A dictionary mapping patient IDs to insurance IDs.
1300
1306
  """
1301
1307
  # Retrieve MAPAT path and slicing information from the configuration
1302
- mapat_path = app_control.get_mapat_med_path()
1308
+ ac = _ac()
1309
+ mapat_path = ac.get_mapat_med_path() if ac else ''
1303
1310
  mapat_slices = crosswalk['mapat_mapping']['slices']
1304
1311
 
1305
1312
  # Initialize the dictionary to hold the patient ID to insurance ID mappings
MediBot/MediBot_UI.py CHANGED
@@ -45,7 +45,13 @@ class AppControl:
45
45
  # PERFORMANCE FIX: Add configuration caching to reduce lookup overhead
46
46
  self._config_cache = {} # Cache for Medicare vs Private configuration lookups
47
47
  # Load initial paths from config when instance is created
48
- self.load_paths_from_config()
48
+ try:
49
+ self.load_paths_from_config()
50
+ except Exception:
51
+ # Defer configuration loading until first access if config is unavailable
52
+ self._deferred_load = True
53
+ else:
54
+ self._deferred_load = False
49
55
 
50
56
  def get_pause_status(self):
51
57
  return self.script_paused
@@ -91,8 +97,29 @@ class AppControl:
91
97
  self.mapat_med_path = cached['mapat_path']
92
98
  self.medisoft_shortcut = cached['shortcut']
93
99
 
94
- app_control = AppControl()
95
-
100
+ def _get_app_control():
101
+ global app_control
102
+ try:
103
+ ac = app_control
104
+ except NameError:
105
+ ac = None
106
+ if ac is None:
107
+ ac = AppControl()
108
+ # If deferred, attempt first load now
109
+ try:
110
+ if getattr(ac, '_deferred_load', False):
111
+ ac.load_paths_from_config()
112
+ ac._deferred_load = False
113
+ except Exception:
114
+ pass
115
+ globals()['app_control'] = ac
116
+ return ac
117
+
118
+ # Lazily initialize app_control to avoid config load at import time
119
+ try:
120
+ app_control
121
+ except NameError:
122
+ app_control = None
96
123
 
97
124
 
98
125
  def is_key_pressed(key_code):
@@ -105,15 +132,16 @@ def manage_script_pause(csv_data, error_message, reverse_mapping):
105
132
  user_action = 0 # initialize as 'continue'
106
133
  VK_END, VK_PAUSE = _get_vk_codes()
107
134
 
108
- if not app_control.get_pause_status() and is_key_pressed(VK_PAUSE):
109
- app_control.set_pause_status(True)
135
+ ac = _get_app_control()
136
+ if not ac.get_pause_status() and is_key_pressed(VK_PAUSE):
137
+ ac.set_pause_status(True)
110
138
  print("Script paused. Opening menu...")
111
139
  interaction_mode = 'normal' # Assuming normal interaction mode for script pause
112
140
  user_action = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
113
141
 
114
- while app_control.get_pause_status():
142
+ while ac.get_pause_status():
115
143
  if is_key_pressed(VK_END):
116
- app_control.set_pause_status(False)
144
+ ac.set_pause_status(False)
117
145
  print("Continuing...")
118
146
  elif is_key_pressed(VK_PAUSE):
119
147
  user_action = user_interaction(csv_data, 'normal', error_message, reverse_mapping)
@@ -276,6 +304,10 @@ def user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
276
304
  if interaction_mode == 'triage':
277
305
  display_menu_header(" =(^.^)= Welcome to MediBot! =(^.^)=")
278
306
 
307
+ # Ensure app_control is initialized before using it in triage
308
+ ac = _get_app_control()
309
+ app_control = ac
310
+
279
311
  while True:
280
312
  try:
281
313
  response = input("\nAm I processing Medicare patients? (yes/no): ").lower().strip()
@@ -285,10 +317,10 @@ def user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
285
317
  continue
286
318
 
287
319
  if response in ['yes', 'y']:
288
- app_control.load_paths_from_config(medicare=True)
320
+ ac.load_paths_from_config(medicare=True)
289
321
  break
290
322
  elif response in ['no', 'n']:
291
- app_control.load_paths_from_config(medicare=False)
323
+ ac.load_paths_from_config(medicare=False)
292
324
  break
293
325
  else:
294
326
  print("Invalid entry. Please enter 'yes' or 'no'.")
@@ -42,6 +42,12 @@ MediBot_UI = import_medibot_module('MediBot_UI')
42
42
  if MediBot_UI:
43
43
  manage_script_pause = getattr(MediBot_UI, 'manage_script_pause', None)
44
44
  app_control = getattr(MediBot_UI, 'app_control', None)
45
+ get_app_control = getattr(MediBot_UI, '_get_app_control', None)
46
+ def _ac():
47
+ try:
48
+ return get_app_control() if get_app_control else getattr(MediBot_UI, 'app_control', None)
49
+ except Exception:
50
+ return getattr(MediBot_UI, 'app_control', None)
45
51
  else:
46
52
  manage_script_pause = None
47
53
  app_control = None
@@ -146,7 +152,8 @@ def enforce_significant_length(output):
146
152
  def format_street(value, csv_data, reverse_mapping, parsed_address_components):
147
153
  _ensure_initialized()
148
154
  # Temporarily disable script pause status
149
- app_control.set_pause_status(False)
155
+ if _ac():
156
+ _ac().set_pause_status(False)
150
157
 
151
158
  # Remove period characters.
152
159
  value = value.replace('.', '')
@@ -197,8 +204,12 @@ def format_street(value, csv_data, reverse_mapping, parsed_address_components):
197
204
  except Exception as e:
198
205
  # Handle exceptions by logging and offering to correct data manually
199
206
  print("Address format error: Unable to parse address '{}'. Error: {}".format(value, e))
200
- app_control.set_pause_status(True)
201
- open_csv_for_editing(CSV_FILE_PATH)
207
+ if _ac():
208
+ _ac().set_pause_status(True)
209
+ if MediBot_Preprocessor_lib and hasattr(MediBot_Preprocessor_lib, 'CSV_FILE_PATH'):
210
+ open_csv_for_editing(MediBot_Preprocessor_lib.CSV_FILE_PATH)
211
+ else:
212
+ open_csv_for_editing('')
202
213
  manage_script_pause(csv_data, e, reverse_mapping)
203
214
  # Return original value with spaces formatted, enforcing significant length
204
215
  return enforce_significant_length(value.replace(' ', '{Space}'))
@@ -105,13 +105,13 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
105
105
  sys.exit(1) # Exit the script due to a critical error in configuration loading
106
106
  except FileNotFoundError:
107
107
  print("One or both configuration files not found. Config: {}, Crosswalk: {}".format(config_path, crosswalk_path))
108
- sys.exit(1) # Exit the script due to a critical error in configuration loading
108
+ raise
109
109
  except KeyError as e:
110
110
  print("Critical configuration is missing: {}".format(e))
111
- sys.exit(1) # Exit the script due to a critical error in configuration loading
111
+ raise
112
112
  except Exception as e:
113
113
  print("An unexpected error occurred while loading the configuration: {}".format(e))
114
- sys.exit(1) # Exit the script due to a critical error in configuration loading
114
+ raise
115
115
 
116
116
  def clear_config_cache():
117
117
  """Clear the configuration cache to force reloading on next call."""
@@ -124,14 +124,23 @@ def log(message, config=None, level="INFO", error_type=None, claim=None, verbose
124
124
 
125
125
  # If config is not provided, use cached config or load it
126
126
  if config is None:
127
- if _CONFIG_CACHE is None:
128
- config, _ = load_configuration()
129
- else:
130
- config = _CONFIG_CACHE
127
+ try:
128
+ if _CONFIG_CACHE is None:
129
+ config, _ = load_configuration()
130
+ else:
131
+ config = _CONFIG_CACHE
132
+ except BaseException:
133
+ # Configuration unavailable; fall back to minimal console logging
134
+ config = {}
131
135
 
132
136
  # Setup logger if not already configured
133
137
  if not logging.root.handlers:
134
- local_storage_path = config['MediLink_Config'].get('local_storage_path', '.') if isinstance(config, dict) else '.'
138
+ local_storage_path = '.'
139
+ if isinstance(config, dict):
140
+ try:
141
+ local_storage_path = config.get('MediLink_Config', {}).get('local_storage_path', '.')
142
+ except Exception:
143
+ local_storage_path = '.'
135
144
  log_filename = datetime.now().strftime("Log_%m%d%Y.log")
136
145
  log_filepath = os.path.join(local_storage_path, log_filename)
137
146
 
@@ -141,12 +150,15 @@ def log(message, config=None, level="INFO", error_type=None, claim=None, verbose
141
150
  # Create formatter
142
151
  formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
143
152
 
144
- # Create file handler
145
- file_handler = logging.FileHandler(log_filepath, mode='a')
146
- file_handler.setFormatter(formatter)
147
-
148
- # Create handlers list - always include file handler
149
- handlers = [file_handler]
153
+ handlers = []
154
+ try:
155
+ # Create file handler when path is usable
156
+ file_handler = logging.FileHandler(log_filepath, mode='a')
157
+ file_handler.setFormatter(formatter)
158
+ handlers.append(file_handler)
159
+ except Exception:
160
+ # Fall back to console-only if file handler cannot be created
161
+ pass
150
162
 
151
163
  # Add console handler only if console_output is True
152
164
  if console_output:
@@ -154,7 +166,12 @@ def log(message, config=None, level="INFO", error_type=None, claim=None, verbose
154
166
  console_handler.setFormatter(formatter)
155
167
  handlers.append(console_handler)
156
168
 
157
- # Configure root logger
169
+ # If no handlers could be added (e.g., file path invalid and console_output False), add a console handler
170
+ if not handlers:
171
+ console_handler = logging.StreamHandler()
172
+ console_handler.setFormatter(formatter)
173
+ handlers.append(console_handler)
174
+
158
175
  logging.basicConfig(level=logging_level, handlers=handlers)
159
176
 
160
177
  # Prepare log message
MediCafe/core_utils.py CHANGED
@@ -513,16 +513,17 @@ def get_api_client_factory():
513
513
  config_loader = get_shared_config_loader()
514
514
  if config_loader:
515
515
  try:
516
+ # Be resilient to SystemExit raised inside loaders
516
517
  config, _ = config_loader.load_configuration()
517
- factory_config = config.get('API_Factory_Config', {})
518
+ factory_config = config.get('API_Factory_Config', {}) if isinstance(config, dict) else {}
518
519
  return APIClientFactory(factory_config)
519
- except Exception:
520
- # Fall back to default configuration
520
+ except BaseException:
521
+ # Fall back to default configuration on any loader failure (including SystemExit)
521
522
  return APIClientFactory()
522
523
  else:
523
524
  return APIClientFactory()
524
- except Exception as e:
525
- # Don't log error here - just return None silently
525
+ except BaseException:
526
+ # Do not allow API client factory acquisition to crash callers during import time
526
527
  return None
527
528
 
528
529
  def get_api_client(**kwargs):
@@ -555,4 +556,154 @@ def get_api_core_client(**kwargs):
555
556
  return APIClient(**kwargs)
556
557
  except ImportError:
557
558
  # Don't log error here - just return None silently
558
- return None
559
+ return None
560
+
561
+ # --- Compatibility & Process Utilities (Python 3.4.4 / Windows XP friendly) ---
562
+
563
+ def is_python_34_compatible(version_info=None):
564
+ """
565
+ Return True if the interpreter is Python 3.4.4+ (as required in XP env).
566
+ This consolidates scattered version checks into a single function so
567
+ call sites remain concise and intention-revealing.
568
+ """
569
+ try:
570
+ if version_info is None:
571
+ version_info = sys.version_info
572
+ major = getattr(version_info, 'major', version_info[0])
573
+ minor = getattr(version_info, 'minor', version_info[1])
574
+ micro = getattr(version_info, 'micro', version_info[2])
575
+ return (major, minor, micro) >= (3, 4, 4)
576
+ except Exception:
577
+ # Be conservative if we cannot determine
578
+ return False
579
+
580
+ def format_py34(template, *args, **kwargs):
581
+ """
582
+ Safe stand-in for f-strings (not available in Python 3.4).
583
+ Centralized to avoid ad-hoc "format_string" helpers scattered around.
584
+ """
585
+ try:
586
+ return template.format(*args, **kwargs)
587
+ except Exception:
588
+ # If formatting fails, return template unmodified to avoid crashes
589
+ return template
590
+
591
+ def _decode_bytes(data, encoding_list=None):
592
+ """
593
+ Decode bytes to text using a tolerant strategy.
594
+ We avoid relying on platform defaults (XP may vary) and try multiple
595
+ encodings to reduce boilerplate at call sites.
596
+ """
597
+ if data is None:
598
+ return ''
599
+ if isinstance(data, str):
600
+ return data
601
+ if encoding_list is None:
602
+ encoding_list = ['utf-8', 'latin-1', 'ascii']
603
+ for enc in encoding_list:
604
+ try:
605
+ return data.decode(enc, 'ignore')
606
+ except Exception:
607
+ continue
608
+ # Last resort: str() on bytes
609
+ try:
610
+ return str(data)
611
+ except Exception:
612
+ return ''
613
+
614
+ def run_cmd(cmd, timeout=None, input_text=None, shell=False, cwd=None, env=None):
615
+ """
616
+ Cross-version subprocess runner for Python 3.4/XP environments.
617
+ - Uses subprocess.Popen (subprocess.run is 3.5+)
618
+ - Returns (returncode, stdout_text, stderr_text)
619
+ - Accepts optional timeout (best-effort with polling for 3.4)
620
+ - Accepts optional input_text (string) which will be encoded to bytes
621
+ This consolidates repetitive Popen/communicate/decode blocks.
622
+ """
623
+ import subprocess
624
+ try:
625
+ # Prepare stdin if input is provided
626
+ stdin_pipe = subprocess.PIPE if input_text is not None else None
627
+ p = subprocess.Popen(
628
+ cmd,
629
+ stdin=stdin_pipe,
630
+ stdout=subprocess.PIPE,
631
+ stderr=subprocess.PIPE,
632
+ shell=shell,
633
+ cwd=cwd,
634
+ env=env
635
+ )
636
+ if timeout is None:
637
+ out_bytes, err_bytes = p.communicate(
638
+ input=input_text.encode('utf-8') if isinstance(input_text, str) else input_text
639
+ )
640
+ return p.returncode, _decode_bytes(out_bytes), _decode_bytes(err_bytes)
641
+ # Implement simple timeout loop compatible with 3.4
642
+ import time
643
+ start_time = time.time()
644
+ while True:
645
+ if p.poll() is not None:
646
+ out_bytes, err_bytes = p.communicate()
647
+ return p.returncode, _decode_bytes(out_bytes), _decode_bytes(err_bytes)
648
+ if time.time() - start_time > timeout:
649
+ try:
650
+ p.kill()
651
+ except Exception:
652
+ try:
653
+ p.terminate()
654
+ except Exception:
655
+ pass
656
+ out_bytes, err_bytes = p.communicate()
657
+ return 124, _decode_bytes(out_bytes), _decode_bytes(err_bytes)
658
+ time.sleep(0.05)
659
+ except Exception as e:
660
+ # Standardize failure surface
661
+ return 1, '', str(e)
662
+
663
+ def file_is_ascii(file_path):
664
+ """
665
+ Return True if the given file can be read as ASCII-only.
666
+ Consolidates repeated try/open/decode blocks into one helper.
667
+ """
668
+ try:
669
+ if not os.path.exists(file_path):
670
+ return False
671
+ # Use strict ASCII to detect any non-ASCII characters
672
+ f = open(file_path, 'r')
673
+ try:
674
+ # Explicit codec arg not supported in some XP Python builds for text mode
675
+ # so we emulate by checking ordinals while reading
676
+ data = f.read()
677
+ finally:
678
+ try:
679
+ f.close()
680
+ except Exception:
681
+ pass
682
+ try:
683
+ data.encode('ascii')
684
+ return True
685
+ except Exception:
686
+ return False
687
+ except Exception:
688
+ return False
689
+
690
+ def check_ascii_files(paths):
691
+ """
692
+ Given a list of paths (absolute or relative), return a list of those that
693
+ contain non-ASCII characters or could not be checked. This replaces
694
+ duplicated loops in various verification scripts.
695
+ """
696
+ problematic = []
697
+ try:
698
+ for p in paths:
699
+ # Normalize relative to current working directory for consistency
700
+ if not os.path.isabs(p):
701
+ abs_p = os.path.abspath(p)
702
+ else:
703
+ abs_p = p
704
+ if not file_is_ascii(abs_p):
705
+ problematic.append(p)
706
+ except Exception:
707
+ # If a failure occurs mid-scan, return what we have so far
708
+ return problematic
709
+ return problematic