medicafe 0.251027.1__py3-none-any.whl → 0.251027.3__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.

Potentially problematic release.


This version of medicafe might be problematic. Click here for more details.

MediBot/MediBot.bat CHANGED
@@ -729,18 +729,20 @@ echo 1. Open Latest Log File
729
729
  echo 2. Open WinSCP Logs
730
730
  echo 3. Clear Python Cache
731
731
  echo 4. Toggle Performance Logging ^(session^)
732
- echo 5. Forced MediCafe version rollback
733
- echo 6. Process CSV Files
734
- echo 7. Back to Main Menu
732
+ echo 5. Send TEST error report (email)
733
+ echo 6. Forced MediCafe version rollback
734
+ echo 7. Process CSV Files
735
+ echo 8. Back to Main Menu
735
736
  echo.
736
737
  set /p tchoice=Enter your choice:
737
738
  if "%tchoice%"=="1" goto open_latest_log
738
739
  if "%tchoice%"=="2" goto open_winscp_logs
739
740
  if "%tchoice%"=="3" goto clear_cache_menu
740
741
  if "%tchoice%"=="4" goto toggle_perf_logging
741
- if "%tchoice%"=="5" goto forced_version_rollback
742
- if "%tchoice%"=="6" goto process_csvs
743
- if "%tchoice%"=="7" goto main_menu
742
+ if "%tchoice%"=="5" goto send_test_error_report
743
+ if "%tchoice%"=="6" goto forced_version_rollback
744
+ if "%tchoice%"=="7" goto process_csvs
745
+ if "%tchoice%"=="8" goto main_menu
744
746
  echo Invalid choice. Please try again.
745
747
  pause
746
748
  goto troubleshooting_menu
