medicafe 0.250728.7__tar.gz → 0.250728.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink.py +48 -6
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_837p_encoder_library.py +15 -1
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_API_v3.py +62 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Up.py +6 -1
- medicafe-0.250728.8/MediLink/MediLink_api_utils.py +305 -0
- medicafe-0.250728.8/MediLink/MediLink_insurance_utils.py +265 -0
- medicafe-0.250728.8/MediLink/insurance_type_integration_test.py +368 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/PKG-INFO +1 -1
- {medicafe-0.250728.7 → medicafe-0.250728.8}/medicafe.egg-info/PKG-INFO +1 -1
- {medicafe-0.250728.7 → medicafe-0.250728.8}/medicafe.egg-info/SOURCES.txt +3 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/setup.py +1 -1
- {medicafe-0.250728.7 → medicafe-0.250728.8}/LICENSE +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MANIFEST.in +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot.bat +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot_Crosswalk_Library.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot_Preprocessor_lib.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot_UI.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/PDF_to_CSV_Cleaner.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/__init__.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/update_json.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediBot/update_medicafe.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_837p_encoder.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_API_v2.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_APIs.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_ClaimStatus.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_ConfigLoader.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_DataMgmt.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Deductible.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Deductible_Validator.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Gmail.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_GraphQL.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/MediLink_UI.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/__init__.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/openssl.cnf +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/test.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/test_cob_library.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/test_validation.py +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/MediLink/webapp.html +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/README.md +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.250728.7 → medicafe-0.250728.8}/setup.cfg +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# MediLink.py
|
|
2
|
-
import os, sys
|
|
2
|
+
import os, sys, time
|
|
3
3
|
import MediLink_Down
|
|
4
4
|
import MediLink_Up
|
|
5
5
|
import MediLink_ConfigLoader
|
|
@@ -7,7 +7,12 @@ import MediLink_DataMgmt
|
|
|
7
7
|
|
|
8
8
|
# For UI Functions
|
|
9
9
|
import MediLink_UI # Import UI module for handling all user interfaces
|
|
10
|
-
|
|
10
|
+
try:
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
except ImportError:
|
|
13
|
+
# Fallback for when tqdm is not available
|
|
14
|
+
def tqdm(iterable, **kwargs):
|
|
15
|
+
return iterable
|
|
11
16
|
|
|
12
17
|
# Add parent directory of the project to the Python path
|
|
13
18
|
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
@@ -49,6 +54,7 @@ def collect_detailed_patient_data(selected_files, config, crosswalk):
|
|
|
49
54
|
def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_mapping=None):
|
|
50
55
|
"""
|
|
51
56
|
Enriches the detailed patient data with insurance type based on patient ID.
|
|
57
|
+
Enhanced with optional API integration and comprehensive logging.
|
|
52
58
|
|
|
53
59
|
Parameters:
|
|
54
60
|
- detailed_patient_data: List of dictionaries containing detailed patient data.
|
|
@@ -64,14 +70,50 @@ def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_map
|
|
|
64
70
|
UHC and which ones are not yet supported so they know which ones they need to edit. It is possible that we may want to isolate the
|
|
65
71
|
patient data that is already pulled from UHC so that the user can see which ones are already using the enriched data.
|
|
66
72
|
"""
|
|
73
|
+
# Import enhancement utilities
|
|
74
|
+
try:
|
|
75
|
+
from MediLink_insurance_utils import (
|
|
76
|
+
get_feature_flag,
|
|
77
|
+
enrich_patient_data_with_metadata,
|
|
78
|
+
generate_insurance_assignment_summary,
|
|
79
|
+
validate_insurance_type_from_config
|
|
80
|
+
)
|
|
81
|
+
enhanced_mode = get_feature_flag('enhanced_insurance_enrichment', default=False)
|
|
82
|
+
except ImportError:
|
|
83
|
+
MediLink_ConfigLoader.log("Insurance utils not available, using legacy mode", level="DEBUG")
|
|
84
|
+
enhanced_mode = False
|
|
85
|
+
|
|
67
86
|
if patient_insurance_type_mapping is None:
|
|
68
87
|
MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.", level="WARNING")
|
|
69
88
|
patient_insurance_type_mapping = {}
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
# Enhanced mode with metadata and validation
|
|
91
|
+
if enhanced_mode:
|
|
92
|
+
MediLink_ConfigLoader.log("Using enhanced insurance type enrichment", level="INFO")
|
|
93
|
+
|
|
94
|
+
for data in detailed_patient_data:
|
|
95
|
+
patient_id = data.get('PATID') # I think this is the right name?
|
|
96
|
+
|
|
97
|
+
# Get insurance type with validation
|
|
98
|
+
raw_insurance_type = patient_insurance_type_mapping.get(patient_id, '12') # Default to '12' (PPO)
|
|
99
|
+
validated_insurance_type = validate_insurance_type_from_config(raw_insurance_type, patient_id)
|
|
100
|
+
|
|
101
|
+
# Add enhanced metadata
|
|
102
|
+
data['insurance_type'] = validated_insurance_type
|
|
103
|
+
data['insurance_type_source'] = 'MANUAL' if patient_id in patient_insurance_type_mapping else 'DEFAULT'
|
|
104
|
+
data['insurance_type_validated'] = True
|
|
105
|
+
data['insurance_type_timestamp'] = time.time()
|
|
106
|
+
|
|
107
|
+
# Generate summary statistics
|
|
108
|
+
generate_insurance_assignment_summary(detailed_patient_data)
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
# Legacy mode (preserve existing behavior exactly)
|
|
112
|
+
for data in detailed_patient_data:
|
|
113
|
+
patient_id = data.get('PATID') # I think this is the right name?
|
|
114
|
+
insurance_type = patient_insurance_type_mapping.get(patient_id, '12') # Default to '12' (PPO)
|
|
115
|
+
data['insurance_type'] = insurance_type
|
|
116
|
+
|
|
75
117
|
return detailed_patient_data
|
|
76
118
|
|
|
77
119
|
def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
@@ -572,7 +572,8 @@ def create_sbr_segment(config, parsed_data, endpoint):
|
|
|
572
572
|
|
|
573
573
|
def insurance_type_selection(parsed_data):
|
|
574
574
|
"""
|
|
575
|
-
|
|
575
|
+
Enhanced insurance type selection with optional API integration and safe fallbacks.
|
|
576
|
+
Maintains exact same signature as existing implementation.
|
|
576
577
|
|
|
577
578
|
TODO (HIGH SBR09) Finish making this function.
|
|
578
579
|
This should eventually integrate into a menu upstream. This menu flow probably needs to be alongside the suggested endpoint flow.
|
|
@@ -602,6 +603,19 @@ def insurance_type_selection(parsed_data):
|
|
|
602
603
|
"""
|
|
603
604
|
MediLink_ConfigLoader.log("insurance_type_selection(parsed_data): {}".format(parsed_data), level="DEBUG")
|
|
604
605
|
|
|
606
|
+
# Try enhanced selection with safe fallback
|
|
607
|
+
try:
|
|
608
|
+
from MediLink_insurance_utils import safe_insurance_type_selection
|
|
609
|
+
return safe_insurance_type_selection(parsed_data, _original_insurance_type_selection_logic)
|
|
610
|
+
except ImportError:
|
|
611
|
+
MediLink_ConfigLoader.log("Enhanced insurance selection not available, using original logic", level="DEBUG")
|
|
612
|
+
return _original_insurance_type_selection_logic(parsed_data)
|
|
613
|
+
|
|
614
|
+
def _original_insurance_type_selection_logic(parsed_data):
|
|
615
|
+
"""
|
|
616
|
+
Original insurance type selection logic extracted to preserve exact behavior.
|
|
617
|
+
This ensures backward compatibility when enhanced features are not available.
|
|
618
|
+
"""
|
|
605
619
|
# Check if insurance type is already assigned and is valid
|
|
606
620
|
insurance_type_code = parsed_data.get('insurance_type')
|
|
607
621
|
if insurance_type_code and len(insurance_type_code) == 2 and insurance_type_code.isalnum():
|
|
@@ -112,6 +112,29 @@ class APIClient(BaseAPIClient):
|
|
|
112
112
|
def __init__(self):
|
|
113
113
|
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
114
114
|
super().__init__(config)
|
|
115
|
+
|
|
116
|
+
# Add enhanced features if available
|
|
117
|
+
try:
|
|
118
|
+
from MediLink_api_utils import APICircuitBreaker, APICache, APIRateLimiter
|
|
119
|
+
from MediLink_insurance_utils import get_feature_flag
|
|
120
|
+
|
|
121
|
+
# Initialize enhancements if enabled
|
|
122
|
+
enable_circuit_breaker = get_feature_flag('api_circuit_breaker', default=False)
|
|
123
|
+
enable_caching = get_feature_flag('api_caching', default=False)
|
|
124
|
+
enable_rate_limiting = get_feature_flag('api_rate_limiting', default=False)
|
|
125
|
+
|
|
126
|
+
self.circuit_breaker = APICircuitBreaker() if enable_circuit_breaker else None
|
|
127
|
+
self.api_cache = APICache() if enable_caching else None
|
|
128
|
+
self.rate_limiter = APIRateLimiter() if enable_rate_limiting else None
|
|
129
|
+
|
|
130
|
+
if any([enable_circuit_breaker, enable_caching, enable_rate_limiting]):
|
|
131
|
+
MediLink_ConfigLoader.log("Enhanced API client initialized with circuit_breaker={}, caching={}, rate_limiting={}".format(
|
|
132
|
+
enable_circuit_breaker, enable_caching, enable_rate_limiting), level="INFO")
|
|
133
|
+
except ImportError:
|
|
134
|
+
MediLink_ConfigLoader.log("API enhancements not available, using standard client", level="DEBUG")
|
|
135
|
+
self.circuit_breaker = None
|
|
136
|
+
self.api_cache = None
|
|
137
|
+
self.rate_limiter = None
|
|
115
138
|
|
|
116
139
|
def get_access_token(self, endpoint_name):
|
|
117
140
|
MediLink_ConfigLoader.log("[Get Access Token] Called for {}".format(endpoint_name), level="DEBUG")
|
|
@@ -177,6 +200,45 @@ class APIClient(BaseAPIClient):
|
|
|
177
200
|
return None
|
|
178
201
|
|
|
179
202
|
def make_api_call(self, endpoint_name, call_type, url_extension="", params=None, data=None, headers=None):
|
|
203
|
+
# Try enhanced API call if available
|
|
204
|
+
if hasattr(self, 'circuit_breaker') and self.circuit_breaker:
|
|
205
|
+
try:
|
|
206
|
+
return self._make_enhanced_api_call(endpoint_name, call_type, url_extension, params, data, headers)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
MediLink_ConfigLoader.log("Enhanced API call failed, falling back to standard: {}".format(str(e)), level="WARNING")
|
|
209
|
+
|
|
210
|
+
# Standard API call logic
|
|
211
|
+
return self._make_standard_api_call(endpoint_name, call_type, url_extension, params, data, headers)
|
|
212
|
+
|
|
213
|
+
def _make_enhanced_api_call(self, endpoint_name, call_type, url_extension="", params=None, data=None, headers=None):
|
|
214
|
+
"""Enhanced API call with circuit breaker, caching, and rate limiting"""
|
|
215
|
+
# Check cache first (for GET requests)
|
|
216
|
+
if self.api_cache and call_type == 'GET':
|
|
217
|
+
cached_result = self.api_cache.get(endpoint_name, call_type, url_extension, params)
|
|
218
|
+
if cached_result is not None:
|
|
219
|
+
MediLink_ConfigLoader.log("Cache hit for {} {} {}".format(call_type, endpoint_name, url_extension), level="DEBUG")
|
|
220
|
+
return cached_result
|
|
221
|
+
|
|
222
|
+
# Check rate limits
|
|
223
|
+
if self.rate_limiter:
|
|
224
|
+
self.rate_limiter.wait_if_needed()
|
|
225
|
+
|
|
226
|
+
# Make call with circuit breaker protection
|
|
227
|
+
result = self.circuit_breaker.call_with_breaker(
|
|
228
|
+
self._make_standard_api_call, endpoint_name, call_type, url_extension, params, data, headers)
|
|
229
|
+
|
|
230
|
+
# Record rate limit call
|
|
231
|
+
if self.rate_limiter:
|
|
232
|
+
self.rate_limiter.record_call()
|
|
233
|
+
|
|
234
|
+
# Cache result (for GET requests)
|
|
235
|
+
if self.api_cache and call_type == 'GET':
|
|
236
|
+
self.api_cache.set(result, endpoint_name, call_type, url_extension, params)
|
|
237
|
+
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
def _make_standard_api_call(self, endpoint_name, call_type, url_extension="", params=None, data=None, headers=None):
|
|
241
|
+
"""Standard API call logic preserved for compatibility"""
|
|
180
242
|
token = self.get_access_token(endpoint_name)
|
|
181
243
|
if token:
|
|
182
244
|
MediLink_ConfigLoader.log("[Make API Call] Token found for {}".format(endpoint_name), level="DEBUG")
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# MediLink_Up.py
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
import os, re, subprocess, traceback
|
|
4
|
-
|
|
4
|
+
try:
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
except ImportError:
|
|
7
|
+
# Fallback for when tqdm is not available
|
|
8
|
+
def tqdm(iterable, **kwargs):
|
|
9
|
+
return iterable
|
|
5
10
|
import MediLink_837p_encoder
|
|
6
11
|
from MediLink_ConfigLoader import log, load_configuration
|
|
7
12
|
from MediLink_DataMgmt import operate_winscp
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# MediLink_api_utils.py
|
|
2
|
+
# Enhanced API utilities for circuit breaker, caching, and rate limiting
|
|
3
|
+
# Extracted from enhanced implementations for safe production integration
|
|
4
|
+
# Python 3.4.4 compatible implementation
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from MediLink import MediLink_ConfigLoader
|
|
10
|
+
except ImportError:
|
|
11
|
+
import MediLink_ConfigLoader
|
|
12
|
+
|
|
13
|
+
# Circuit breaker pattern for API resilience
|
|
14
|
+
class APICircuitBreaker:
|
|
15
|
+
"""Circuit breaker pattern for API resilience"""
|
|
16
|
+
def __init__(self, failure_threshold=5, timeout=300):
|
|
17
|
+
self.failure_count = 0
|
|
18
|
+
self.failure_threshold = failure_threshold
|
|
19
|
+
self.timeout = timeout
|
|
20
|
+
self.last_failure_time = None
|
|
21
|
+
self.state = 'CLOSED' # CLOSED, OPEN, HALF_OPEN
|
|
22
|
+
|
|
23
|
+
def can_execute(self):
|
|
24
|
+
"""Check if circuit breaker allows execution"""
|
|
25
|
+
if self.state == 'OPEN':
|
|
26
|
+
if time.time() - self.last_failure_time > self.timeout:
|
|
27
|
+
self.state = 'HALF_OPEN'
|
|
28
|
+
MediLink_ConfigLoader.log("Circuit breaker moving to HALF_OPEN state", level="INFO")
|
|
29
|
+
return True
|
|
30
|
+
else:
|
|
31
|
+
return False
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
def record_success(self):
|
|
35
|
+
"""Record successful API call"""
|
|
36
|
+
if self.state == 'HALF_OPEN':
|
|
37
|
+
self.state = 'CLOSED'
|
|
38
|
+
self.failure_count = 0
|
|
39
|
+
MediLink_ConfigLoader.log("Circuit breaker reset to CLOSED state", level="INFO")
|
|
40
|
+
|
|
41
|
+
def record_failure(self):
|
|
42
|
+
"""Record failed API call"""
|
|
43
|
+
self.failure_count += 1
|
|
44
|
+
self.last_failure_time = time.time()
|
|
45
|
+
if self.failure_count >= self.failure_threshold:
|
|
46
|
+
self.state = 'OPEN'
|
|
47
|
+
MediLink_ConfigLoader.log("Circuit breaker OPENED due to {} failures".format(self.failure_count), level="ERROR")
|
|
48
|
+
|
|
49
|
+
def call_with_breaker(self, api_function, *args, **kwargs):
|
|
50
|
+
"""Call API function with circuit breaker protection"""
|
|
51
|
+
if not self.can_execute():
|
|
52
|
+
raise Exception("API circuit breaker is OPEN - too many failures")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
result = api_function(*args, **kwargs)
|
|
56
|
+
self.record_success()
|
|
57
|
+
return result
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self.record_failure()
|
|
60
|
+
raise e
|
|
61
|
+
|
|
62
|
+
# Cache for API responses to reduce redundant calls
|
|
63
|
+
class APICache:
|
|
64
|
+
"""Simple time-based cache for API responses"""
|
|
65
|
+
def __init__(self, cache_duration=3600): # 1 hour cache
|
|
66
|
+
self.cache = {}
|
|
67
|
+
self.cache_duration = cache_duration
|
|
68
|
+
|
|
69
|
+
def _generate_cache_key(self, *args, **kwargs):
|
|
70
|
+
"""Generate cache key from function arguments"""
|
|
71
|
+
key_parts = []
|
|
72
|
+
for arg in args:
|
|
73
|
+
key_parts.append(str(arg))
|
|
74
|
+
for k, v in sorted(kwargs.items()):
|
|
75
|
+
key_parts.append("{}:{}".format(k, v))
|
|
76
|
+
return "|".join(key_parts)
|
|
77
|
+
|
|
78
|
+
def get(self, *args, **kwargs):
|
|
79
|
+
"""Get cached result if available and not expired"""
|
|
80
|
+
cache_key = self._generate_cache_key(*args, **kwargs)
|
|
81
|
+
if cache_key in self.cache:
|
|
82
|
+
cached_data = self.cache[cache_key]
|
|
83
|
+
if time.time() - cached_data['timestamp'] < self.cache_duration:
|
|
84
|
+
MediLink_ConfigLoader.log("Cache hit for key: {}".format(cache_key[:50]), level="DEBUG")
|
|
85
|
+
return cached_data['result']
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def set(self, result, *args, **kwargs):
|
|
89
|
+
"""Cache result"""
|
|
90
|
+
cache_key = self._generate_cache_key(*args, **kwargs)
|
|
91
|
+
self.cache[cache_key] = {
|
|
92
|
+
'result': result,
|
|
93
|
+
'timestamp': time.time()
|
|
94
|
+
}
|
|
95
|
+
MediLink_ConfigLoader.log("Cached result for key: {}".format(cache_key[:50]), level="DEBUG")
|
|
96
|
+
|
|
97
|
+
def clear_expired(self):
|
|
98
|
+
"""Remove expired cache entries"""
|
|
99
|
+
now = time.time()
|
|
100
|
+
expired_keys = []
|
|
101
|
+
for key, data in self.cache.items():
|
|
102
|
+
if now - data['timestamp'] >= self.cache_duration:
|
|
103
|
+
expired_keys.append(key)
|
|
104
|
+
|
|
105
|
+
for key in expired_keys:
|
|
106
|
+
del self.cache[key]
|
|
107
|
+
|
|
108
|
+
if expired_keys:
|
|
109
|
+
MediLink_ConfigLoader.log("Cleared {} expired cache entries".format(len(expired_keys)), level="DEBUG")
|
|
110
|
+
|
|
111
|
+
# Rate limiter to prevent API overload
|
|
112
|
+
class APIRateLimiter:
|
|
113
|
+
"""Rate limiter to prevent API overload"""
|
|
114
|
+
def __init__(self, max_calls_per_minute=60):
|
|
115
|
+
self.max_calls = max_calls_per_minute
|
|
116
|
+
self.calls = []
|
|
117
|
+
|
|
118
|
+
def can_make_call(self):
|
|
119
|
+
"""Check if we can make another API call within rate limits"""
|
|
120
|
+
now = time.time()
|
|
121
|
+
# Remove calls older than 1 minute
|
|
122
|
+
self.calls = [call_time for call_time in self.calls if now - call_time < 60]
|
|
123
|
+
can_call = len(self.calls) < self.max_calls
|
|
124
|
+
if not can_call:
|
|
125
|
+
MediLink_ConfigLoader.log("Rate limit reached: {} calls in last minute".format(len(self.calls)), level="WARNING")
|
|
126
|
+
return can_call
|
|
127
|
+
|
|
128
|
+
def record_call(self):
|
|
129
|
+
"""Record that an API call was made"""
|
|
130
|
+
self.calls.append(time.time())
|
|
131
|
+
|
|
132
|
+
def wait_if_needed(self):
|
|
133
|
+
"""Wait if rate limit would be exceeded"""
|
|
134
|
+
if not self.can_make_call():
|
|
135
|
+
# Wait until oldest call expires
|
|
136
|
+
if self.calls:
|
|
137
|
+
wait_time = 60 - (time.time() - self.calls[0])
|
|
138
|
+
if wait_time > 0:
|
|
139
|
+
MediLink_ConfigLoader.log("Rate limit reached, waiting {:.1f} seconds".format(wait_time), level="INFO")
|
|
140
|
+
time.sleep(wait_time)
|
|
141
|
+
|
|
142
|
+
# Enhanced API client wrapper
|
|
143
|
+
class EnhancedAPIWrapper:
|
|
144
|
+
"""Wrapper that adds circuit breaker, caching, and rate limiting to any API client"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, base_client, enable_circuit_breaker=True, enable_caching=True, enable_rate_limiting=True):
|
|
147
|
+
self.base_client = base_client
|
|
148
|
+
|
|
149
|
+
# Initialize enhancement components
|
|
150
|
+
self.circuit_breaker = APICircuitBreaker() if enable_circuit_breaker else None
|
|
151
|
+
self.cache = APICache() if enable_caching else None
|
|
152
|
+
self.rate_limiter = APIRateLimiter() if enable_rate_limiting else None
|
|
153
|
+
|
|
154
|
+
MediLink_ConfigLoader.log("Enhanced API wrapper initialized with circuit_breaker={}, caching={}, rate_limiting={}".format(
|
|
155
|
+
enable_circuit_breaker, enable_caching, enable_rate_limiting), level="INFO")
|
|
156
|
+
|
|
157
|
+
def make_enhanced_call(self, method_name, *args, **kwargs):
|
|
158
|
+
"""Make API call with all enhancements applied"""
|
|
159
|
+
# Check cache first
|
|
160
|
+
if self.cache:
|
|
161
|
+
cached_result = self.cache.get(method_name, *args, **kwargs)
|
|
162
|
+
if cached_result is not None:
|
|
163
|
+
return cached_result
|
|
164
|
+
|
|
165
|
+
# Check rate limits
|
|
166
|
+
if self.rate_limiter:
|
|
167
|
+
self.rate_limiter.wait_if_needed()
|
|
168
|
+
|
|
169
|
+
# Get the method from base client
|
|
170
|
+
if not hasattr(self.base_client, method_name):
|
|
171
|
+
raise AttributeError("Base client does not have method: {}".format(method_name))
|
|
172
|
+
|
|
173
|
+
method = getattr(self.base_client, method_name)
|
|
174
|
+
|
|
175
|
+
# Execute with circuit breaker protection
|
|
176
|
+
if self.circuit_breaker:
|
|
177
|
+
result = self.circuit_breaker.call_with_breaker(method, *args, **kwargs)
|
|
178
|
+
else:
|
|
179
|
+
result = method(*args, **kwargs)
|
|
180
|
+
|
|
181
|
+
# Record rate limit call
|
|
182
|
+
if self.rate_limiter:
|
|
183
|
+
self.rate_limiter.record_call()
|
|
184
|
+
|
|
185
|
+
# Cache result
|
|
186
|
+
if self.cache:
|
|
187
|
+
self.cache.set(result, method_name, *args, **kwargs)
|
|
188
|
+
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
# Utility functions for API enhancement integration
|
|
192
|
+
def create_enhanced_api_client(base_client_class, *args, **kwargs):
|
|
193
|
+
"""
|
|
194
|
+
Factory function to create enhanced API client from base client class.
|
|
195
|
+
Returns base client wrapped with enhancements.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
# Create base client
|
|
199
|
+
base_client = base_client_class(*args, **kwargs)
|
|
200
|
+
|
|
201
|
+
# Wrap with enhancements
|
|
202
|
+
enhanced_client = EnhancedAPIWrapper(base_client)
|
|
203
|
+
|
|
204
|
+
MediLink_ConfigLoader.log("Created enhanced API client from {}".format(base_client_class.__name__), level="INFO")
|
|
205
|
+
return enhanced_client
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
MediLink_ConfigLoader.log("Failed to create enhanced API client: {}".format(str(e)), level="ERROR")
|
|
209
|
+
# Return base client as fallback
|
|
210
|
+
return base_client_class(*args, **kwargs)
|
|
211
|
+
|
|
212
|
+
def safe_api_call(api_function, *args, **kwargs):
|
|
213
|
+
"""
|
|
214
|
+
Safe wrapper for any API call with error handling and fallback.
|
|
215
|
+
Returns (result, success_flag, error_message)
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
result = api_function(*args, **kwargs)
|
|
219
|
+
return result, True, None
|
|
220
|
+
except Exception as e:
|
|
221
|
+
error_msg = str(e)
|
|
222
|
+
MediLink_ConfigLoader.log("API call failed: {}".format(error_msg), level="ERROR")
|
|
223
|
+
return None, False, error_msg
|
|
224
|
+
|
|
225
|
+
def validate_api_response(response, required_fields=None):
|
|
226
|
+
"""
|
|
227
|
+
Validate API response has required structure and fields.
|
|
228
|
+
Returns True if valid, False otherwise.
|
|
229
|
+
"""
|
|
230
|
+
if not response:
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
if required_fields:
|
|
234
|
+
for field in required_fields:
|
|
235
|
+
if field not in response:
|
|
236
|
+
MediLink_ConfigLoader.log("Missing required field in API response: {}".format(field), level="WARNING")
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# API health monitoring
|
|
242
|
+
class APIHealthMonitor:
|
|
243
|
+
"""Monitor API health and performance metrics"""
|
|
244
|
+
|
|
245
|
+
def __init__(self):
|
|
246
|
+
self.metrics = {
|
|
247
|
+
'total_calls': 0,
|
|
248
|
+
'successful_calls': 0,
|
|
249
|
+
'failed_calls': 0,
|
|
250
|
+
'cache_hits': 0,
|
|
251
|
+
'circuit_breaker_trips': 0,
|
|
252
|
+
'rate_limit_hits': 0,
|
|
253
|
+
'start_time': time.time()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def record_call(self, success=True):
|
|
257
|
+
"""Record API call outcome"""
|
|
258
|
+
self.metrics['total_calls'] += 1
|
|
259
|
+
if success:
|
|
260
|
+
self.metrics['successful_calls'] += 1
|
|
261
|
+
else:
|
|
262
|
+
self.metrics['failed_calls'] += 1
|
|
263
|
+
|
|
264
|
+
def record_cache_hit(self):
|
|
265
|
+
"""Record cache hit"""
|
|
266
|
+
self.metrics['cache_hits'] += 1
|
|
267
|
+
|
|
268
|
+
def record_circuit_breaker_trip(self):
|
|
269
|
+
"""Record circuit breaker trip"""
|
|
270
|
+
self.metrics['circuit_breaker_trips'] += 1
|
|
271
|
+
|
|
272
|
+
def record_rate_limit_hit(self):
|
|
273
|
+
"""Record rate limit hit"""
|
|
274
|
+
self.metrics['rate_limit_hits'] += 1
|
|
275
|
+
|
|
276
|
+
def get_health_summary(self):
|
|
277
|
+
"""Get health summary statistics"""
|
|
278
|
+
total_calls = self.metrics['total_calls']
|
|
279
|
+
if total_calls == 0:
|
|
280
|
+
return {'status': 'NO_CALLS', 'metrics': self.metrics}
|
|
281
|
+
|
|
282
|
+
success_rate = self.metrics['successful_calls'] / float(total_calls)
|
|
283
|
+
cache_hit_rate = self.metrics['cache_hits'] / float(total_calls)
|
|
284
|
+
|
|
285
|
+
runtime = time.time() - self.metrics['start_time']
|
|
286
|
+
calls_per_minute = (total_calls / runtime) * 60 if runtime > 0 else 0
|
|
287
|
+
|
|
288
|
+
summary = {
|
|
289
|
+
'status': 'HEALTHY' if success_rate > 0.95 else 'DEGRADED' if success_rate > 0.80 else 'UNHEALTHY',
|
|
290
|
+
'success_rate': success_rate,
|
|
291
|
+
'cache_hit_rate': cache_hit_rate,
|
|
292
|
+
'calls_per_minute': calls_per_minute,
|
|
293
|
+
'runtime_seconds': runtime,
|
|
294
|
+
'metrics': self.metrics
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
MediLink_ConfigLoader.log("API Health Summary: {}".format(str(summary)), level="INFO")
|
|
298
|
+
return summary
|
|
299
|
+
|
|
300
|
+
# Global API health monitor instance
|
|
301
|
+
_global_health_monitor = APIHealthMonitor()
|
|
302
|
+
|
|
303
|
+
def get_api_health_monitor():
|
|
304
|
+
"""Get global API health monitor instance"""
|
|
305
|
+
return _global_health_monitor
|