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/MediBot.bat +867 -835
- MediBot/MediBot.py +77 -11
- MediBot/MediBot_Preprocessor_lib.py +1895 -1821
- MediBot/__init__.py +1 -1
- MediCafe/MediLink_ConfigLoader.py +8 -1
- MediCafe/__init__.py +1 -1
- MediCafe/api_core.py +27 -43
- MediCafe/error_reporter.py +90 -113
- MediCafe/graphql_utils.py +40 -21
- MediLink/MediLink_Deductible.py +45 -23
- MediLink/MediLink_Gmail.py +1 -0
- MediLink/MediLink_Up.py +31 -25
- MediLink/MediLink_main.py +65 -58
- MediLink/MediLink_smart_import.py +3 -2
- MediLink/__init__.py +1 -1
- {medicafe-0.251017.1.dist-info → medicafe-0.251026.0.dist-info}/METADATA +1 -1
- {medicafe-0.251017.1.dist-info → medicafe-0.251026.0.dist-info}/RECORD +21 -21
- {medicafe-0.251017.1.dist-info → medicafe-0.251026.0.dist-info}/LICENSE +0 -0
- {medicafe-0.251017.1.dist-info → medicafe-0.251026.0.dist-info}/WHEEL +0 -0
- {medicafe-0.251017.1.dist-info → medicafe-0.251026.0.dist-info}/entry_points.txt +0 -0
- {medicafe-0.251017.1.dist-info → medicafe-0.251026.0.dist-info}/top_level.txt +0 -0
MediBot/__init__.py
CHANGED
|
@@ -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
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
|
-
|
|
1216
|
-
This function
|
|
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
|
|
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
|
-
#
|
|
1244
|
-
|
|
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
|
|
1257
|
+
# Debug/trace: indicate that OPTUMAI eligibility path is active
|
|
1262
1258
|
try:
|
|
1263
1259
|
MediLink_ConfigLoader.log(
|
|
1264
|
-
"
|
|
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
|
|
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
|
-
#
|
|
1409
|
-
|
|
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
|
-
"
|
|
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
|
|
MediCafe/error_reporter.py
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
4
|
-
import requests
|
|
5
|
-
except Exception:
|
|
6
|
-
requests = None
|
|
10
|
+
import requests
|
|
7
11
|
|
|
8
|
-
from
|
|
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=
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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':
|
|
552
|
-
'message':
|
|
570
|
+
'statuscode': statuscode,
|
|
571
|
+
'message': mapped_msg,
|
|
553
572
|
'rawGraphQLResponse': graphql_response
|
|
554
573
|
}
|
|
555
574
|
|