@@ -788,6 +790,34 @@ if "%_winscp_found%"=="0" (
788
790
  pause >nul
789
791
  goto troubleshooting_menu
790
792
 
793
+ :::: Send TEST error report via MediCafe CLI
794
+ :send_test_error_report
795
+ cls
796
+ echo ========================================
797
+ echo Send TEST Error Report (no real traceback)
798
+ echo ========================================
799
+ echo.
800
+ if "!internet_available!"=="0" (
801
+ echo [WARNING] No internet connection available.
802
+ echo This feature requires internet to email the test report.
803
+ pause >nul
804
+ goto troubleshooting_menu
805
+ )
806
+ echo Building and sending test bundle via MediCafe...
807
+ cd /d "%~dp0.."
808
+ python -m MediCafe send_test_error_report
809
+ if errorlevel 1 (
810
+ echo.
811
+ echo [ERROR] Test error report failed to send.
812
+ echo The bundle, if created, remains in reports_queue for manual retry.
813
+ pause >nul
814
+ goto troubleshooting_menu
815
+ )
816
+ echo.
817
+ echo [OK] Test error report sent.
818
+ pause >nul
819
+ goto troubleshooting_menu
820
+
791
821
  ::: End Script
792
822
  :end_script
793
823
  echo Exiting MediBot
MediBot/__init__.py CHANGED
@@ -19,7 +19,7 @@ Smart Import Integration:
19
19
  medibot_main = get_components('medibot_main')
20
20
  """
21
21
 
22
- __version__ = "0.251027.1"
22
+ __version__ = "0.251027.3"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25
 
MediCafe/__init__.py CHANGED
@@ -27,7 +27,7 @@ Smart Import System:
27
27
  api_suite = get_api_access()
28
28
  """
29
29
 
30
- __version__ = "0.251027.1"
30
+ __version__ = "0.251027.3"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
MediCafe/__main__.py CHANGED
@@ -15,6 +15,7 @@ Commands:
15
15
  claims_status - Run United Claims Status checker
16
16
  deductible - Run United Deductible checker
17
17
  download_emails - Run email download functionality
18
+ send_test_error_report - Create and email a TEST support bundle
18
19
  version - Show MediCafe version information
19
20
 
20
21
  The entry point preserves user choices and navigational flow from the
@@ -252,9 +253,6 @@ def run_download_emails():
252
253
  except ImportError as e:
253
254
  print("Error: Unable to import MediLink_Gmail: {}".format(e))
254
255
  return 1
255
- except Exception as e:
256
- print("Error running email download: {}".format(e))
257
- return 1
258
256
 
259
257
  def show_version():
260
258
  """Show MediCafe version information"""
@@ -280,7 +278,7 @@ def main():
280
278
 
281
279
  parser.add_argument(
282
280
  'command',
283
- choices=['medibot', 'medilink', 'claims_status', 'deductible', 'download_emails', 'version'],
281
+ choices=['medibot', 'medilink', 'claims_status', 'deductible', 'download_emails', 'send_test_error_report', 'version'],
284
282
  help='Command to execute'
285
283
  )
286
284
 
@@ -309,6 +307,10 @@ def main():
309
307
  return run_deductible()
310
308
  elif args.command == 'download_emails':
311
309
  return run_download_emails()
310
+ elif args.command == 'send_test_error_report':
311
+ # Import lazily and call directly to avoid middlemen
312
+ from MediCafe.error_reporter import email_test_error_report_flow
313
+ return email_test_error_report_flow()
312
314
  elif args.command == 'version':
313
315
  return show_version()
314
316
  else:
@@ -1,5 +1,4 @@
1
1
  import base64
2
- import hashlib
3
2
  import json
4
3
  import os
5
4
  import platform
@@ -8,6 +7,7 @@ import time
8
7
  import zipfile
9
8
 
10
9
  import requests
10
+ import traceback
11
11
 
12
12
  from email.mime.application import MIMEApplication
13
13
  from email.mime.multipart import MIMEMultipart
@@ -99,72 +99,153 @@ def _ensure_dir(path):
99
99
  return False
100
100
 
101
101
 
102
+ # Resolve a writable queue directory with fallback to ./reports_queue
103
+ def _resolve_queue_dir(medi_config):
104
+ try:
105
+ local_storage_path = medi_config.get('local_storage_path', '.') if isinstance(medi_config, dict) else '.'
106
+ except Exception:
107
+ local_storage_path = '.'
108
+ primary = os.path.join(local_storage_path, 'reports_queue')
109
+ if _ensure_dir(primary):
110
+ return primary
111
+ fallback = os.path.join('.', 'reports_queue')
112
+ if _ensure_dir(fallback):
113
+ try:
114
+ mc_log("Queue directory fallback to ./reports_queue due to path/permission issue.", level="WARNING")
115
+ print("Falling back to ./reports_queue for support bundles (check local_storage_path permissions).")
116
+ except Exception:
117
+ pass
118
+ return fallback
119
+ return primary
120
+
121
+
122
+ def _build_support_zip(zip_path, local_storage_path, max_log_lines, traceback_text, include_winscp, meta):
123
+ latest_log = _get_latest_log_path(local_storage_path)
124
+ log_tail = _tail_file(latest_log, max_log_lines) if latest_log else ''
125
+ log_tail = _redact(log_tail)
126
+
127
+ try:
128
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
129
+ z.writestr('meta.json', json.dumps(meta, ensure_ascii=True, indent=2))
130
+ if latest_log and log_tail:
131
+ z.writestr('log_tail.txt', log_tail)
132
+ if traceback_text:
133
+ z.writestr('traceback.txt', _redact(traceback_text))
134
+ if include_winscp:
135
+ upload_log = os.path.join(local_storage_path, 'winscp_upload.log')
136
+ download_log = os.path.join(local_storage_path, 'winscp_download.log')
137
+ winscp_logs = [(p, os.path.getmtime(p)) for p in [upload_log, download_log] if os.path.exists(p)]
138
+ if winscp_logs:
139
+ latest_winscp = max(winscp_logs, key=lambda x: x[1])[0]
140
+ winscp_tail = _tail_file(latest_winscp, max_log_lines) if latest_winscp else ''
141
+ winscp_tail = _redact(winscp_tail)
142
+ if winscp_tail:
143
+ z.writestr('winscp_log_tail.txt', winscp_tail)
144
+ return True
145
+ except Exception as e:
146
+ mc_log('Error creating support bundle at {}: {}'.format(zip_path, e), level='ERROR')
147
+ return False
148
+
149
+ # Centralized self-healing helpers for email reporting
150
+ def _attempt_gmail_reauth_interactive(max_wait_seconds=120):
151
+ """
152
+ Delegate to MediLink.MediLink_Gmail.ensure_authenticated_for_gmail_send to
153
+ reuse existing OAuth/server logic without duplicating implementations.
154
+ """
155
+ try:
156
+ from MediLink.MediLink_Gmail import ensure_authenticated_for_gmail_send
157
+ return bool(ensure_authenticated_for_gmail_send(max_wait_seconds=max_wait_seconds))
158
+ except Exception as e:
159
+ try:
160
+ mc_log("Failed to initiate Gmail re-authorization: {0}".format(e), level="ERROR")
161
+ except Exception:
162
+ pass
163
+ return False
164
+
102
165
  # _compute_report_id removed (unused)
103
166
 
104
167
 
105
168
  def collect_support_bundle(include_traceback=True, max_log_lines=500):
106
- config, _ = load_configuration()
107
- medi = config.get('MediLink_Config', {})
108
- local_storage_path = medi.get('local_storage_path', '.')
109
- queue_dir = os.path.join(local_storage_path, 'reports_queue')
110
- _ensure_dir(queue_dir)
111
-
112
- stamp = time.strftime('%Y%m%d_%H%M%S')
113
- bundle_name = 'support_report_{}.zip'.format(stamp)
114
- zip_path = os.path.join(queue_dir, bundle_name)
115
-
116
- latest_log = _get_latest_log_path(local_storage_path)
117
- log_tail = _tail_file(latest_log, max_log_lines) if latest_log else ''
118
- log_tail = _redact(log_tail)
119
-
120
- traceback_txt = ''
121
- if include_traceback:
122
- try:
123
- trace_path = os.path.join(local_storage_path, 'traceback.txt')
124
- if os.path.exists(trace_path):
125
- with open(trace_path, 'r') as tf:
126
- traceback_txt = _redact(tf.read())
127
- except Exception:
128
- traceback_txt = ''
129
-
130
- meta = {
131
- 'app_version': _safe_ascii(_get_version()),
132
- 'python_version': _safe_ascii(sys.version.split(' ')[0]),
133
- 'platform': _safe_ascii(platform.platform()),
134
- 'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
135
- 'error_summary': _safe_ascii(_first_line(traceback_txt)),
136
- 'traceback_present': bool(traceback_txt),
137
- 'config_flags': {
138
- 'console_logging': bool(medi.get('logging', {}).get('console_output', False)),
139
- 'test_mode': bool(medi.get('TestMode', False))
140
- }
141
- }
169
+ config, _ = load_configuration()
170
+ medi = config.get('MediLink_Config', {})
171
+ local_storage_path = medi.get('local_storage_path', '.')
172
+ queue_dir = _resolve_queue_dir(medi)
142
173
 
143
- try:
144
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
145
- z.writestr('meta.json', json.dumps(meta, ensure_ascii=True, indent=2))
146
- if latest_log and log_tail:
147
- z.writestr('log_tail.txt', log_tail)
148
- if traceback_txt:
149
- z.writestr('traceback.txt', traceback_txt)
150
- # Get latest WinSCP log
151
- upload_log = os.path.join(local_storage_path, 'winscp_upload.log')
152
- download_log = os.path.join(local_storage_path, 'winscp_download.log')
153
- winscp_logs = [(p, os.path.getmtime(p)) for p in [upload_log, download_log] if os.path.exists(p)]
154
- if winscp_logs:
155
- latest_winscp = max(winscp_logs, key=lambda x: x[1])[0]
156
- winscp_tail = _tail_file(latest_winscp, max_log_lines) if latest_winscp else ''
157
- winscp_tail = _redact(winscp_tail)
158
- else:
159
- winscp_tail = ''
160
- if winscp_tail:
161
- z.writestr('winscp_log_tail.txt', winscp_tail)
162
- except Exception as e:
163
- mc_log('Error creating support bundle: {}'.format(e), level='ERROR')
164
- return None
174
+ stamp = time.strftime('%Y%m%d_%H%M%S')
175
+ zip_path = os.path.join(queue_dir, 'support_report_{}.zip'.format(stamp))
165
176
 
166
- # Do not delete oversize bundles here; sender will enforce size based on config
167
- return zip_path
177
+ traceback_txt = ''
178
+ if include_traceback:
179
+ try:
180
+ trace_path = os.path.join(local_storage_path, 'traceback.txt')
181
+ if os.path.exists(trace_path):
182
+ with open(trace_path, 'r') as tf:
183
+ traceback_txt = tf.read()
184
+ except Exception:
185
+ traceback_txt = ''
186
+
187
+ meta = {
188
+ 'app_version': _safe_ascii(_get_version()),
189
+ 'python_version': _safe_ascii(sys.version.split(' ')[0]),
190
+ 'platform': _safe_ascii(platform.platform()),
191
+ 'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
192
+ 'error_summary': _safe_ascii(_first_line(traceback_txt)),
193
+ 'traceback_present': bool(traceback_txt),
194
+ 'config_flags': {
195
+ 'console_logging': bool(medi.get('logging', {}).get('console_output', False)),
196
+ 'test_mode': bool(medi.get('TestMode', False))
197
+ }
198
+ }
199
+
200
+ ok = _build_support_zip(zip_path, local_storage_path, max_log_lines, traceback_txt, True, meta)
201
+ return zip_path if ok else None
202
+
203
+
204
+ def collect_test_support_bundle(max_log_lines=500):
205
+ """
206
+ Build a support bundle using the latest available logs and a placeholder
207
+ (fake) traceback to exercise the reporting pipeline without exposing
208
+ real exception data.
209
+
210
+ Returns absolute path to the created ZIP, or None on failure.
211
+ """
212
+ try:
213
+ config, _ = load_configuration()
214
+ medi = config.get('MediLink_Config', {})
215
+ local_storage_path = medi.get('local_storage_path', '.')
216
+ queue_dir = _resolve_queue_dir(medi)
217
+
218
+ stamp = time.strftime('%Y%m%d_%H%M%S')
219
+ zip_path = os.path.join(queue_dir, 'support_report_TEST_{}.zip'.format(stamp))
220
+
221
+ # Build a placeholder traceback - ASCII-only, no real data
222
+ fake_tb = (
223
+ "Traceback (most recent call last):\n"
224
+ " File \"MediCafe/test_runner.py\", line 42, in <module>\n"
225
+ " File \"MediCafe/error_reporter.py\", line 123, in simulate_error\n"
226
+ "Exception: This is a TEST placeholder traceback for pipeline verification only.\n"
227
+ "-- No real patient or PHI data is included. --\n"
228
+ )
229
+ meta = {
230
+ 'app_version': _safe_ascii(_get_version()),
231
+ 'python_version': _safe_ascii(sys.version.split(' ')[0]),
232
+ 'platform': _safe_ascii(platform.platform()),
233
+ 'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
234
+ 'error_summary': 'TEST: Placeholder traceback',
235
+ 'traceback_present': True,
236
+ 'config_flags': {
237
+ 'console_logging': bool(medi.get('logging', {}).get('console_output', False)),
238
+ 'test_mode': True
239
+ }
240
+ }
241
+ ok = _build_support_zip(zip_path, local_storage_path, max_log_lines, fake_tb, True, meta)
242
+ return zip_path if ok else None
243
+ except Exception as e:
244
+ try:
245
+ mc_log('Error creating TEST support bundle: {}'.format(e), level='ERROR')
246
+ except Exception:
247
+ pass
248
+ return None
168
249
 
169
250
 
170
251
  def _first_line(text):
@@ -192,12 +273,11 @@ def capture_unhandled_traceback(exc_type, exc_value, exc_traceback):
192
273
  medi = config.get('MediLink_Config', {})
193
274
  local_storage_path = medi.get('local_storage_path', '.')
194
275
  trace_path = os.path.join(local_storage_path, 'traceback.txt')
195
- import traceback
196
276
  text = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
197
277
  text = _redact(text)
198
278
  with open(trace_path, 'w') as f:
199
279
  f.write(text)
200
- print("An error occurred. A traceback was saved to {}".format(trace_path))
280
+ print("An error occurred. A traceback was saved to {}".format(trace_path))
201
281
  except Exception:
202
282
  try:
203
283
  mc_log('Failed to capture traceback to file', level='WARNING')
@@ -209,6 +289,10 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
209
289
  zip_path = collect_support_bundle(include_traceback)
210
290
  if not zip_path:
211
291
  mc_log("Failed to create bundle.", level="ERROR")
292
+ try:
293
+ print("Failed to create support bundle. Ensure 'MediLink_Config.local_storage_path' is writable and logs exist.")
294
+ except Exception:
295
+ pass
212
296
  return False
213
297
  bundle_size = os.path.getsize(zip_path)
214
298
  config, _ = load_configuration()
@@ -220,18 +304,56 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
220
304
  max_bytes = 1572864
221
305
  if bundle_size > max_bytes:
222
306
  mc_log("Bundle too large ({} bytes > {} bytes) - leaving in queue.".format(bundle_size, max_bytes), level="WARNING")
307
+ try:
308
+ print("Bundle too large to email ({} KB > {} KB). Left in 'reports_queue'. Path: {}".format(int(bundle_size/1024), int(max_bytes/1024), zip_path))
309
+ print("Attempting to create and send a smaller LITE bundle (reduced logs, no traceback).")
310
+ except Exception:
311
+ pass
312
+ # Attempt a smaller bundle automatically
313
+ lite_zip = collect_support_bundle_lite(max_log_lines=200)
314
+ if lite_zip and os.path.exists(lite_zip):
315
+ try:
316
+ lite_size = os.path.getsize(lite_zip)
317
+ except Exception:
318
+ lite_size = -1
319
+ if 0 < lite_size <= max_bytes:
320
+ return submit_support_bundle_email(zip_path=lite_zip, include_traceback=False)
321
+ else:
322
+ try:
323
+ print("LITE bundle is still too large ({} KB).".format(int(max(lite_size, 0)/1024)))
324
+ except Exception:
325
+ pass
326
+ try:
327
+ print("Tip: Reduce log size or increase 'MediLink_Config.error_reporting.email.max_bundle_bytes'.")
328
+ except Exception:
329
+ pass
223
330
  return False
224
331
  # Feature is always available; proceed if recipients and token are available
225
332
  # Normalize and validate recipients
226
333
  to_emails = _normalize_recipients(email_config.get('to', []))
227
334
  if not to_emails:
228
335
  mc_log("No valid recipients configured in error_reporting.email.to", level="ERROR")
336
+ try:
337
+ print("No recipients configured. Set 'MediLink_Config.error_reporting.email.to' to one or more email addresses.")
338
+ except Exception:
339
+ pass
229
340
  return False
230
341
  subject_prefix = email_config.get('subject_prefix', 'MediCafe Error Report')
231
342
  access_token = get_access_token()
232
343
  if not access_token:
233
- mc_log("No access token - authenticate first.", level="ERROR")
234
- return False
344
+ mc_log("No access token - attempting Gmail re-authorization.", level="ERROR")
345
+ try:
346
+ print("No Gmail token found. Starting re-authorization...")
347
+ except Exception:
348
+ pass
349
+ if _attempt_gmail_reauth_interactive():
350
+ access_token = get_access_token()
351
+ if not access_token:
352
+ try:
353
+ print("Authentication incomplete. Please finish Gmail consent, then retry.")
354
+ except Exception:
355
+ pass
356
+ return False
235
357
  mc_log("Building email...", level="INFO")
236
358
  msg = MIMEMultipart()
237
359
  msg['To'] = ', '.join(to_emails)
@@ -246,7 +368,16 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
246
368
  mc_log("Sending report...", level="INFO")
247
369
  headers = {'Authorization': 'Bearer {}'.format(access_token), 'Content-Type': 'application/json'}
248
370
  data = {'raw': raw}
249
- resp = requests.post('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', headers=headers, json=data)
371
+ try:
372
+ resp = requests.post('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', headers=headers, json=data)
373
+ except Exception as e:
374
+ mc_log("Network error during Gmail send: {0}".format(e), level="ERROR")
375
+ try:
376
+ print("Network error while sending report: {0}".format(e))
377
+ print("Check internet connectivity or proxy settings and retry.")
378
+ except Exception:
379
+ pass
380
+ return False
250
381
  if resp.status_code == 200:
251
382
  mc_log("Report sent successfully!", level="INFO")
252
383
  try:
@@ -255,20 +386,62 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
255
386
  pass
256
387
  return True
257
388
  else:
258
- # Handle auth errors by prompting re-consent using existing OAuth helpers
389
+ # Handle auth errors by prompting re-consent using existing OAuth helpers, then retry once
259
390
  if resp.status_code in (401, 403):
391
+ mc_log("Gmail send unauthorized ({}). Attempting re-authorization and retry.".format(resp.status_code), level="WARNING")
260
392
  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)
