medicafe 0.251017.1__py3-none-any.whl → 0.251026.0__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.251017.1"
22
+ __version__ = "0.251026.0"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25
 
@@ -38,7 +38,14 @@ def get_default_config():
38
38
  'endpoint_url': '',
39
39
  'auth_token': '',
40
40
  'insecure_http': False,
41
- 'max_bundle_bytes': 2097152
41
+ 'max_bundle_bytes': 2097152,
42
+ 'email': {
43
+ 'enabled': False,
44
+ 'to': '',
45
+ 'subject_prefix': 'MediCafe Error Report',
46
+ 'max_bundle_bytes': 1572864,
47
+ 'transport': 'gmail_api'
48
+ }
42
49
  },
43
50
  # STRATEGIC NOTE (COB Configuration): COB library is fully implemented and ready
44
51
  # To enable COB functionality, add the following configuration:
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.251017.1"
30
+ __version__ = "0.251026.0"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
MediCafe/api_core.py CHANGED
@@ -1212,14 +1212,14 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
1212
1212
  corporate_tax_owner_id=None, corporate_tax_owner_name=None, organization_name=None,
1213
1213
  organization_id=None, identify_service_level_deductible=True):
1214
1214
  """
1215
- GraphQL Super Connector version of eligibility check that maps to the same interface as get_eligibility_v3.
1216
- This function provides a drop-in replacement for the REST API with identical input/output behavior.
1215
+ OPTUMAI eligibility (GraphQL) that maps to the same interface as get_eligibility_v3.
1216
+ This function does not perform legacy fallback; callers should invoke legacy v3 separately if desired.
1217
1217
  """
1218
1218
  # Ensure all required parameters have values
1219
1219
  if not all([client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi]):
1220
1220
  raise ValueError("All required parameters must have values: client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi")
1221
1221
 
1222
- # Prefer OPTUMAI endpoint if configured, otherwise fall back to legacy UHCAPI super connector
1222
+ # Prefer OPTUMAI endpoint if configured, otherwise fall back to legacy UHCAPI v3 (REST)
1223
1223
  try:
1224
1224
  endpoints_cfg = client.config['MediLink_Config']['endpoints']
1225
1225
  except Exception:
@@ -1240,12 +1240,8 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
1240
1240
  pass
1241
1241
 
1242
1242
  if not endpoint_name:
1243
- # Fallback to legacy UHCAPI super connector path for backward compatibility
1244
- endpoint_name = 'UHCAPI'
1245
- try:
1246
- url_extension = endpoints_cfg.get(endpoint_name, {}).get('additional_endpoints', {}).get('eligibility_super_connector')
1247
- except Exception:
1248
- url_extension = None
1243
+ # No fallback from this function; surface configuration error
1244
+ raise ValueError("OPTUMAI eligibility endpoint not configured")
1249
1245
 
1250
1246
  # Validate payer_id against the selected endpoint's list
1251
1247
  # - If OPTUMAI is used, allow the augmented list (includes LIFE1, WELM2, etc.).
@@ -1258,17 +1254,17 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
1258
1254
  if not url_extension:
1259
1255
  raise ValueError("Eligibility endpoint not configured for {}".format(endpoint_name))
1260
1256
 
1261
- # Debug/trace: indicate which endpoint/path is being used for the Super Connector call
1257
+ # Debug/trace: indicate that OPTUMAI eligibility path is active
1262
1258
  try:
1263
1259
  MediLink_ConfigLoader.log(
1264
- "Super Connector eligibility using endpoint '{}' with path '{}'".format(endpoint_name, url_extension),
1260
+ "Eligibility using OPTUMAI endpoint with path '{}'".format(url_extension),
1265
1261
  level="INFO",
1266
1262
  console_output=CONSOLE_LOGGING
1267
1263
  )
1268
1264
  except Exception:
1269
1265
  pass
1270
1266
  try:
1271
- print("[Eligibility] Using endpoint: {} (path: {})".format(endpoint_name, url_extension))
1267
+ print("[Eligibility] Using OPTUMAI (path: {})".format(url_extension))
1272
1268
  except Exception:
1273
1269
  pass
1274
1270
 
@@ -1405,36 +1401,8 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
1405
1401
  except Exception:
1406
1402
  pass
1407
1403
 
