medicafe 0.251027.2__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/__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.2"
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.2"
30
+ __version__ = "0.251027.3"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
@@ -99,72 +99,106 @@ 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))
176
+
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
+ }
165
199
 
166
- # Do not delete oversize bundles here; sender will enforce size based on config
167
- return zip_path
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
168
202
 
169
203
 
170
204
  def collect_test_support_bundle(max_log_lines=500):
@@ -179,17 +213,10 @@ def collect_test_support_bundle(max_log_lines=500):
179
213
  config, _ = load_configuration()
180
214
  medi = config.get('MediLink_Config', {})
181
215
  local_storage_path = medi.get('local_storage_path', '.')
182
- queue_dir = os.path.join(local_storage_path, 'reports_queue')
183
- _ensure_dir(queue_dir)
216
+ queue_dir = _resolve_queue_dir(medi)
184
217
 
185
218
  stamp = time.strftime('%Y%m%d_%H%M%S')
186
- bundle_name = 'support_report_TEST_{}.zip'.format(stamp)
187
- zip_path = os.path.join(queue_dir, bundle_name)
188
-
189
- # Prepare components
190
- latest_log = _get_latest_log_path(local_storage_path)
191
- log_tail = _tail_file(latest_log, max_log_lines) if latest_log else ''
192
- log_tail = _redact(log_tail)
219
+ zip_path = os.path.join(queue_dir, 'support_report_TEST_{}.zip'.format(stamp))
193
220
 
194
221
  # Build a placeholder traceback - ASCII-only, no real data
195
222
  fake_tb = (
@@ -199,8 +226,6 @@ def collect_test_support_bundle(max_log_lines=500):
199
226
  "Exception: This is a TEST placeholder traceback for pipeline verification only.\n"
200
227
  "-- No real patient or PHI data is included. --\n"
201
228
  )
202
- fake_tb = _redact(fake_tb)
203
-
204
229
  meta = {
205
230
  'app_version': _safe_ascii(_get_version()),
206
231
  'python_version': _safe_ascii(sys.version.split(' ')[0]),
@@ -213,25 +238,8 @@ def collect_test_support_bundle(max_log_lines=500):
213
238
  'test_mode': True
214
239
  }
215
240
  }
216
-
217
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
218
- z.writestr('meta.json', json.dumps(meta, ensure_ascii=True, indent=2))
219
- if latest_log and log_tail:
220
- z.writestr('log_tail.txt', log_tail)
221
- # Always include the fake traceback for this test bundle
222
- z.writestr('traceback.txt', fake_tb)
223
- # Include WinSCP tail if present
224
- upload_log = os.path.join(local_storage_path, 'winscp_upload.log')
225
- download_log = os.path.join(local_storage_path, 'winscp_download.log')
226
- winscp_logs = [(p, os.path.getmtime(p)) for p in [upload_log, download_log] if os.path.exists(p)]
227
- if winscp_logs:
228
- latest_winscp = max(winscp_logs, key=lambda x: x[1])[0]
229
- winscp_tail = _tail_file(latest_winscp, max_log_lines) if latest_winscp else ''
230
- winscp_tail = _redact(winscp_tail)
231
- if winscp_tail:
232
- z.writestr('winscp_log_tail.txt', winscp_tail)
233
-
234
- return zip_path
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
235
243
  except Exception as e:
236
244
  try:
237
245
  mc_log('Error creating TEST support bundle: {}'.format(e), level='ERROR')
@@ -281,6 +289,10 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
281
289
  zip_path = collect_support_bundle(include_traceback)
282
290
  if not zip_path:
283
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
284
296
  return False
285
297
  bundle_size = os.path.getsize(zip_path)
286
298
  config, _ = load_configuration()
@@ -292,18 +304,56 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
292
304
  max_bytes = 1572864
293
305
  if bundle_size > max_bytes:
294
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
295
330
  return False
296
331
  # Feature is always available; proceed if recipients and token are available
297
332
  # Normalize and validate recipients
298
333
  to_emails = _normalize_recipients(email_config.get('to', []))
299
334
  if not to_emails:
300
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
301
340
  return False
302
341
  subject_prefix = email_config.get('subject_prefix', 'MediCafe Error Report')
303
342
  access_token = get_access_token()
304
343
  if not access_token:
305
- mc_log("No access token - authenticate first.", level="ERROR")
306
- 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
307
357
  mc_log("Building email...", level="INFO")
308
358
  msg = MIMEMultipart()
309
359
  msg['To'] = ', '.join(to_emails)
@@ -318,7 +368,16 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
318
368
  mc_log("Sending report...", level="INFO")
319
369
  headers = {'Authorization': 'Bearer {}'.format(access_token), 'Content-Type': 'application/json'}