393
+ print("Gmail permission issue detected ({}). Starting re-authorization...".format(resp.status_code))
269
394
  except Exception:
270
395
  pass
396
+ if _attempt_gmail_reauth_interactive():
397
+ new_token = get_access_token()
398
+ if new_token:
399
+ headers['Authorization'] = 'Bearer {}'.format(new_token)
400
+ try:
401
+ resp = requests.post('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', headers=headers, json=data)
402
+ except Exception as e:
403
+ mc_log("Network error on retry: {0}".format(e), level="ERROR")
404
+ try:
405
+ print("Network error on retry: {0}".format(e))
406
+ except Exception:
407
+ pass
408
+ return False
409
+ if resp.status_code == 200:
410
+ mc_log("Report sent successfully after re-authorization!", level="INFO")
411
+ try:
412
+ os.remove(zip_path)
413
+ except Exception:
414
+ pass
415
+ return True
416
+ # Map common Gmail errors to actionable hints
417
+ hint = ''
418
+ try:
419
+ body = resp.json()
420
+ err = body.get('error', {}) if isinstance(body, dict) else {}
421
+ status = (err.get('status') or '').upper()
422
+ message = err.get('message') or ''
423
+ reasons = ','.join([e.get('reason') for e in err.get('errors', []) if isinstance(e, dict) and e.get('reason')])
424
+ if 'RATELIMIT' in status or 'rateLimitExceeded' in reasons:
425
+ hint = 'Quota exceeded. Wait and retry later.'
426
+ elif 'DAILY' in status or 'dailyLimitExceeded' in reasons:
427
+ hint = 'Daily quota exceeded. Try again tomorrow.'
428
+ elif status in ('PERMISSION_DENIED', 'FORBIDDEN'):
429
+ hint = 'Permissions insufficient. Re-authorize Gmail with required scopes.'
430
+ elif status == 'INVALID_ARGUMENT':
431
+ hint = 'Invalid request. Check recipient emails and attachment size.'
432
+ elif status == 'UNAUTHENTICATED':
433
+ hint = 'Authentication required. Re-authorize and retry.'
434
+ except Exception:
435
+ pass
271
436
  mc_log("Failed to send: {} - {}".format(resp.status_code, _redact(resp.text)), level="ERROR")
