medicafe 0.251026.0__tar.gz → 0.251027.1__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.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/__init__.py +1 -1
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/MediLink_ConfigLoader.py +33 -7
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/__init__.py +1 -1
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/error_reporter.py +143 -46
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Gmail.py +27 -4
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_main.py +36 -8
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/__init__.py +1 -1
- {medicafe-0.251026.0/medicafe.egg-info → medicafe-0.251027.1}/PKG-INFO +1 -1
- {medicafe-0.251026.0 → medicafe-0.251027.1/medicafe.egg-info}/PKG-INFO +1 -1
- {medicafe-0.251026.0 → medicafe-0.251027.1}/setup.py +1 -1
- {medicafe-0.251026.0 → medicafe-0.251027.1}/LICENSE +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MANIFEST.in +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot.bat +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_Crosswalk_Library.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_Notepad_Utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_Preprocessor_lib.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_UI.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_debug.bat +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/MediBot_smart_import.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/clear_cache.bat +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/crash_diagnostic.bat +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/f_drive_diagnostic.bat +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/full_debug_suite.bat +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/get_medicafe_version.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/process_csvs.bat +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/update_json.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediBot/update_medicafe.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/__main__.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/api_core.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/api_factory.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/api_utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/core_utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/deductible_utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/graphql_utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/logging_config.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/logging_demo.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/migration_helpers.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/smart_import.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediCafe/submission_index.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/InsuranceTypeService.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_837p_encoder.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_837p_encoder_library.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Charges.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_ClaimStatus.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_DataMgmt.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Deductible.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Deductible_Validator.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Display_Utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_PatientProcessor.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_UI.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_Up.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_insurance_utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/MediLink_smart_import.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/gmail_http_utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/gmail_oauth_utils.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/openssl.cnf +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/test.py +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/MediLink/webapp.html +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/README.md +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/medicafe.egg-info/SOURCES.txt +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/medicafe.egg-info/entry_points.txt +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.251026.0 → medicafe-0.251027.1}/setup.cfg +0 -0
|
@@ -43,8 +43,7 @@ def get_default_config():
|
|
|
43
43
|
'enabled': False,
|
|
44
44
|
'to': '',
|
|
45
45
|
'subject_prefix': 'MediCafe Error Report',
|
|
46
|
-
'max_bundle_bytes': 1572864
|
|
47
|
-
'transport': 'gmail_api'
|
|
46
|
+
'max_bundle_bytes': 1572864
|
|
48
47
|
}
|
|
49
48
|
},
|
|
50
49
|
# STRATEGIC NOTE (COB Configuration): COB library is fully implemented and ready
|
|
@@ -158,16 +157,43 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
|
|
|
158
157
|
print("Using default configurations due to missing files")
|
|
159
158
|
|
|
160
159
|
except ValueError as e:
|
|
160
|
+
# LOUD NOTIFICATION for malformed JSON files
|
|
161
|
+
print("\n" + "="*80)
|
|
162
|
+
print("*** CRITICAL ERROR: MALFORMED JSON CONFIGURATION FILES ***")
|
|
163
|
+
print("="*80)
|
|
161
164
|
if isinstance(e, UnicodeDecodeError):
|
|
162
|
-
print("
|
|
165
|
+
print("ERROR: Cannot decode configuration file - invalid character encoding")
|
|
166
|
+
print("DETAILS: {}".format(e))
|
|
163
167
|
else:
|
|
164
|
-
print("
|
|
165
|
-
|
|
168
|
+
print("ERROR: Configuration files contain invalid JSON syntax")
|
|
169
|
+
print("DETAILS: {}".format(e))
|
|
170
|
+
print("\nAFFECTED FILES:")
|
|
171
|
+
print("- Config file: {}".format(config_path))
|
|
172
|
+
print("- Crosswalk file: {}".format(crosswalk_path))
|
|
173
|
+
print("\nACTION REQUIRED:")
|
|
174
|
+
print("1. Check JSON syntax in both files")
|
|
175
|
+
print("2. Validate JSON using an online validator")
|
|
176
|
+
print("3. Fix syntax errors and restart the application")
|
|
177
|
+
print("\nFALLBACK: Using default configurations (may cause issues)")
|
|
178
|
+
print("="*80 + "\n")
|
|
179
|
+
|
|
166
180
|
config = get_default_config()
|
|
167
181
|
crosswalk = get_default_crosswalk()
|
|
168
182
|
except KeyError as e:
|
|
169
|
-
|
|
170
|
-
print("
|
|
183
|
+
# LOUD NOTIFICATION for missing required configuration keys
|
|
184
|
+
print("\n" + "="*80)
|
|
185
|
+
print("*** CRITICAL ERROR: MISSING REQUIRED CONFIGURATION ***")
|
|
186
|
+
print("="*80)
|
|
187
|
+
print("ERROR: Required configuration key is missing")
|
|
188
|
+
print("DETAILS: {}".format(e))
|
|
189
|
+
print("\nAFFECTED FILE: {}".format(config_path))
|
|
190
|
+
print("\nACTION REQUIRED:")
|
|
191
|
+
print("1. Ensure 'MediLink_Config' section exists in config.json")
|
|
192
|
+
print("2. Check that all required configuration keys are present")
|
|
193
|
+
print("3. Verify JSON structure matches expected format")
|
|
194
|
+
print("\nFALLBACK: Using default configurations (may cause issues)")
|
|
195
|
+
print("="*80 + "\n")
|
|
196
|
+
|
|
171
197
|
config = get_default_config()
|
|
172
198
|
crosswalk = get_default_crosswalk()
|
|
173
199
|
except Exception as e:
|
|
@@ -16,11 +16,6 @@ from email.mime.text import MIMEText
|
|
|
16
16
|
from MediCafe.MediLink_ConfigLoader import load_configuration, log as mc_log
|
|
17
17
|
from MediLink.MediLink_Gmail import get_access_token
|
|
18
18
|
|
|
19
|
-
ASCII_SAFE_REPLACEMENTS = [
|
|
20
|
-
('"', '"'),
|
|
21
|
-
("'", "'"),
|
|
22
|
-
]
|
|
23
|
-
|
|
24
19
|
|
|
25
20
|
def _safe_ascii(text):
|
|
26
21
|
try:
|
|
@@ -66,21 +61,33 @@ def _get_latest_log_path(local_storage_path):
|
|
|
66
61
|
|
|
67
62
|
|
|
68
63
|
def _redact(text):
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
64
|
+
# Best-effort ASCII redaction: mask common secrets in logs and JSON
|
|
65
|
+
try:
|
|
66
|
+
text = _safe_ascii(text)
|
|
67
|
+
import re
|
|
68
|
+
patterns = [
|
|
69
|
+
# SSN-like
|
|
70
|
+
(r'\b(\d{3}-?\d{2}-?\d{4})\b', '***-**-****'),
|
|
71
|
+
# 9-11 digit numeric IDs
|
|
72
|
+
(r'\b(\d{9,11})\b', '*********'),
|
|
73
|
+
# Authorization headers
|
|
74
|
+
(r'Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]+=*', 'Authorization: Bearer ***'),
|
|
75
|
+
(r'Authorization:\s*[^\n\r]+', 'Authorization: ***'),
|
|
76
|
+
# JSON token fields
|
|
77
|
+
(r'("access_token"\s*:\s*")([^"]+)(")', r'\1***\3'),
|
|
78
|
+
(r'("refresh_token"\s*:\s*")([^"]+)(")', r'\1***\3'),
|
|
79
|
+
(r'("id_token"\s*:\s*")([^"]+)(")', r'\1***\3'),
|
|
80
|
+
(r'("X-Auth-Token"\s*:\s*")([^"]+)(")', r'\1***\3'),
|
|
81
|
+
# URL query params: token=..., access_token=..., auth=...
|
|
82
|
+
(r'(token|access_token|auth|authorization)=([^&\s]+)', r'\1=***'),
|
|
83
|
+
# Bearer fragments in JSON or text
|
|
84
|
+
(r'Bearer\s+[A-Za-z0-9\-._~+/]+=*', 'Bearer ***'),
|
|
85
|
+
]
|
|
86
|
+
for pat, rep in patterns:
|
|
87
|
+
text = re.sub(pat, rep, text)
|
|
88
|
+
return text
|
|
89
|
+
except Exception:
|
|
90
|
+
return text
|
|
84
91
|
|
|
85
92
|
|
|
86
93
|
def _ensure_dir(path):
|
|
@@ -92,15 +99,7 @@ def _ensure_dir(path):
|
|
|
92
99
|
return False
|
|
93
100
|
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
try:
|
|
97
|
-
h = hashlib.sha256()
|
|
98
|
-
with open(zip_path, 'rb') as f:
|
|
99
|
-
chunk = f.read(256 * 1024)
|
|
100
|
-
h.update(chunk)
|
|
101
|
-
return 'mc-{}-{}'.format(int(time.time()), h.hexdigest()[:12])
|
|
102
|
-
except Exception:
|
|
103
|
-
return 'mc-{}-{}'.format(int(time.time()), '000000000000')
|
|
102
|
+
# _compute_report_id removed (unused)
|
|
104
103
|
|
|
105
104
|
|
|
106
105
|
def collect_support_bundle(include_traceback=True, max_log_lines=500):
|
|
@@ -128,7 +127,7 @@ def collect_support_bundle(include_traceback=True, max_log_lines=500):
|
|
|
128
127
|
except Exception:
|
|
129
128
|
traceback_txt = ''
|
|
130
129
|
|
|
131
|
-
|
|
130
|
+
meta = {
|
|
132
131
|
'app_version': _safe_ascii(_get_version()),
|
|
133
132
|
'python_version': _safe_ascii(sys.version.split(' ')[0]),
|
|
134
133
|
'platform': _safe_ascii(platform.platform()),
|
|
@@ -164,11 +163,7 @@ def collect_support_bundle(include_traceback=True, max_log_lines=500):
|
|
|
164
163
|
mc_log('Error creating support bundle: {}'.format(e), level='ERROR')
|
|
165
164
|
return None
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
if bundle_size > 1572864:
|
|
169
|
-
os.remove(zip_path)
|
|
170
|
-
mc_log('Bundle too large ({} bytes) - discarded'.format(bundle_size), level='WARNING')
|
|
171
|
-
return None
|
|
166
|
+
# Do not delete oversize bundles here; sender will enforce size based on config
|
|
172
167
|
return zip_path
|
|
173
168
|
|
|
174
169
|
|
|
@@ -202,9 +197,12 @@ def capture_unhandled_traceback(exc_type, exc_value, exc_traceback):
|
|
|
202
197
|
text = _redact(text)
|
|
203
198
|
with open(trace_path, 'w') as f:
|
|
204
199
|
f.write(text)
|
|
205
|
-
|
|
200
|
+
print("An error occurred. A traceback was saved to {}".format(trace_path))
|
|
206
201
|
except Exception:
|
|
207
|
-
|
|
202
|
+
try:
|
|
203
|
+
mc_log('Failed to capture traceback to file', level='WARNING')
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
208
206
|
|
|
209
207
|
def submit_support_bundle_email(zip_path=None, include_traceback=True):
|
|
210
208
|
if not zip_path:
|
|
@@ -213,16 +211,22 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
|
|
|
213
211
|
mc_log("Failed to create bundle.", level="ERROR")
|
|
214
212
|
return False
|
|
215
213
|
bundle_size = os.path.getsize(zip_path)
|
|
216
|
-
if bundle_size > 1572864:
|
|
217
|
-
mc_log("Bundle too large ({} bytes) - discarding.".format(bundle_size), level="WARNING")
|
|
218
|
-
os.remove(zip_path)
|
|
219
|
-
return False
|
|
220
214
|
config, _ = load_configuration()
|
|
221
215
|
email_config = config.get('MediLink_Config', {}).get('error_reporting', {}).get('email', {})
|
|
222
|
-
|
|
223
|
-
|
|
216
|
+
# Determine max size from config with default 1.5MB
|
|
217
|
+
try:
|
|
218
|
+
max_bytes = int(email_config.get('max_bundle_bytes', 1572864))
|
|
219
|
+
except Exception:
|
|
220
|
+
max_bytes = 1572864
|
|
221
|
+
if bundle_size > max_bytes:
|
|
222
|
+
mc_log("Bundle too large ({} bytes > {} bytes) - leaving in queue.".format(bundle_size, max_bytes), level="WARNING")
|
|
223
|
+
return False
|
|
224
|
+
# Feature is always available; proceed if recipients and token are available
|
|
225
|
+
# Normalize and validate recipients
|
|
226
|
+
to_emails = _normalize_recipients(email_config.get('to', []))
|
|
227
|
+
if not to_emails:
|
|
228
|
+
mc_log("No valid recipients configured in error_reporting.email.to", level="ERROR")
|
|
224
229
|
return False
|
|
225
|
-
to_emails = email_config.get('to', [])
|
|
226
230
|
subject_prefix = email_config.get('subject_prefix', 'MediCafe Error Report')
|
|
227
231
|
access_token = get_access_token()
|
|
228
232
|
if not access_token:
|
|
@@ -245,12 +249,105 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
|
|
|
245
249
|
resp = requests.post('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', headers=headers, json=data)
|
|
246
250
|
if resp.status_code == 200:
|
|
247
251
|
mc_log("Report sent successfully!", level="INFO")
|
|
248
|
-
|
|
252
|
+
try:
|
|
253
|
+
os.remove(zip_path)
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
249
256
|
return True
|
|
250
257
|
else:
|
|
251
|
-
|
|
258
|
+
# Handle auth errors by prompting re-consent using existing OAuth helpers
|
|
259
|
+
if resp.status_code in (401, 403):
|
|
260
|
+
try:
|
|
261
|
+
from MediLink.MediLink_Gmail import get_authorization_url, open_browser_with_executable
|
|
262
|
+
auth_url = get_authorization_url()
|
|
263
|
+
mc_log("Gmail send unauthorized ({}). Opening browser for re-consent.".format(resp.status_code), level="WARNING")
|
|
264
|
+
try:
|
|
265
|
+
print("Your Google session needs to be refreshed to gain gmail.send permission. A browser will open to re-authorize.")
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
open_browser_with_executable(auth_url)
|
|
269
|
+
except Exception:
|
|
270
|
+
pass
|
|
271
|
+
mc_log("Failed to send: {} - {}".format(resp.status_code, _redact(resp.text)), level="ERROR")
|
|
272
|
+
# Preserve bundle in queue for manual retry
|
|
252
273
|
return False
|
|
253
274
|
|
|
275
|
+
|
|
276
|
+
def _normalize_recipients(to_field):
|
|
277
|
+
try:
|
|
278
|
+
# Flatten to a list of strings
|
|
279
|
+
if isinstance(to_field, str):
|
|
280
|
+
candidates = [p.strip() for p in to_field.split(',')]
|
|
281
|
+
elif isinstance(to_field, list):
|
|
282
|
+
candidates = [str(p).strip() for p in to_field]
|
|
283
|
+
else:
|
|
284
|
+
candidates = []
|
|
285
|
+
# Basic email regex: local@domain.tld
|
|
286
|
+
import re
|
|
287
|
+
email_re = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
|
288
|
+
valid = []
|
|
289
|
+
for addr in candidates:
|
|
290
|
+
if not addr:
|
|
291
|
+
continue
|
|
292
|
+
if email_re.match(addr):
|
|
293
|
+
valid.append(addr)
|
|
294
|
+
else:
|
|
295
|
+
try:
|
|
296
|
+
mc_log("Invalid email recipient skipped: {}".format(addr), level="WARNING")
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
return valid
|
|
300
|
+
except Exception:
|
|
301
|
+
return []
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def list_queued_bundles():
|
|
305
|
+
try:
|
|
306
|
+
config, _ = load_configuration()
|
|
307
|
+
medi = config.get('MediLink_Config', {})
|
|
308
|
+
local_storage_path = medi.get('local_storage_path', '.')
|
|
309
|
+
queue_dir = os.path.join(local_storage_path, 'reports_queue')
|
|
310
|
+
if not os.path.isdir(queue_dir):
|
|
311
|
+
return []
|
|
312
|
+
files = [os.path.join(queue_dir, f) for f in os.listdir(queue_dir) if f.endswith('.zip')]
|
|
313
|
+
files.sort()
|
|
314
|
+
return files
|
|
315
|
+
except Exception:
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def submit_all_queued_bundles():
|
|
320
|
+
sent = 0
|
|
321
|
+
failed = 0
|
|
322
|
+
try:
|
|
323
|
+
queued = list_queued_bundles()
|
|
324
|
+
for z in queued:
|
|
325
|
+
try:
|
|
326
|
+
ok = submit_support_bundle_email(zip_path=z, include_traceback=False)
|
|
327
|
+
if ok:
|
|
328
|
+
sent += 1
|
|
329
|
+
else:
|
|
330
|
+
failed += 1
|
|
331
|
+
except Exception:
|
|
332
|
+
failed += 1
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
return sent, failed
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def delete_all_queued_bundles():
|
|
339
|
+
deleted = 0
|
|
340
|
+
try:
|
|
341
|
+
for z in list_queued_bundles():
|
|
342
|
+
try:
|
|
343
|
+
os.remove(z)
|
|
344
|
+
deleted += 1
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
except Exception:
|
|
348
|
+
pass
|
|
349
|
+
return deleted
|
|
350
|
+
|
|
254
351
|
def email_error_report_flow():
|
|
255
352
|
try:
|
|
256
353
|
sent = submit_support_bundle_email(zip_path=None, include_traceback=True)
|
|
@@ -115,11 +115,34 @@ def get_authorization_url():
|
|
|
115
115
|
def exchange_code_for_token(auth_code, retries=3):
|
|
116
116
|
return oauth_exchange_code_for_token(auth_code, CREDENTIALS_PATH, REDIRECT_URI, log, retries=retries)
|
|
117
117
|
|
|
118
|
+
def _mask_token_value(value):
|
|
119
|
+
try:
|
|
120
|
+
s = str(value or '')
|
|
121
|
+
if len(s) <= 8:
|
|
122
|
+
return '***'
|
|
123
|
+
return s[:4] + '…' + s[-4:]
|
|
124
|
+
except Exception:
|
|
125
|
+
return '***'
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _mask_token_payload(token_data):
|
|
129
|
+
try:
|
|
130
|
+
if not isinstance(token_data, dict):
|
|
131
|
+
return '{"token":"***"}'
|
|
132
|
+
masked = dict(token_data)
|
|
133
|
+
for k in ('access_token', 'refresh_token', 'id_token'):
|
|
134
|
+
if k in masked:
|
|
135
|
+
masked[k] = _mask_token_value(masked.get(k))
|
|
136
|
+
return masked
|
|
137
|
+
except Exception:
|
|
138
|
+
return '{"token":"***"}'
|
|
139
|
+
|
|
140
|
+
|
|
118
141
|
def get_access_token():
|
|
119
142
|
if os.path.exists(TOKEN_PATH):
|
|
120
143
|
with open(TOKEN_PATH, 'r') as token_file:
|
|
121
144
|
token_data = json.load(token_file)
|
|
122
|
-
log("Loaded token data:\n {}".format(token_data))
|
|
145
|
+
log("Loaded token data (masked):\n {}".format(_mask_token_payload(token_data)))
|
|
123
146
|
if 'access_token' in token_data and 'expires_in' in token_data:
|
|
124
147
|
try:
|
|
125
148
|
token_time = token_data.get('token_time', time.time())
|
|
@@ -128,7 +151,7 @@ def get_access_token():
|
|
|
128
151
|
log("KeyError while accessing token data: {}".format(e))
|
|
129
152
|
return None
|
|
130
153
|
if token_expiry_time > time.time():
|
|
131
|
-
log("Access token is still valid. Expires in {} seconds.".format(token_expiry_time - time.time()))
|
|
154
|
+
log("Access token is still valid. Expires in {} seconds.".format(int(token_expiry_time - time.time())))
|
|
132
155
|
return token_data['access_token']
|
|
133
156
|
else:
|
|
134
157
|
log("Access token has expired. Current time: {}, Expiry time: {}".format(time.time(), token_expiry_time))
|
|
@@ -137,10 +160,10 @@ def get_access_token():
|
|
|
137
160
|
new_token_data['token_time'] = time.time()
|
|
138
161
|
with open(TOKEN_PATH, 'w') as token_file:
|
|
139
162
|
json.dump(new_token_data, token_file)
|
|
140
|
-
log("Access token refreshed successfully. New token data: {}".format(new_token_data))
|
|
163
|
+
log("Access token refreshed successfully. New token data (masked): {}".format(_mask_token_payload(new_token_data)))
|
|
141
164
|
return new_token_data['access_token']
|
|
142
165
|
else:
|
|
143
|
-
log("Failed to refresh access token.
|
|
166
|
+
log("Failed to refresh access token. Response (masked): {}".format(_mask_token_payload(new_token_data)))
|
|
144
167
|
return None
|
|
145
168
|
log("Access token not found. Please authenticate.")
|
|
146
169
|
return None
|
|
@@ -23,7 +23,7 @@ if PERFORMANCE_LOGGING:
|
|
|
23
23
|
# Now import core utilities after path setup
|
|
24
24
|
from MediCafe.core_utils import get_shared_config_loader, setup_module_paths, extract_medilink_config
|
|
25
25
|
from MediCafe.error_reporter import collect_support_bundle, capture_unhandled_traceback
|
|
26
|
-
from MediCafe.error_reporter import submit_support_bundle_email
|
|
26
|
+
from MediCafe.error_reporter import submit_support_bundle_email, list_queued_bundles, submit_all_queued_bundles, delete_all_queued_bundles
|
|
27
27
|
setup_module_paths(__file__)
|
|
28
28
|
|
|
29
29
|
# Import modules after path setup
|
|
@@ -74,6 +74,7 @@ def _tools_menu(config, medi):
|
|
|
74
74
|
options = [
|
|
75
75
|
"Rebuild submission index now",
|
|
76
76
|
"Submit Error Report (email)",
|
|
77
|
+
"Resolve Queued Error Reports",
|
|
77
78
|
"Back"
|
|
78
79
|
]
|
|
79
80
|
MediLink_UI.display_menu(options)
|
|
@@ -113,6 +114,18 @@ def _tools_menu(config, medi):
|
|
|
113
114
|
except Exception as e:
|
|
114
115
|
print("Error during email report submission: {}".format(e))
|
|
115
116
|
elif choice == '3':
|
|
117
|
+
try:
|
|
118
|
+
queued = list_queued_bundles()
|
|
119
|
+
if not queued:
|
|
120
|
+
print("No queued bundles found.")
|
|
121
|
+
else:
|
|
122
|
+
print("Found {} queued bundle(s).".format(len(queued)))
|
|
123
|
+
print("Attempting to send now...")
|
|
124
|
+
sent, failed = submit_all_queued_bundles()
|
|
125
|
+
print("Queued send complete. Sent: {} Failed: {}".format(sent, failed))
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print("Error while processing queued bundles: {}".format(e))
|
|
128
|
+
elif choice == '4':
|
|
116
129
|
break
|
|
117
130
|
else:
|
|
118
131
|
MediLink_UI.display_invalid_choice()
|
|
@@ -502,21 +515,36 @@ if __name__ == "__main__":
|
|
|
502
515
|
online = check_internet_connection()
|
|
503
516
|
if online:
|
|
504
517
|
success = submit_support_bundle_email(zip_path)
|
|
505
|
-
if
|
|
506
|
-
|
|
518
|
+
if success:
|
|
519
|
+
# On success, remove the bundle
|
|
520
|
+
try:
|
|
521
|
+
os.remove(zip_path)
|
|
522
|
+
except Exception:
|
|
523
|
+
pass
|
|
524
|
+
else:
|
|
525
|
+
# Preserve bundle for manual retry
|
|
526
|
+
print("Send failed - bundle preserved at {} for retry.".format(zip_path))
|
|
507
527
|
else:
|
|
508
528
|
ans = input("Offline. Connect to internet, then press Y to retry or N to discard: ").strip().lower()
|
|
509
529
|
if ans == 'y':
|
|
510
530
|
online = check_internet_connection()
|
|
511
531
|
if online:
|
|
512
532
|
success = submit_support_bundle_email(zip_path)
|
|
513
|
-
if
|
|
514
|
-
|
|
533
|
+
if success:
|
|
534
|
+
try:
|
|
535
|
+
os.remove(zip_path)
|
|
536
|
+
except Exception:
|
|
537
|
+
pass
|
|
538
|
+
else:
|
|
539
|
+
print("Send failed - bundle preserved at {} for retry.".format(zip_path))
|
|
515
540
|
else:
|
|
516
|
-
print("Still offline -
|
|
541
|
+
print("Still offline - preserving bundle at {} for retry.".format(zip_path))
|
|
517
542
|
else:
|
|
518
|
-
print("Discarding.")
|
|
519
|
-
|
|
543
|
+
print("Discarding bundle at user request.")
|
|
544
|
+
try:
|
|
545
|
+
os.remove(zip_path)
|
|
546
|
+
except Exception:
|
|
547
|
+
pass
|
|
520
548
|
exit_code = 1
|
|
521
549
|
finally:
|
|
522
550
|
if exit_code == 0 and PERFORMANCE_LOGGING:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|