helix.fhir.client.sdk 4.2.3__py3-none-any.whl → 4.2.18__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.
- helix_fhir_client_sdk/fhir_auth_mixin.py +17 -10
- helix_fhir_client_sdk/fhir_client.py +152 -79
- helix_fhir_client_sdk/fhir_delete_mixin.py +62 -48
- helix_fhir_client_sdk/fhir_merge_mixin.py +188 -166
- helix_fhir_client_sdk/fhir_merge_resources_mixin.py +200 -15
- helix_fhir_client_sdk/fhir_patch_mixin.py +97 -84
- helix_fhir_client_sdk/fhir_update_mixin.py +71 -57
- helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py +0 -24
- helix_fhir_client_sdk/open_telemetry/__init__.py +0 -0
- helix_fhir_client_sdk/open_telemetry/attribute_names.py +7 -0
- helix_fhir_client_sdk/open_telemetry/span_names.py +12 -0
- helix_fhir_client_sdk/queue/request_queue_mixin.py +17 -12
- helix_fhir_client_sdk/responses/fhir_client_protocol.py +10 -6
- helix_fhir_client_sdk/responses/fhir_get_response.py +1 -1
- helix_fhir_client_sdk/responses/fhir_response_processor.py +73 -54
- helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +1 -1
- helix_fhir_client_sdk/responses/get/fhir_get_error_response.py +0 -1
- helix_fhir_client_sdk/responses/get/fhir_get_list_by_resource_type_response.py +1 -1
- helix_fhir_client_sdk/responses/get/fhir_get_list_response.py +1 -1
- helix_fhir_client_sdk/responses/get/fhir_get_response_factory.py +0 -1
- helix_fhir_client_sdk/responses/get/fhir_get_single_response.py +1 -1
- helix_fhir_client_sdk/responses/merge/fhir_merge_resource_response_entry.py +30 -0
- helix_fhir_client_sdk/utilities/cache/request_cache.py +32 -43
- helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py +185 -154
- helix_fhir_client_sdk/utilities/retryable_aiohttp_response.py +2 -1
- helix_fhir_client_sdk/validators/async_fhir_validator.py +3 -0
- helix_fhir_client_sdk-4.2.18.dist-info/METADATA +200 -0
- {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/RECORD +35 -28
- tests/async/test_benchmark_compress.py +448 -0
- tests/async/test_benchmark_merge.py +506 -0
- tests/async/test_retryable_client_session_management.py +159 -0
- tests/test_fhir_client_clone.py +155 -0
- helix_fhir_client_sdk-4.2.3.dist-info/METADATA +0 -115
- {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/WHEEL +0 -0
- {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/licenses/LICENSE +0 -0
- {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/top_level.txt +0 -0
|
@@ -7,12 +7,15 @@ from typing import Any, cast
|
|
|
7
7
|
import async_timeout
|
|
8
8
|
from aiohttp import ClientError, ClientResponse, ClientResponseError, ClientSession
|
|
9
9
|
from multidict import MultiMapping
|
|
10
|
+
from opentelemetry import trace
|
|
10
11
|
|
|
11
12
|
from helix_fhir_client_sdk.function_types import (
|
|
12
13
|
RefreshTokenFunction,
|
|
13
14
|
RefreshTokenResult,
|
|
14
15
|
TraceRequestFunction,
|
|
15
16
|
)
|
|
17
|
+
from helix_fhir_client_sdk.open_telemetry.attribute_names import FhirClientSdkOpenTelemetryAttributeNames
|
|
18
|
+
from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
|
|
16
19
|
from helix_fhir_client_sdk.utilities.retryable_aiohttp_response import (
|
|
17
20
|
RetryableAioHttpResponse,
|
|
18
21
|
)
|
|
@@ -20,6 +23,8 @@ from helix_fhir_client_sdk.utilities.retryable_aiohttp_url_result import (
|
|
|
20
23
|
RetryableAioHttpUrlResult,
|
|
21
24
|
)
|
|
22
25
|
|
|
26
|
+
TRACER = trace.get_tracer(__name__)
|
|
27
|
+
|
|
23
28
|
|
|
24
29
|
class RetryableAioHttpClient:
|
|
25
30
|
def __init__(
|
|
@@ -32,6 +37,7 @@ class RetryableAioHttpClient:
|
|
|
32
37
|
refresh_token_func: RefreshTokenFunction | None,
|
|
33
38
|
tracer_request_func: TraceRequestFunction | None,
|
|
34
39
|
fn_get_session: Callable[[], ClientSession] | None = None,
|
|
40
|
+
caller_managed_session: bool = False,
|
|
35
41
|
exclude_status_codes_from_retry: list[int] | None = None,
|
|
36
42
|
use_data_streaming: bool | None,
|
|
37
43
|
compress: bool | None = False,
|
|
@@ -40,13 +46,34 @@ class RetryableAioHttpClient:
|
|
|
40
46
|
log_all_url_results: bool = False,
|
|
41
47
|
access_token: str | None,
|
|
42
48
|
access_token_expiry_date: datetime | None,
|
|
43
|
-
persistent_session: ClientSession | None = None,
|
|
44
|
-
use_persistent_session: bool = False,
|
|
45
|
-
close_session_on_exit: bool = True,
|
|
46
49
|
) -> None:
|
|
47
50
|
"""
|
|
48
|
-
RetryableClient provides a way to make HTTP calls with automatic retry and automatic refreshing of access tokens
|
|
51
|
+
RetryableClient provides a way to make HTTP calls with automatic retry and automatic refreshing of access tokens.
|
|
52
|
+
|
|
53
|
+
Session Lifecycle Management:
|
|
54
|
+
- If caller_managed_session is False (default): The SDK manages the session lifecycle.
|
|
55
|
+
The session will be automatically closed when exiting the context manager.
|
|
56
|
+
- If caller_managed_session is True: The caller is responsible for managing the session lifecycle.
|
|
57
|
+
The SDK will NOT close the session - the caller must close it themselves.
|
|
49
58
|
|
|
59
|
+
:param retries: Number of retry attempts for failed requests
|
|
60
|
+
:param timeout_in_seconds: Timeout for HTTP requests
|
|
61
|
+
:param backoff_factor: Factor for exponential backoff between retries
|
|
62
|
+
:param retry_status_codes: HTTP status codes that trigger a retry
|
|
63
|
+
:param refresh_token_func: Function to refresh authentication tokens
|
|
64
|
+
:param tracer_request_func: Function to trace/log requests
|
|
65
|
+
:param fn_get_session: Optional callable that returns a ClientSession. If None, a basic
|
|
66
|
+
ClientSession will be created internally.
|
|
67
|
+
:param caller_managed_session: If True, the caller is responsible for closing the session.
|
|
68
|
+
If False (default), the SDK will close the session on exit.
|
|
69
|
+
:param exclude_status_codes_from_retry: Status codes to exclude from retry logic
|
|
70
|
+
:param use_data_streaming: Whether to stream response data
|
|
71
|
+
:param compress: Whether to compress request data
|
|
72
|
+
:param send_data_as_chunked: Whether to use chunked transfer encoding
|
|
73
|
+
:param throw_exception_on_error: Whether to raise exceptions on HTTP errors
|
|
74
|
+
:param log_all_url_results: Whether to log all URL results
|
|
75
|
+
:param access_token: Access token for authentication
|
|
76
|
+
:param access_token_expiry_date: Expiry date of the access token
|
|
50
77
|
"""
|
|
51
78
|
self.retries: int = retries
|
|
52
79
|
self.timeout_in_seconds: float | None = timeout_in_seconds
|
|
@@ -56,6 +83,8 @@ class RetryableAioHttpClient:
|
|
|
56
83
|
)
|
|
57
84
|
self.refresh_token_func_async: RefreshTokenFunction | None = refresh_token_func
|
|
58
85
|
self.trace_function_async: TraceRequestFunction | None = tracer_request_func
|
|
86
|
+
self._caller_managed_session: bool = caller_managed_session
|
|
87
|
+
# If no session factory provided, use a default one that creates a basic ClientSession
|
|
59
88
|
self.fn_get_session: Callable[[], ClientSession] = (
|
|
60
89
|
fn_get_session if fn_get_session is not None else lambda: ClientSession()
|
|
61
90
|
)
|
|
@@ -68,15 +97,9 @@ class RetryableAioHttpClient:
|
|
|
68
97
|
self.log_all_url_results: bool = log_all_url_results
|
|
69
98
|
self.access_token: str | None = access_token
|
|
70
99
|
self.access_token_expiry_date: datetime | None = access_token_expiry_date
|
|
71
|
-
self.close_session_on_exit: bool = close_session_on_exit
|
|
72
|
-
self.persistent_session: ClientSession | None = persistent_session
|
|
73
|
-
self.use_persistent_session: bool = use_persistent_session
|
|
74
100
|
|
|
75
101
|
async def __aenter__(self) -> "RetryableAioHttpClient":
|
|
76
|
-
|
|
77
|
-
self.session = self.persistent_session
|
|
78
|
-
else:
|
|
79
|
-
self.session = self.fn_get_session()
|
|
102
|
+
self.session = self.fn_get_session()
|
|
80
103
|
return self
|
|
81
104
|
|
|
82
105
|
async def __aexit__(
|
|
@@ -85,7 +108,9 @@ class RetryableAioHttpClient:
|
|
|
85
108
|
exc_val: BaseException | None,
|
|
86
109
|
exc_tb: type[BaseException] | None | None,
|
|
87
110
|
) -> None:
|
|
88
|
-
|
|
111
|
+
# Only close the session if SDK created it (fn_get_session was not provided)
|
|
112
|
+
# If the caller provided fn_get_session, they are responsible for closing the session
|
|
113
|
+
if not self._caller_managed_session and self.session is not None:
|
|
89
114
|
await self.session.close()
|
|
90
115
|
|
|
91
116
|
@staticmethod
|
|
@@ -119,7 +144,7 @@ class RetryableAioHttpClient:
|
|
|
119
144
|
try:
|
|
120
145
|
if headers:
|
|
121
146
|
kwargs["headers"] = headers
|
|
122
|
-
# if there is no data then remove from kwargs so as not to confuse aiohttp
|
|
147
|
+
# if there is no data, then remove from kwargs so as not to confuse aiohttp
|
|
123
148
|
if "data" in kwargs and kwargs["data"] is None:
|
|
124
149
|
del kwargs["data"]
|
|
125
150
|
# compression and chunked can only be enabled if there is content sent
|
|
@@ -129,164 +154,101 @@ class RetryableAioHttpClient:
|
|
|
129
154
|
if self.compress:
|
|
130
155
|
kwargs["compress"] = self.compress
|
|
131
156
|
assert self.session is not None
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
method,
|
|
157
|
+
with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.HTTP_GET) as span:
|
|
158
|
+
span.set_attribute(
|
|
159
|
+
FhirClientSdkOpenTelemetryAttributeNames.URL,
|
|
136
160
|
url,
|
|
137
|
-
**kwargs,
|
|
138
161
|
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
162
|
+
async with async_timeout.timeout(self.timeout_in_seconds):
|
|
163
|
+
start_time: float = time.time()
|
|
164
|
+
response: ClientResponse = await self.session.request(
|
|
165
|
+
method,
|
|
166
|
+
url,
|
|
167
|
+
**kwargs,
|
|
168
|
+
)
|
|
169
|
+
# Append the result to the list of results
|
|
170
|
+
if self.log_all_url_results:
|
|
171
|
+
results_by_url.append(
|
|
172
|
+
RetryableAioHttpUrlResult(
|
|
173
|
+
ok=response.ok,
|
|
174
|
+
url=url,
|
|
175
|
+
status_code=response.status,
|
|
176
|
+
retry_count=retry_attempts,
|
|
177
|
+
start_time=start_time,
|
|
178
|
+
end_time=time.time(),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
response_headers: dict[str, str] = {
|
|
182
|
+
k: ",".join(response.headers.getall(k)) for k in response.headers.keys()
|
|
183
|
+
}
|
|
184
|
+
response_headers_multi_mapping: MultiMapping[str] = cast(MultiMapping[str], response.headers)
|
|
185
|
+
|
|
186
|
+
if self.trace_function_async:
|
|
187
|
+
request_headers: dict[str, str] = {
|
|
188
|
+
k: ",".join(response.request_info.headers.getall(k))
|
|
189
|
+
for k in response.request_info.headers.keys()
|
|
190
|
+
}
|
|
191
|
+
await self.trace_function_async(
|
|
143
192
|
ok=response.ok,
|
|
144
193
|
url=url,
|
|
145
194
|
status_code=response.status,
|
|
195
|
+
access_token=access_token,
|
|
196
|
+
expiry_date=expiry_date,
|
|
146
197
|
retry_count=retry_attempts,
|
|
147
198
|
start_time=start_time,
|
|
148
199
|
end_time=time.time(),
|
|
200
|
+
request_headers=request_headers,
|
|
201
|
+
response_headers=response_headers,
|
|
149
202
|
)
|
|
150
|
-
)
|
|
151
|
-
response_headers: dict[str, str] = {
|
|
152
|
-
k: ",".join(response.headers.getall(k)) for k in response.headers.keys()
|
|
153
|
-
}
|
|
154
|
-
response_headers_multi_mapping: MultiMapping[str] = cast(MultiMapping[str], response.headers)
|
|
155
203
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
k: ",".join(response.request_info.headers.getall(k))
|
|
159
|
-
for k in response.request_info.headers.keys()
|
|
160
|
-
}
|
|
161
|
-
await self.trace_function_async(
|
|
162
|
-
ok=response.ok,
|
|
163
|
-
url=url,
|
|
164
|
-
status_code=response.status,
|
|
165
|
-
access_token=access_token,
|
|
166
|
-
expiry_date=expiry_date,
|
|
167
|
-
retry_count=retry_attempts,
|
|
168
|
-
start_time=start_time,
|
|
169
|
-
end_time=time.time(),
|
|
170
|
-
request_headers=request_headers,
|
|
171
|
-
response_headers=response_headers,
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
if response.ok:
|
|
175
|
-
# If the response is successful, return the response
|
|
176
|
-
return RetryableAioHttpResponse(
|
|
177
|
-
ok=response.ok,
|
|
178
|
-
status=response.status,
|
|
179
|
-
response_headers=response_headers,
|
|
180
|
-
response_text=(
|
|
181
|
-
await self.get_safe_response_text_async(response=response)
|
|
182
|
-
if not self.use_data_streaming
|
|
183
|
-
else ""
|
|
184
|
-
),
|
|
185
|
-
content=response.content,
|
|
186
|
-
use_data_streaming=self.use_data_streaming,
|
|
187
|
-
results_by_url=results_by_url,
|
|
188
|
-
access_token=access_token,
|
|
189
|
-
access_token_expiry_date=expiry_date,
|
|
190
|
-
retry_count=retry_attempts,
|
|
191
|
-
)
|
|
192
|
-
elif (
|
|
193
|
-
self.exclude_status_codes_from_retry and response.status in self.exclude_status_codes_from_retry
|
|
194
|
-
):
|
|
195
|
-
return RetryableAioHttpResponse(
|
|
196
|
-
ok=response.ok,
|
|
197
|
-
status=response.status,
|
|
198
|
-
response_headers=response_headers,
|
|
199
|
-
response_text=await self.get_safe_response_text_async(response=response),
|
|
200
|
-
content=response.content,
|
|
201
|
-
use_data_streaming=self.use_data_streaming,
|
|
202
|
-
results_by_url=results_by_url,
|
|
203
|
-
access_token=access_token,
|
|
204
|
-
access_token_expiry_date=expiry_date,
|
|
205
|
-
retry_count=retry_attempts,
|
|
206
|
-
)
|
|
207
|
-
elif response.status == 400:
|
|
208
|
-
return RetryableAioHttpResponse(
|
|
209
|
-
ok=response.ok,
|
|
210
|
-
status=response.status,
|
|
211
|
-
response_headers=response_headers,
|
|
212
|
-
response_text=await self.get_safe_response_text_async(response=response),
|
|
213
|
-
content=response.content,
|
|
214
|
-
use_data_streaming=self.use_data_streaming,
|
|
215
|
-
results_by_url=results_by_url,
|
|
216
|
-
access_token=access_token,
|
|
217
|
-
access_token_expiry_date=expiry_date,
|
|
218
|
-
retry_count=retry_attempts,
|
|
219
|
-
)
|
|
220
|
-
elif response.status in [403, 404]:
|
|
221
|
-
return RetryableAioHttpResponse(
|
|
222
|
-
ok=response.ok,
|
|
223
|
-
status=response.status,
|
|
224
|
-
response_headers=response_headers,
|
|
225
|
-
response_text=await self.get_safe_response_text_async(response=response),
|
|
226
|
-
content=response.content,
|
|
227
|
-
use_data_streaming=self.use_data_streaming,
|
|
228
|
-
results_by_url=results_by_url,
|
|
229
|
-
access_token=access_token,
|
|
230
|
-
access_token_expiry_date=expiry_date,
|
|
231
|
-
retry_count=retry_attempts,
|
|
232
|
-
)
|
|
233
|
-
elif response.status == 429:
|
|
234
|
-
await self._handle_429(response=response, full_url=url)
|
|
235
|
-
elif response.status == 401 and self.refresh_token_func_async:
|
|
236
|
-
# Call the token refresh function if status code is 401
|
|
237
|
-
refresh_token_result: RefreshTokenResult = await self.refresh_token_func_async(
|
|
238
|
-
current_token=access_token,
|
|
239
|
-
expiry_date=expiry_date,
|
|
240
|
-
url=url,
|
|
241
|
-
status_code=response.status,
|
|
242
|
-
retry_count=retry_attempts,
|
|
243
|
-
)
|
|
244
|
-
if refresh_token_result.abort_request or refresh_token_result.access_token is None:
|
|
204
|
+
if response.ok:
|
|
205
|
+
# If the response is successful, return the response
|
|
245
206
|
return RetryableAioHttpResponse(
|
|
246
|
-
ok=
|
|
247
|
-
status=
|
|
248
|
-
response_headers=
|
|
249
|
-
response_text=
|
|
250
|
-
|
|
207
|
+
ok=response.ok,
|
|
208
|
+
status=response.status,
|
|
209
|
+
response_headers=response_headers,
|
|
210
|
+
response_text=(
|
|
211
|
+
await self.get_safe_response_text_async(response=response)
|
|
212
|
+
if not self.use_data_streaming
|
|
213
|
+
else ""
|
|
214
|
+
),
|
|
215
|
+
content=response.content,
|
|
251
216
|
use_data_streaming=self.use_data_streaming,
|
|
252
217
|
results_by_url=results_by_url,
|
|
253
218
|
access_token=access_token,
|
|
254
219
|
access_token_expiry_date=expiry_date,
|
|
255
220
|
retry_count=retry_attempts,
|
|
256
221
|
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if retry_attempts >= self.retries:
|
|
264
|
-
raise ClientResponseError(
|
|
265
|
-
status=response.status,
|
|
266
|
-
message="Unauthorized",
|
|
267
|
-
headers=response_headers_multi_mapping,
|
|
268
|
-
history=response.history,
|
|
269
|
-
request_info=response.request_info,
|
|
270
|
-
)
|
|
271
|
-
await asyncio.sleep(self.backoff_factor * (2 ** (retry_attempts - 1)))
|
|
272
|
-
elif self.retry_status_codes and response.status in self.retry_status_codes:
|
|
273
|
-
raise ClientResponseError(
|
|
274
|
-
status=response.status,
|
|
275
|
-
message="Retryable status code received",
|
|
276
|
-
headers=response_headers_multi_mapping,
|
|
277
|
-
history=response.history,
|
|
278
|
-
request_info=response.request_info,
|
|
279
|
-
)
|
|
280
|
-
else:
|
|
281
|
-
if self._throw_exception_on_error:
|
|
282
|
-
raise ClientResponseError(
|
|
222
|
+
elif (
|
|
223
|
+
self.exclude_status_codes_from_retry
|
|
224
|
+
and response.status in self.exclude_status_codes_from_retry
|
|
225
|
+
):
|
|
226
|
+
return RetryableAioHttpResponse(
|
|
227
|
+
ok=response.ok,
|
|
283
228
|
status=response.status,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
229
|
+
response_headers=response_headers,
|
|
230
|
+
response_text=await self.get_safe_response_text_async(response=response),
|
|
231
|
+
content=response.content,
|
|
232
|
+
use_data_streaming=self.use_data_streaming,
|
|
233
|
+
results_by_url=results_by_url,
|
|
234
|
+
access_token=access_token,
|
|
235
|
+
access_token_expiry_date=expiry_date,
|
|
236
|
+
retry_count=retry_attempts,
|
|
288
237
|
)
|
|
289
|
-
|
|
238
|
+
elif response.status == 400:
|
|
239
|
+
return RetryableAioHttpResponse(
|
|
240
|
+
ok=response.ok,
|
|
241
|
+
status=response.status,
|
|
242
|
+
response_headers=response_headers,
|
|
243
|
+
response_text=await self.get_safe_response_text_async(response=response),
|
|
244
|
+
content=response.content,
|
|
245
|
+
use_data_streaming=self.use_data_streaming,
|
|
246
|
+
results_by_url=results_by_url,
|
|
247
|
+
access_token=access_token,
|
|
248
|
+
access_token_expiry_date=expiry_date,
|
|
249
|
+
retry_count=retry_attempts,
|
|
250
|
+
)
|
|
251
|
+
elif response.status in [403, 404]:
|
|
290
252
|
return RetryableAioHttpResponse(
|
|
291
253
|
ok=response.ok,
|
|
292
254
|
status=response.status,
|
|
@@ -299,6 +261,75 @@ class RetryableAioHttpClient:
|
|
|
299
261
|
access_token_expiry_date=expiry_date,
|
|
300
262
|
retry_count=retry_attempts,
|
|
301
263
|
)
|
|
264
|
+
elif response.status == 429:
|
|
265
|
+
await self._handle_429(response=response, full_url=url)
|
|
266
|
+
elif response.status == 401 and self.refresh_token_func_async:
|
|
267
|
+
# Call the token refresh function if status code is 401
|
|
268
|
+
refresh_token_result: RefreshTokenResult = await self.refresh_token_func_async(
|
|
269
|
+
current_token=access_token,
|
|
270
|
+
expiry_date=expiry_date,
|
|
271
|
+
url=url,
|
|
272
|
+
status_code=response.status,
|
|
273
|
+
retry_count=retry_attempts,
|
|
274
|
+
)
|
|
275
|
+
if refresh_token_result.abort_request or refresh_token_result.access_token is None:
|
|
276
|
+
return RetryableAioHttpResponse(
|
|
277
|
+
ok=False,
|
|
278
|
+
status=401,
|
|
279
|
+
response_headers={},
|
|
280
|
+
response_text="Unauthorized",
|
|
281
|
+
content=None,
|
|
282
|
+
use_data_streaming=self.use_data_streaming,
|
|
283
|
+
results_by_url=results_by_url,
|
|
284
|
+
access_token=access_token,
|
|
285
|
+
access_token_expiry_date=expiry_date,
|
|
286
|
+
retry_count=retry_attempts,
|
|
287
|
+
)
|
|
288
|
+
else: # we got a valid token
|
|
289
|
+
access_token = refresh_token_result.access_token
|
|
290
|
+
expiry_date = refresh_token_result.expiry_date
|
|
291
|
+
if not headers:
|
|
292
|
+
headers = {}
|
|
293
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
294
|
+
if retry_attempts >= self.retries:
|
|
295
|
+
raise ClientResponseError(
|
|
296
|
+
status=response.status,
|
|
297
|
+
message="Unauthorized",
|
|
298
|
+
headers=response_headers_multi_mapping,
|
|
299
|
+
history=response.history,
|
|
300
|
+
request_info=response.request_info,
|
|
301
|
+
)
|
|
302
|
+
await asyncio.sleep(self.backoff_factor * (2 ** (retry_attempts - 1)))
|
|
303
|
+
elif self.retry_status_codes and response.status in self.retry_status_codes:
|
|
304
|
+
raise ClientResponseError(
|
|
305
|
+
status=response.status,
|
|
306
|
+
message="Retryable status code received",
|
|
307
|
+
headers=response_headers_multi_mapping,
|
|
308
|
+
history=response.history,
|
|
309
|
+
request_info=response.request_info,
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
if self._throw_exception_on_error:
|
|
313
|
+
raise ClientResponseError(
|
|
314
|
+
status=response.status,
|
|
315
|
+
message="Non-retryable status code received",
|
|
316
|
+
headers=response_headers_multi_mapping,
|
|
317
|
+
history=response.history,
|
|
318
|
+
request_info=response.request_info,
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
return RetryableAioHttpResponse(
|
|
322
|
+
ok=response.ok,
|
|
323
|
+
status=response.status,
|
|
324
|
+
response_headers=response_headers,
|
|
325
|
+
response_text=await self.get_safe_response_text_async(response=response),
|
|
326
|
+
content=response.content,
|
|
327
|
+
use_data_streaming=self.use_data_streaming,
|
|
328
|
+
results_by_url=results_by_url,
|
|
329
|
+
access_token=access_token,
|
|
330
|
+
access_token_expiry_date=expiry_date,
|
|
331
|
+
retry_count=retry_attempts,
|
|
332
|
+
)
|
|
302
333
|
except (TimeoutError, ClientError, ClientResponseError) as e:
|
|
303
334
|
if retry_attempts >= self.retries:
|
|
304
335
|
if self._throw_exception_on_error:
|
|
@@ -397,7 +428,7 @@ class RetryableAioHttpClient:
|
|
|
397
428
|
if retry_after_text:
|
|
398
429
|
# noinspection PyBroadException
|
|
399
430
|
try:
|
|
400
|
-
if retry_after_text.isnumeric(): # it is number of seconds
|
|
431
|
+
if retry_after_text.isnumeric(): # it is a number of seconds
|
|
401
432
|
await asyncio.sleep(int(retry_after_text))
|
|
402
433
|
else:
|
|
403
434
|
wait_till: datetime = datetime.strptime(retry_after_text, "%a, %d %b %Y %H:%M:%S GMT")
|
|
@@ -411,7 +442,7 @@ class RetryableAioHttpClient:
|
|
|
411
442
|
if time_diff > 0:
|
|
412
443
|
await asyncio.sleep(time_diff)
|
|
413
444
|
except Exception:
|
|
414
|
-
# if there was some exception parsing the Retry-After header, sleep for 60 seconds
|
|
445
|
+
# if there was some exception, parsing the Retry-After header, sleep for 60 seconds
|
|
415
446
|
await asyncio.sleep(60)
|
|
416
447
|
else:
|
|
417
448
|
await asyncio.sleep(60)
|
|
@@ -3,6 +3,7 @@ from datetime import datetime
|
|
|
3
3
|
from typing import Any, cast
|
|
4
4
|
|
|
5
5
|
from aiohttp import StreamReader
|
|
6
|
+
from multidict import CIMultiDict
|
|
6
7
|
|
|
7
8
|
from helix_fhir_client_sdk.utilities.retryable_aiohttp_url_result import (
|
|
8
9
|
RetryableAioHttpUrlResult,
|
|
@@ -53,7 +54,7 @@ class RetryableAioHttpResponse:
|
|
|
53
54
|
self.status: int = status
|
|
54
55
|
""" Status code of the response """
|
|
55
56
|
|
|
56
|
-
self.response_headers:
|
|
57
|
+
self.response_headers: CIMultiDict[str] = CIMultiDict(response_headers)
|
|
57
58
|
""" Headers of the response """
|
|
58
59
|
|
|
59
60
|
self._response_text: str = response_text
|
|
@@ -23,6 +23,7 @@ class AsyncFhirValidator:
|
|
|
23
23
|
resource_name: str,
|
|
24
24
|
validation_server_url: str,
|
|
25
25
|
access_token: str | None,
|
|
26
|
+
caller_managed_session: bool = False,
|
|
26
27
|
) -> None:
|
|
27
28
|
"""
|
|
28
29
|
Calls the validation server url to validate the given resource
|
|
@@ -32,6 +33,7 @@ class AsyncFhirValidator:
|
|
|
32
33
|
:param resource_name: name of resource
|
|
33
34
|
:param validation_server_url: url to validation server
|
|
34
35
|
:param access_token: access token to use
|
|
36
|
+
:param caller_managed_session: if True, the caller is responsible for closing the session
|
|
35
37
|
"""
|
|
36
38
|
# check each resource against the validation server
|
|
37
39
|
headers = {"Content-Type": "application/fhir+json"}
|
|
@@ -42,6 +44,7 @@ class AsyncFhirValidator:
|
|
|
42
44
|
full_validation_uri /= "$validate"
|
|
43
45
|
async with RetryableAioHttpClient(
|
|
44
46
|
fn_get_session=fn_get_session,
|
|
47
|
+
caller_managed_session=caller_managed_session,
|
|
45
48
|
use_data_streaming=False,
|
|
46
49
|
access_token=access_token,
|
|
47
50
|
access_token_expiry_date=None,
|