437
+ try:
438
+ base_msg = "Failed to send report: HTTP {}.".format(resp.status_code)
439
+ if hint:
440
+ base_msg += " Hint: {}".format(hint)
441
+ print(base_msg)
442
+ print("The bundle remains in 'reports_queue'. See latest log for details.")
443
+ except Exception:
444
+ pass
272
445
  # Preserve bundle in queue for manual retry
273
446
  return False
274
447
 
@@ -277,7 +450,8 @@ def _normalize_recipients(to_field):
277
450
  try:
278
451
  # Flatten to a list of strings
279
452
  if isinstance(to_field, str):
280
- candidates = [p.strip() for p in to_field.split(',')]
453
+ separators_normalized = to_field.replace(';', ',').replace('\n', ',').replace('\r', ',')
454
+ candidates = [p.strip() for p in separators_normalized.split(',')]
281
455
  elif isinstance(to_field, list):
282
456
  candidates = [str(p).strip() for p in to_field]
283
457
  else:
@@ -301,16 +475,48 @@ def _normalize_recipients(to_field):
301
475
  return []
302
476
 
303
477
 
478
+ def collect_support_bundle_lite(max_log_lines=200):
479
+ """Create a smaller 'lite' support bundle with reduced logs and no traceback/winscp logs."""
480
+ try:
481
+ config, _ = load_configuration()
482
+ medi = config.get('MediLink_Config', {})
483
+ local_storage_path = medi.get('local_storage_path', '.')
484
+ queue_dir = _resolve_queue_dir(medi)
485
+ stamp = time.strftime('%Y%m%d_%H%M%S')
486
+ zip_path = os.path.join(queue_dir, 'support_report_LITE_{}.zip'.format(stamp))
487
+ meta = {
488
+ 'app_version': _safe_ascii(_get_version()),
489
+ 'python_version': _safe_ascii(sys.version.split(' ')[0]),
490
+ 'platform': _safe_ascii(platform.platform()),
491
+ 'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
492
+ 'error_summary': 'LITE: No traceback included',
493
+ 'traceback_present': False,
494
+ 'config_flags': {
495
+ 'console_logging': bool(medi.get('logging', {}).get('console_output', False)),
496
+ 'test_mode': bool(medi.get('TestMode', False))
497
+ }
498
+ }
499
+ ok = _build_support_zip(zip_path, local_storage_path, max_log_lines, None, False, meta)
500
+ return zip_path if ok else None
501
+ except Exception:
502
+ return None
503
+
504
+
304
505
  def list_queued_bundles():