1408
- # Progressive hardening: allow disabling fallback to UHC via config
1409
- try:
1410
- disable_fallback = False
1411
- try:
1412
- disable_fallback = bool(medi.get('optumai_disable_fallback', False))
1413
- except Exception:
1414
- disable_fallback = False
1415
- if not disable_fallback and endpoint_name == 'OPTUMAI':
1416
- fallback_url = endpoints_cfg.get('UHCAPI', {}).get('additional_endpoints', {}).get('eligibility_super_connector')
1417
- if fallback_url:
1418
- try:
1419
- MediLink_ConfigLoader.log(
1420
- "OPTUMAI call failed. Falling back to UHCAPI path '{}'".format(fallback_url),
1421
- level="WARNING",
1422
- console_output=CONSOLE_LOGGING
1423
- )
1424
- except Exception:
1425
- pass
1426
- try:
1427
- print("[Eligibility] Fallback to UHCAPI (path: {})".format(fallback_url))
1428
- except Exception:
1429
- pass
1430
- response = client.make_api_call('UHCAPI', 'POST', fallback_url, params=None, data=graphql_body, headers=headers)
1431
- else:
1432
- raise
1433
- else:
1434
- raise
1435
- except Exception:
1436
- # Propagate original error if fallback not possible or also fails
1437
- raise
1404
+ # No fallback from this function; re-raise so callers can handle or try legacy explicitly
1405
+ raise
1438
1406
 
1439
1407
  # Transform GraphQL response to match REST API format
1440
1408
  # This ensures the calling code doesn't know the difference
@@ -1446,7 +1414,7 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
1446
1414
  if sc_status and sc_status != '200':
1447
1415
  msg = transformed_response.get('message')
1448
1416
  MediLink_ConfigLoader.log(
1449
- "Super Connector transformed response status: {} msg: {}".format(sc_status, msg),
1417
+ "OPTUMAI eligibility transformed response status: {} msg: {}".format(sc_status, msg),
1450
1418
  level="INFO",
1451
1419
  console_output=CONSOLE_LOGGING
1452
1420
  )
@@ -1464,6 +1432,22 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
1464
1432
  print("[Eligibility] GraphQL error code={} desc={}".format(code, desc))
1465
1433
  except Exception:
1466
1434
  pass
1435
+
1436
+ # Terminal self-help hints for auth/authorization cases
1437
+ # Non-throwing hint emitter (kept outside core logic path)
1438
+ def _emit_hint_for_status(status_str):
1439
+ try:
1440
+ if status_str == '401':
1441
+ print("[Eligibility] Hint: Authentication failed. Verify client credentials/token and endpoint config.")
1442
+ elif status_str == '403':
1443
+ print("[Eligibility] Hint: Access denied. Verify providerTaxId/TIN and account permissions/roles for endpoint.")
1444
+ except Exception:
1445
+ pass
1446
+
1447
+ try:
1448
+ _emit_hint_for_status(str(sc_status))
1449
+ except Exception:
1450
+ pass
1467
1451
  except Exception:
1468
1452
  pass
1469
1453
 
@@ -1,12 +1,20 @@
1
- import os, sys, time, json, zipfile, hashlib, platform
1
+ import base64
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import platform
6
+ import sys
7
+ import time
8
+ import zipfile
2
9
 
3
- try:
4
- import requests
5
- except Exception:
6
- requests = None
10
+ import requests
7
11
 
8
- from MediCafe.MediLink_ConfigLoader import load_configuration, log as mc_log
12
+ from email.mime.application import MIMEApplication
13
+ from email.mime.multipart import MIMEMultipart
14
+ from email.mime.text import MIMEText
9
15
 
16
+ from MediCafe.MediLink_ConfigLoader import load_configuration, log as mc_log
17
+ from MediLink.MediLink_Gmail import get_access_token
10
18
 
