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 +41 -55
- MediBot/MediBot.py +14 -6
- MediBot/MediBot_Preprocessor_lib.py +8 -1
- MediBot/MediBot_UI.py +41 -9
- MediBot/MediBot_dataformat_library.py +14 -3
- MediCafe/MediLink_ConfigLoader.py +32 -15
- MediCafe/core_utils.py +157 -6
- MediLink/MediLink_Gmail.py +57 -308
- MediLink/gmail_http_utils.py +88 -0
- MediLink/gmail_oauth_utils.py +98 -0
- medicafe-0.250811.1.dist-info/METADATA +138 -0
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.1.dist-info}/RECORD +16 -14
- medicafe-0.250810.7.dist-info/METADATA +0 -62
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.1.dist-info}/LICENSE +0 -0
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.1.dist-info}/WHEEL +0 -0
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.1.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.1.dist-info}/top_level.txt +0 -0
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]
|
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
|
476
|
+
echo UPDATE PREPARATION
|
477
477
|
echo ========================================
|
478
|
-
echo The
|
479
|
-
echo
|
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
|
-
::
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
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
|
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
|
-
|
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
|
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,
|
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(
|
614
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
109
|
-
|
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
|
142
|
+
while ac.get_pause_status():
|
115
143
|
if is_key_pressed(VK_END):
|
116
|
-
|
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
|
-
|
320
|
+
ac.load_paths_from_config(medicare=True)
|
289
321
|
break
|
290
322
|
elif response in ['no', 'n']:
|
291
|
-
|
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
|
-
|
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
|
-
|
201
|
-
|
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
|
-
|
108
|
+
raise
|
109
109
|
except KeyError as e:
|
110
110
|
print("Critical configuration is missing: {}".format(e))
|
111
|
-
|
111
|
+
raise
|
112
112
|
except Exception as e:
|
113
113
|
print("An unexpected error occurred while loading the configuration: {}".format(e))
|
114
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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 =
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
#
|
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
|
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
|
525
|
-
#
|
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
|