305
506
  try:
306
507
  config, _ = load_configuration()
307
508
  medi = config.get('MediLink_Config', {})
308
509
  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()
510
+ primary = os.path.join(local_storage_path, 'reports_queue')
511
+ fallback = os.path.join('.', 'reports_queue')
512
+ files = []
513
+ for q in (primary, fallback):
514
+ try:
515
+ if os.path.isdir(q):
516
+ files.extend([os.path.join(q, f) for f in os.listdir(q) if f.endswith('.zip')])
517
+ except Exception:
518
+ pass
519
+ files = sorted(set(files))
314
520
  return files
315
521
  except Exception:
316
522
  return []
@@ -354,6 +560,42 @@ def email_error_report_flow():
354
560
  return 0 if sent else 1
355
561
  except Exception as e:
356
562
  mc_log("[ERROR] Exception during email report flow: {0}".format(e), level="ERROR")
563
+ try:
564
+ print("Unexpected error while sending error report: {0}".format(e))
565
+ except Exception:
566
+ pass
567
+ return 1
568
+
569
+ def email_test_error_report_flow():
570
+ """
571
+ Create and send a TEST error report bundle, containing a placeholder
572
+ traceback and latest log tails. Intended for troubleshooting the
573
+ submission pipeline only.
574
+ """
575
+ try:
576
+ zip_path = collect_test_support_bundle()
577
+ if not zip_path:
578
+ try:
579
+ print("Failed to create TEST support bundle. Ensure 'MediLink_Config.local_storage_path' is writable.")
580
+ except Exception:
581
+ pass
582
+ return 1
583
+ sent = submit_support_bundle_email(zip_path=zip_path, include_traceback=False)
584
+ if not sent:
585
+ try:
586
+ print("TEST error report was not sent. See messages above for the reason. The ZIP remains queued if creation succeeded.")
587
+ except Exception:
588
+ pass
589
+ return 0 if sent else 1
590
+ except Exception as e:
591
+ try:
592
+ mc_log("[ERROR] Exception during test email report flow: {0}".format(e), level="ERROR")
593
+ except Exception:
594
+ pass
595
+ try:
596
+ print("Unexpected error during TEST report flow: {0}".format(e))
597
+ except Exception:
598
+ pass
357
599
  return 1