11
19
  ASCII_SAFE_REPLACEMENTS = [
12
20
  ('"', '"'),
@@ -95,7 +103,7 @@ def _compute_report_id(zip_path):
95
103
  return 'mc-{}-{}'.format(int(time.time()), '000000000000')
96
104
 
97
105
 
98
- def collect_support_bundle(include_traceback=True, max_log_lines=2000):
106
+ def collect_support_bundle(include_traceback=True, max_log_lines=500):
99
107
  config, _ = load_configuration()
100
108
  medi = config.get('MediLink_Config', {})
101
109
  local_storage_path = medi.get('local_storage_path', '.')
@@ -140,12 +148,29 @@ def collect_support_bundle(include_traceback=True, max_log_lines=2000):
140
148
  z.writestr('log_tail.txt', log_tail)
141
149
  if traceback_txt:
142
150
  z.writestr('traceback.txt', traceback_txt)
143
- z.writestr('README.txt', _readme_text())
144
- return zip_path
151
+ # Get latest WinSCP log
152
+ upload_log = os.path.join(local_storage_path, 'winscp_upload.log')
153
+ download_log = os.path.join(local_storage_path, 'winscp_download.log')
154
+ winscp_logs = [(p, os.path.getmtime(p)) for p in [upload_log, download_log] if os.path.exists(p)]
155
+ if winscp_logs:
156
+ latest_winscp = max(winscp_logs, key=lambda x: x[1])[0]
157
+ winscp_tail = _tail_file(latest_winscp, max_log_lines) if latest_winscp else ''
158
+ winscp_tail = _redact(winscp_tail)
159
+ else:
160
+ winscp_tail = ''
161
+ if winscp_tail:
162
+ z.writestr('winscp_log_tail.txt', winscp_tail)
145
163
  except Exception as e:
146
164
  mc_log('Error creating support bundle: {}'.format(e), level='ERROR')
147
165
  return None
148
166
 
167
+ bundle_size = os.path.getsize(zip_path)
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
172
+ return zip_path
173
+
149
174
 
150
175
  def _first_line(text):
151
176
  try:
@@ -158,14 +183,6 @@ def _first_line(text):
158
183
  return ''
159
184
 
160
185
 
161
- def _readme_text():
162
- return (
163
- "MediCafe Support Bundle\n\n"
164
- "This archive contains a redacted log tail, optional traceback, and metadata.\n"
165
- "You may submit this bundle automatically from the app or send it manually to support.\n"
166
- )
167
-
168
-
169
186
  def _get_version():
170
187
  try:
171
188
  from MediCafe import __version__
@@ -174,102 +191,6 @@ def _get_version():
174
191
  return 'unknown'
175
192
 
176
193
 
177
- def submit_support_bundle(zip_path):
178
- config, _ = load_configuration()
179
- medi = config.get('MediLink_Config', {})
180
- rep = medi.get('error_reporting', {}) if isinstance(medi, dict) else {}
181
- endpoint_url = _safe_ascii(rep.get('endpoint_url', ''))
182
- auth_token = _safe_ascii(rep.get('auth_token', ''))
183
- insecure = bool(rep.get('insecure_http', False))
184
- max_bytes = int(rep.get('max_bundle_bytes', 2097152))
185
-
186
- if not requests:
187
- print("[ERROR] requests module not available; cannot submit report.")
188
- return False
189
- if not endpoint_url:
190
- print("[ERROR] error_reporting.endpoint_url not configured.")
191
- return False
192
- if not os.path.exists(zip_path):
193
- print("[ERROR] Bundle not found: {}".format(zip_path))
194
- return False
195
- try:
196
- size = os.path.getsize(zip_path)
197
- if size > max_bytes:
198
- print("[INFO] Bundle size {} exceeds cap {}; rebuilding smaller not implemented here.".format(size, max_bytes))
199
- except Exception:
200
- pass
201
-
202
- report_id = _compute_report_id(zip_path)
203
- headers = {
204
- 'X-Auth-Token': auth_token or '',
205
- 'X-Report-Id': report_id,
206
- 'User-Agent': 'MediCafe-Reporter/1.0'
207
- }
208
-
209
- # Prepare meta.json stream derived from inside the zip for server convenience
210
- meta_json = '{}'
211
- try:
212
- with zipfile.ZipFile(zip_path, 'r') as z:
213
- if 'meta.json' in z.namelist():
214
- meta_json = z.read('meta.json')
215
- except Exception:
216
- meta_json = '{}'
217
-
218
- try:
219
- bundle_fh = open(zip_path, 'rb')
220
- files = {
221
- 'meta': ('meta.json', meta_json, 'application/json'),
222
- 'bundle': (os.path.basename(zip_path), bundle_fh, 'application/zip')
223
- }
224
- r = requests.post(endpoint_url, headers=headers, files=files, timeout=(10, 20), verify=(not insecure))
225
- code = getattr(r, 'status_code', None)
226
- if code == 200:
227
- print("[SUCCESS] Report submitted. ID: {}".format(report_id))
228
- return True
229
- elif code == 401:
230
- print("[ERROR] Unauthorized (401). Check error_reporting.auth_token.")
231
- return False
232
- elif code == 413:
233
- print("[ERROR] Too large (413). Consider reducing max log lines.")
234
- return False
235
- else:
236
- print("[ERROR] Submission failed with status {}".format(code))
237
- return False
238
- except Exception as e:
239
- print("[ERROR] Submission exception: {}".format(e))
240
- return False
241
- finally:
242
- try:
243
- bundle_fh.close()
244
- except Exception:
245
- pass
246
-
247
-
248
- def flush_queued_reports():
249
- config, _ = load_configuration()
250
- medi = config.get('MediLink_Config', {})
251
- local_storage_path = medi.get('local_storage_path', '.')
252
- queue_dir = os.path.join(local_storage_path, 'reports_queue')
253
- if not os.path.isdir(queue_dir):
254
- return 0, 0
255
- count_ok = 0
256
- count_total = 0
257
- for name in sorted(os.listdir(queue_dir)):
258
- if not name.endswith('.zip'):
259
- continue
260
- zip_path = os.path.join(queue_dir, name)
261
- count_total += 1
262
- print("Attempting upload of queued report: {}".format(name))
263
- ok = submit_support_bundle(zip_path)
264
- if ok:
265
- try:
266
- os.remove(zip_path)
267
- except Exception:
268
- pass
269
- count_ok += 1
270
- return count_ok, count_total
271
-
272
-
273
194
  def capture_unhandled_traceback(exc_type, exc_value, exc_traceback):
274
195
  try:
275
196
  config, _ = load_configuration()
@@ -285,3 +206,59 @@ def capture_unhandled_traceback(exc_type, exc_value, exc_traceback):
285
206
  except Exception:
286
207
  pass
287
208
 
209
+ def submit_support_bundle_email(zip_path=None, include_traceback=True):
210
+ if not zip_path:
211
+ zip_path = collect_support_bundle(include_traceback)
212
+ if not zip_path:
213
+ mc_log("Failed to create bundle.", level="ERROR")
214
+ return False
215
+ 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
+ config, _ = load_configuration()
221
+ email_config = config.get('MediLink_Config', {}).get('error_reporting', {}).get('email', {})
222
+ if not email_config.get('enabled', False):
223
+ mc_log("Email reporting disabled.", level="INFO")
224
+ return False
225
+ to_emails = email_config.get('to', [])
226
+ subject_prefix = email_config.get('subject_prefix', 'MediCafe Error Report')
227
+ access_token = get_access_token()
228
+ if not access_token:
229
+ mc_log("No access token - authenticate first.", level="ERROR")
230
+ return False
231
+ mc_log("Building email...", level="INFO")
232
+ msg = MIMEMultipart()
233
+ msg['To'] = ', '.join(to_emails)
234
+ msg['Subject'] = '{} - {}'.format(subject_prefix, time.strftime('%Y%m%d_%H%M%S'))
235
+ with open(zip_path, 'rb') as f:
236
+ attach = MIMEApplication(f.read(), _subtype='zip')
237
+ attach.add_header('Content-Disposition', 'attachment', filename=os.path.basename(zip_path))
238
+ msg.attach(attach)
239
+ body = "Error report attached."
240
+ msg.attach(MIMEText(body, 'plain'))
241
+ raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
242
+ mc_log("Sending report...", level="INFO")
243
+ headers = {'Authorization': 'Bearer {}'.format(access_token), 'Content-Type': 'application/json'}
244
+ data = {'raw': raw}
245
+ resp = requests.post('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', headers=headers, json=data)
246
+ if resp.status_code == 200:
247
+ mc_log("Report sent successfully!", level="INFO")
248
+ os.remove(zip_path)
249
+ return True
250
+ else:
251
+ mc_log("Failed to send: {} - {}".format(resp.status_code, resp.text), level="ERROR")
252
+ return False
253
+
254
+ def email_error_report_flow():
255
+ try:
256
+ sent = submit_support_bundle_email(zip_path=None, include_traceback=True)
257
+ return 0 if sent else 1
258
+ except Exception as e:
259
+ mc_log("[ERROR] Exception during email report flow: {0}".format(e), level="ERROR")
260
+ return 1
261
+
262
+ if __name__ == "__main__":
263
+ raise SystemExit(email_error_report_flow())
264
+
MediCafe/graphql_utils.py CHANGED
@@ -268,6 +268,36 @@ class GraphQLQueryBuilder:
268
268
  }