320
370
  data = {'raw': raw}
321
- 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
322
381
  if resp.status_code == 200:
323
382
  mc_log("Report sent successfully!", level="INFO")
324
383
  try:
@@ -327,20 +386,62 @@ def submit_support_bundle_email(zip_path=None, include_traceback=True):
327
386
  pass
328
387
  return True
329
388
  else:
330
- # 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
331
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")
332
392
  try:
333
- from MediLink.MediLink_Gmail import get_authorization_url, open_browser_with_executable
334
- auth_url = get_authorization_url()
335
- mc_log("Gmail send unauthorized ({}). Opening browser for re-consent.".format(resp.status_code), level="WARNING")
336
- try:
337
- print("Your Google session needs to be refreshed to gain gmail.send permission. A browser will open to re-authorize.")
338
- except Exception:
339
- pass
340
- open_browser_with_executable(auth_url)
393
+ print("Gmail permission issue detected ({}). Starting re-authorization...".format(resp.status_code))
341
394
  except Exception:
342
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
343
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
344
445
  # Preserve bundle in queue for manual retry
345
446
  return False
346
447
 
@@ -349,7 +450,8 @@ def _normalize_recipients(to_field):
349
450
  try:
350
451
  # Flatten to a list of strings
351
452
  if isinstance(to_field, str):
352
- 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(',')]
353
455
  elif isinstance(to_field, list):
354
456
  candidates = [str(p).strip() for p in to_field]
355
457
  else:
@@ -373,16 +475,48 @@ def _normalize_recipients(to_field):
373
475
  return []
374
476
 
375
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
+
376
505
  def list_queued_bundles():
377
506
  try:
378
507
  config, _ = load_configuration()
379
508
  medi = config.get('MediLink_Config', {})
380
509
  local_storage_path = medi.get('local_storage_path', '.')
381
- queue_dir = os.path.join(local_storage_path, 'reports_queue')
382
- if not os.path.isdir(queue_dir):
383
- return []
384
- files = [os.path.join(queue_dir, f) for f in os.listdir(queue_dir) if f.endswith('.zip')]
385
- 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))
386
520
  return files
387
521
  except Exception:
388
522
  return []
@@ -426,6 +560,10 @@ def email_error_report_flow():
426
560
  return 0 if sent else 1
427
561
  except Exception as e:
428
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
429
567
  return 1
430
568
 
431
569
  def email_test_error_report_flow():
@@ -437,14 +575,27 @@ def email_test_error_report_flow():
437
575
  try:
438
576
  zip_path = collect_test_support_bundle()
439
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
440
582
  return 1
441
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
442
589
  return 0 if sent else 1
443
590
  except Exception as e:
444
591
  try:
445
592
  mc_log("[ERROR] Exception during test email report flow: {0}".format(e), level="ERROR")
446
593
  except Exception:
447
594
  pass
595
+ try:
596
+ print("Unexpected error during TEST report flow: {0}".format(e))
597
+ except Exception:
598
+ pass
448
599
  return 1
449
600
 
450
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.2"
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.2
3
+ Version: 0.251027.3
4
4
  Summary: MediCafe
5
5
  Home-page: https://github.com/katanada2/MediCafe
6
6
  Author: Daniel Vidaud
@@ -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=paoTAa_Pvw1WQa1rfQAhhoIe2cGJG86VgBJN0jY1at4,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=KbP2Gqit62W6clfrnB5ONUW-hWDkJdRDGWdcycyk7rY,5721
25
+ MediCafe/__init__.py,sha256=56u8QPxfilu-bGcU5MF8kleMtvRMpsOVEQ1VKKlZai4,5721
26
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=wskeDp8XtddKTGukeZR2pmYUz-mHWXzniU40G83jWRc,16278
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=XPx_-C6fPBZrFWCGWQmgsYhU2E86ZG1QmrimiRGQ1_Q,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.2.dist-info/LICENSE,sha256=65lb-vVujdQK7uMH3RRJSMwUW-WMrMEsc5sOaUn2xUk,1096
73
- medicafe-0.251027.2.dist-info/METADATA,sha256=mJR2IXdkiGCl38hcO2EvQL4A8eAkiNukWILCvuVG_RY,3414
74
- medicafe-0.251027.2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
75
- medicafe-0.251027.2.dist-info/entry_points.txt,sha256=m3RBUBjr-xRwEkKJ5W4a7NlqHZP_1rllGtjZnrRqKe8,52
76
- medicafe-0.251027.2.dist-info/top_level.txt,sha256=U6-WBJ9RCEjyIs0BlzbQq_PwedCp_IV9n1616NNV5zA,26
77
- medicafe-0.251027.2.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,,