358
600
 
359
601
  if __name__ == "__main__":
@@ -594,6 +594,71 @@ def clear_token_cache():
594
594
  def check_invalid_grant_causes(auth_code):
595
595
  log("FUTURE IMPLEMENTATION: Checking common causes for invalid_grant error with auth code: {}".format(auth_code))
596
596
 
597
+
598
+ def ensure_authenticated_for_gmail_send(max_wait_seconds=120):
599
+ """Ensure a valid Gmail access token is available for sending.
600
+
601
+ - Reuses existing OAuth helpers in this module.
602
+ - Starts the local HTTPS server if needed, opens the browser for consent,
603
+ and polls for a token for up to max_wait_seconds.
604
+ - Returns True if a usable access token is available after the flow; otherwise False.
605
+ """
606
+ try:
607
+ token = get_access_token()
608
+ except Exception:
609
+ token = None
610
+ if token:
611
+ return True
612
+
613
+ # Prepare server and certificates
614
+ try:
615
+ generate_self_signed_cert(cert_file, key_file)
616
+ except Exception as e:
617
+ log("Warning: could not ensure self-signed certs: {}".format(e))
618
+
619
+ server_started_here = False
620
+ global httpd
621
+ try:
622
+ if httpd is None:
623
+ log("Starting local HTTPS server for OAuth redirect handling.")
624
+ server_thread = Thread(target=run_server)
625
+ server_thread.daemon = True
626
+ server_thread.start()
627
+ server_started_here = True
628
+ time.sleep(0.5)
629
+ except Exception as e:
630
+ log("Failed to start OAuth local server: {}".format(e))
631
+
632
+ try:
633
+ auth_url = get_authorization_url()
634
+ print("Opening browser to authorize Gmail permission for sending...")
635
+ open_browser_with_executable(auth_url)
636
+ except Exception as e:
637
+ log("Failed to open authorization URL: {}".format(e))
638
+
639
+ # Poll for token availability within timeout
640
+ start_ts = time.time()
641
+ token = None
642
+ while time.time() - start_ts < max_wait_seconds:
643
+ try:
644
+ token = get_access_token()
645
+ except Exception:
646
+ token = None
647
+ if token:
648
+ break
649
+ time.sleep(3)
650
+
651
+ if server_started_here:
652
+ try:
653
+ stop_server()
654
+ except Exception:
655
+ pass
656
+
657
+ if not token:
658
+ print("Gmail authorization not completed within timeout. Please finish consent and retry.")
659
+
660
+ return bool(token)
661
+
597
662
  if __name__ == "__main__":
598
663
  signal.signal(signal.SIGINT, signal_handler)
599
664
  signal.signal(signal.SIGTERM, signal_handler)
MediLink/__init__.py CHANGED
@@ -22,7 +22,7 @@ Smart Import Integration:
22
22
  datamgmt = get_components('medilink_datamgmt')