269
269
  }
270
270
 
271
+ def _map_graphql_error_to_status_message(error):
272
+ """Map provider GraphQL error to (statuscode, message) using simple heuristics.
273
+ Python 3.4-compatible; avoids dependencies and centralizes logic.
274
+ """
275
+ try:
276
+ if not isinstance(error, dict):
277
+ return '500', 'GraphQL error: unknown format'
278
+ code = ((error.get('extensions', {}) or {}).get('code') or
279
+ error.get('code') or 'GRAPHQL_ERROR')
280
+ msg = (error.get('message') or error.get('description') or 'GraphQL error')
281
+ code_upper = str(code).upper()
282
+ msg_lower = str(msg).lower()
283
+
284
+ # 401 auth failures
285
+ if (code == 'UNAUTHORIZED_AUTHENTICATION_FAILED' or
286
+ ('UNAUTH' in code_upper) or
287
+ ('AUTHENTICATION' in code_upper)):
288
+ return '401', 'Authentication failed: {}'.format(msg)
289
+
290
+ # 403 authorization/access issues
291
+ if ('FORBIDDEN' in code_upper) or ('AUTHORIZATION' in code_upper) or ('ACCESS_DENIED' in code_upper) or \
292
+ ('forbidden' in msg_lower) or ('permission' in msg_lower):
293
+ return '403', 'Authorization failed: {}'.format(msg)
294
+
295
+ # Default
296
+ return '500', '{}: {}'.format(code, msg)
297
+ except Exception:
298
+ return '500', 'GraphQL error: unknown'
299
+
300
+
271
301
  class GraphQLResponseTransformer:
272
302
  """Transforms GraphQL responses to match REST API format"""
273
303
 
@@ -284,24 +314,15 @@ class GraphQLResponseTransformer:
284
314
  Transformed response matching REST API format
285
315
  """
286
316
  try:
287
- # Check for authentication errors first
317
+ # Check for authentication/authorization errors first
288
318
  if 'errors' in graphql_response:
289
319
  error = graphql_response['errors'][0]
290
- error_code = error.get('extensions', {}).get('code', 'UNKNOWN_ERROR')
291
- error_message = error.get('message', 'Unknown error')
292
-
293
- if error_code == 'UNAUTHORIZED_AUTHENTICATION_FAILED':
294
- return {
295
- 'statuscode': '401',
296
- 'message': 'Authentication failed: {}'.format(error_message),
297
- 'rawGraphQLResponse': graphql_response
298
- }
299
- else:
300
- return {
301
- 'statuscode': '500',
302
- 'message': 'GraphQL error: {} - {}'.format(error_code, error_message),
303
- 'rawGraphQLResponse': graphql_response
304
- }
320
+ statuscode, mapped_msg = _map_graphql_error_to_status_message(error)
321
+ return {
322
+ 'statuscode': statuscode,
323
+ 'message': mapped_msg,
324
+ 'rawGraphQLResponse': graphql_response
325
+ }
305
326
 
306
327
  # Check if GraphQL response has data
307
328
  if 'data' not in graphql_response:
@@ -544,12 +565,10 @@ class GraphQLResponseTransformer:
544
565
  # Handle GraphQL errors
545
566
  if isinstance(graphql_response, dict) and 'errors' in graphql_response:
546
567
  first_err = graphql_response['errors'][0] if graphql_response['errors'] else {}
547
- code = (first_err.get('extensions', {}) or {}).get('code') or first_err.get('code') or 'GRAPHQL_ERROR'
548
- msg = first_err.get('message') or first_err.get('description') or 'GraphQL error'
549
- status = '401' if 'AUTH' in str(code).upper() else '500'
568
+ statuscode, mapped_msg = _map_graphql_error_to_status_message(first_err)
550
569
  return {
551
- 'statuscode': status,
552
- 'message': '{}: {}'.format(code, msg),
570
+ 'statuscode': statuscode,
571
+ 'message': mapped_msg,
553
572
  'rawGraphQLResponse': graphql_response
554
573
  }
555
574