23
23
  """
24
24
 
25
- __version__ = "0.251027.1"
25
+ __version__ = "0.251027.3"
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.251027.1
3
+ Version: 0.251027.3
4
4
  Summary: MediCafe
5
5
  Home-page: https://github.com/katanada2/MediCafe
6
6
  Author: Daniel Vidaud
@@ -1,4 +1,4 @@
1
- MediBot/MediBot.bat,sha256=vWcKZK7MdNIAnGpvpaSglN5DiAlKJVBpdtYwfQfpSQA,28234
1
+ MediBot/MediBot.bat,sha256=mBoD0Dit4Om_iGgSbSoVZ0d0kScGWofA7qaYqhlJhWo,29161
2
2
  MediBot/MediBot.py,sha256=ABSqWikb_c1VSuR4n8Vh5YfOqzDCi2jnnp3sg_xOYg0,51092
3
3
  MediBot/MediBot_Charges.py,sha256=a28if_f_IoazIHiqlaFosFnfEgEoCwb9LQ6aOyk5-D0,10704
4
4
  MediBot/MediBot_Crosswalk_Library.py,sha256=6LrpRx2UKVeH3TspS9LpR93iw5M7nTqN6IYpC-6PPGE,26060
@@ -12,7 +12,7 @@ MediBot/MediBot_dataformat_library.py,sha256=D46fdPtxcgfWTzaLBtSvjtozzZBNqNiODgu
12
12
  MediBot/MediBot_debug.bat,sha256=F5Lfi3nFEEo4Ddx9EbX94u3fNAMgzMp3wsn-ULyASTM,6017
13
13
  MediBot/MediBot_docx_decoder.py,sha256=9BSjV-kB90VHnqfL_5iX4zl5u0HcHvHuL7YNfx3gXpQ,33143
14
14
  MediBot/MediBot_smart_import.py,sha256=Emvz7NwemHGCHvG5kZcUyXMcCheidbGKaPfOTg-YCEs,6684
15
- MediBot/__init__.py,sha256=lyqFeD1_7gd2pYechV5n8IZmOJBoz8cFanU3yDdYYTA,3192
15
+ MediBot/__init__.py,sha256=W7JANBDt-VBMAzdtL-KXgJ3-ODNLji-8stnIn38LUIQ,3192
16
16
  MediBot/clear_cache.bat,sha256=F6-VhETWw6xDdGWG2wUqvtXjCl3lY4sSUFqF90bM8-8,1860
17
17
  MediBot/crash_diagnostic.bat,sha256=j8kUtyBg6NOWbXpeFuEqIRHOkVzgUrLOqO3FBMfNxTo,9268
18
18
  MediBot/f_drive_diagnostic.bat,sha256=4572hZaiwZ5wVAarPcZJQxkOSTwAdDuT_X914noARak,6878
@@ -22,14 +22,14 @@ MediBot/process_csvs.bat,sha256=3tI7h1z9eRj8rUUL4wJ7dy-Qrak20lRmpAPtGbUMbVQ,3489
22
22
  MediBot/update_json.py,sha256=vvUF4mKCuaVly8MmoadDO59M231fCIInc0KI1EtDtPA,3704
23
23
  MediBot/update_medicafe.py,sha256=G1lyvVOHYuho1d-TJQNN6qaB4HBWaJ2PpXqemBoPlRQ,17937
24
24
  MediCafe/MediLink_ConfigLoader.py,sha256=heTbZ0ItwlpxqbAb0oV2dbTFRSTMnZyhBT9fS8nI0YU,12735
25
- MediCafe/__init__.py,sha256=0mjW4VRx22WO9-iGooERKEGl9_XiPSXLPsmyAMhGcVk,5721
26
- MediCafe/__main__.py,sha256=mRNyk3D9Ilnu2XhgVI_rut7r5Ro7UIKtwV871giAHI8,12992
25
+ MediCafe/__init__.py,sha256=56u8QPxfilu-bGcU5MF8kleMtvRMpsOVEQ1VKKlZai4,5721
26
+ MediCafe/__main__.py,sha256=NZMgsjuXYF39YS7mCWMjTO0vYf_0IAotQpNyUXfbahs,13233
27
27
  MediCafe/api_core.py,sha256=wLAdRNZdmovKReXvzsmAgKrbYon4-wbJbGCyOm_C3AU,89896
28
28
  MediCafe/api_factory.py,sha256=I5AeJoyu6m7oCrjc2OvVvO_4KSBRutTsR1riiWhTZV0,12086
29
29
  MediCafe/api_utils.py,sha256=KWQB0q1k5E6frOFFlKWcFpHNcqfrS7KJ_82672wbupw,14041
30
30
  MediCafe/core_utils.py,sha256=XKUpyv7yKjIQ8iNrhD76PIURyt6GZxb98v0daiI7aaw,27303
31
31
  MediCafe/deductible_utils.py,sha256=-ixDYwI3JNAyACrFjKqoX_hD3Awzownq441U0PSrwXw,64932
32
- MediCafe/error_reporter.py,sha256=mKC6zbQN0rJHO91ASgrVgHHTCaUOFmCPisbwJiyn8gc,12296
32
+ MediCafe/error_reporter.py,sha256=421z3lYMlHD0p-OIJSDyvKWRYsImVJKvm7nG0s5drFc,23848
33
33
  MediCafe/graphql_utils.py,sha256=jo4CboMb9i5_qD0jkfrLbL87_Q3aFiwOntZhjF9fMsI,51928
34
34
  MediCafe/logging_config.py,sha256=auT65LN5oDEXVhkMeLke63kJHTWxYf2o8YihAfQFgzU,5493
35
35
  MediCafe/logging_demo.py,sha256=TwUhzafna5pMdN3zSKGrpUWRqX96F1JGGsSUtr3dygs,1975
@@ -51,7 +51,7 @@ MediLink/MediLink_Deductible.py,sha256=opGa5YQ6tfowurlf8xDWRtAtQMmoNYout0gYe3R5f
51
51
  MediLink/MediLink_Deductible_Validator.py,sha256=x6tHJOi88TblUpDPSH6QhIdXXRgr3rXI7kYPVGZYCgU,24998
52
52
  MediLink/MediLink_Display_Utils.py,sha256=MonsX6VPbdvqwY_V8sHUYrXCS0fMKc4toJvG0oyr-V4,24872
53
53
  MediLink/MediLink_Down.py,sha256=s4_z-RaqHYanjwbQCl-OSkg4XIpcIQ2Q6jXa8-q_QXw,28111
54
- MediLink/MediLink_Gmail.py,sha256=jS1mbqhndJ8emhMshd1qeOegF6k_3V_YeCGUNerqdqA,29394
54
+ MediLink/MediLink_Gmail.py,sha256=Gnc-tI8ACeFaw0YVPp5JUBAXhwYfptY8QGxShJu4Pbg,31476
55
55
  MediLink/MediLink_Mailer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
56
  MediLink/MediLink_Parser.py,sha256=eRVZ4ckZ5gDOrcvtCUZP3DOd3Djly66rCIk0aYXLz14,12567
57
57
  MediLink/MediLink_PatientProcessor.py,sha256=9r2w4p45d30Tn0kbXL3j5574MYOehP83tDirNOw_Aek,19977
@@ -63,15 +63,15 @@ MediLink/MediLink_insurance_utils.py,sha256=g741Fj2K26cMy0JX5d_XavMw9LgkK6hjaUJY
63
63
  MediLink/MediLink_main.py,sha256=1nmDtbJ9IfPwLnFhCZvDtBPSG_8gzj6LneuoV60l4IQ,26736
64
64
  MediLink/MediLink_smart_import.py,sha256=ZUXvAkIA2Pk2uuyLZazKfKK8YGdkZt1VAeZo_ZSUyxk,9942
65
65
  MediLink/Soumit_api.py,sha256=5JfOecK98ZC6NpZklZW2AkOzkjvrbYxpJpZNH3rFxDw,497
66
- MediLink/__init__.py,sha256=EXoIfj9XVlTBOpC-QfJ4xmQY8ILGHPxrd8xtgVQs-2U,3888
66
+ MediLink/__init__.py,sha256=I6BiRTvpQ5QhK_7dLbb_4VFtyYWZf1nH2dLX1OYtgr0,3888
67
67
  MediLink/gmail_http_utils.py,sha256=mYChIhkbA1oJaAJA-nY3XgHQY-H7zvZJUZPhUagomsI,4047
68
68
  MediLink/gmail_oauth_utils.py,sha256=Ugr-DEqs4_RddRMSCJ_dbgA3TVeaxpbAor-dktcTIgY,3713
69
69
  MediLink/openssl.cnf,sha256=76VdcGCykf0Typyiv8Wd1mMVKixrQ5RraG6HnfKFqTo,887
70
70
  MediLink/test.py,sha256=DM_E8gEbhbVfTAm3wTMiNnK2GCD1e5eH6gwTk89QIc4,3116
71
71
  MediLink/webapp.html,sha256=DwDYjVvluGJ7eDdvEogfKN4t24ZJRoIUuSBfCYCL-3w,21252
72
- medicafe-0.251027.1.dist-info/LICENSE,sha256=65lb-vVujdQK7uMH3RRJSMwUW-WMrMEsc5sOaUn2xUk,1096
73
- medicafe-0.251027.1.dist-info/METADATA,sha256=XLFJXaRWI3dI2gnxmGrhbt4aOPvqwqPsZwjl2pQaZ_E,3414
74
- medicafe-0.251027.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
75
- medicafe-0.251027.1.dist-info/entry_points.txt,sha256=m3RBUBjr-xRwEkKJ5W4a7NlqHZP_1rllGtjZnrRqKe8,52
76
- medicafe-0.251027.1.dist-info/top_level.txt,sha256=U6-WBJ9RCEjyIs0BlzbQq_PwedCp_IV9n1616NNV5zA,26
77
- medicafe-0.251027.1.dist-info/RECORD,,
72
+ medicafe-0.251027.3.dist-info/LICENSE,sha256=65lb-vVujdQK7uMH3RRJSMwUW-WMrMEsc5sOaUn2xUk,1096
73
+ medicafe-0.251027.3.dist-info/METADATA,sha256=xsE3H4MHme5SYio00FfRVaSBO51wYxwQIC190GL8_7Y,3414
74
+ medicafe-0.251027.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
75
+ medicafe-0.251027.3.dist-info/entry_points.txt,sha256=m3RBUBjr-xRwEkKJ5W4a7NlqHZP_1rllGtjZnrRqKe8,52
76
+ medicafe-0.251027.3.dist-info/top_level.txt,sha256=U6-WBJ9RCEjyIs0BlzbQq_PwedCp_IV9n1616NNV5zA,26
77
+ medicafe-0.251027.3.dist-info/RECORD,,