helix.fhir.client.sdk 4.2.3__py3-none-any.whl → 4.2.19__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 +147 -49
- 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 +3 -4
- helix_fhir_client_sdk/responses/fhir_response_processor.py +73 -54
- helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +49 -28
- 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/responses/resource_separator.py +35 -40
- 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.19.dist-info/METADATA +200 -0
- {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/RECORD +36 -29
- 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.19.dist-info}/WHEEL +0 -0
- {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/licenses/LICENSE +0 -0
- {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/top_level.txt +0 -0
|
@@ -2,9 +2,7 @@ import json
|
|
|
2
2
|
import time
|
|
3
3
|
from collections import deque
|
|
4
4
|
from collections.abc import AsyncGenerator
|
|
5
|
-
from typing import
|
|
6
|
-
cast,
|
|
7
|
-
)
|
|
5
|
+
from typing import Any, cast
|
|
8
6
|
from urllib import parse
|
|
9
7
|
|
|
10
8
|
import requests
|
|
@@ -45,6 +43,195 @@ from helix_fhir_client_sdk.validators.async_fhir_validator import AsyncFhirValid
|
|
|
45
43
|
|
|
46
44
|
|
|
47
45
|
class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
46
|
+
async def merge_bundle_uncompressed(
|
|
47
|
+
self,
|
|
48
|
+
id_: str | None,
|
|
49
|
+
bundle: FhirBundle,
|
|
50
|
+
) -> FhirMergeResourceResponse:
|
|
51
|
+
"""
|
|
52
|
+
Optimized variant of :meth:`merge_bundle_async` that bypasses storage-mode handling.
|
|
53
|
+
Use this method when you do not need storage-mode behavior, or features such as request/response compression.
|
|
54
|
+
:param id_: id of the resource to merge
|
|
55
|
+
:param bundle: FHIR Bundle to merge
|
|
56
|
+
:return: FhirMergeResourceResponse
|
|
57
|
+
"""
|
|
58
|
+
# Initialize profiling dictionary
|
|
59
|
+
profiling: dict[str, float] = {
|
|
60
|
+
"total_time": 0.0,
|
|
61
|
+
"build_url": 0.0,
|
|
62
|
+
"get_access_token": 0.0,
|
|
63
|
+
"prepare_payload": 0.0,
|
|
64
|
+
"http_post": 0.0,
|
|
65
|
+
"parse_response": 0.0,
|
|
66
|
+
"create_response_objects": 0.0,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
merge_start_time = time.time()
|
|
70
|
+
|
|
71
|
+
request_id: str | None = None
|
|
72
|
+
response_status: int = 500
|
|
73
|
+
|
|
74
|
+
# Build URL
|
|
75
|
+
build_url_start = time.time()
|
|
76
|
+
full_uri: furl = furl(self._url)
|
|
77
|
+
full_uri /= self._resource
|
|
78
|
+
|
|
79
|
+
# Prepare headers
|
|
80
|
+
headers = {"Content-Type": "application/fhir+json"}
|
|
81
|
+
headers.update(self._additional_request_headers)
|
|
82
|
+
profiling["build_url"] = time.time() - build_url_start
|
|
83
|
+
|
|
84
|
+
# Get access token
|
|
85
|
+
get_token_start = time.time()
|
|
86
|
+
access_token_result: GetAccessTokenResult = await self.get_access_token_async()
|
|
87
|
+
access_token: str | None = access_token_result.access_token
|
|
88
|
+
if access_token:
|
|
89
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
90
|
+
profiling["get_access_token"] = time.time() - get_token_start
|
|
91
|
+
|
|
92
|
+
# Prepare JSON payload
|
|
93
|
+
prepare_payload_start = time.time()
|
|
94
|
+
first_resource: FhirResource | None = bundle.entry[0].resource
|
|
95
|
+
assert first_resource is not None
|
|
96
|
+
json_payload: str = first_resource.json() if len(bundle.entry) == 1 else bundle.json()
|
|
97
|
+
|
|
98
|
+
# Build merge URL
|
|
99
|
+
obj_id: str = id_ or "1"
|
|
100
|
+
resource_uri: furl = full_uri / parse.quote(str(obj_id), safe="") / "$merge"
|
|
101
|
+
profiling["prepare_payload"] = time.time() - prepare_payload_start
|
|
102
|
+
|
|
103
|
+
response_text: str | None = None
|
|
104
|
+
responses: list[dict[str, Any]] = []
|
|
105
|
+
errors: list[dict[str, Any]] = []
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
async with RetryableAioHttpClient(
|
|
109
|
+
fn_get_session=self.create_http_session,
|
|
110
|
+
refresh_token_func=self._refresh_token_function,
|
|
111
|
+
tracer_request_func=self._trace_request_function,
|
|
112
|
+
retries=self._retry_count,
|
|
113
|
+
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
114
|
+
use_data_streaming=self._use_data_streaming,
|
|
115
|
+
send_data_as_chunked=self._send_data_as_chunked,
|
|
116
|
+
compress=self._compress,
|
|
117
|
+
throw_exception_on_error=self._throw_exception_on_error,
|
|
118
|
+
log_all_url_results=self._log_all_response_urls,
|
|
119
|
+
access_token=self._access_token,
|
|
120
|
+
access_token_expiry_date=self._access_token_expiry_date,
|
|
121
|
+
) as client:
|
|
122
|
+
http_post_start = time.time()
|
|
123
|
+
response: RetryableAioHttpResponse = await client.post(
|
|
124
|
+
url=resource_uri.url,
|
|
125
|
+
data=json_payload,
|
|
126
|
+
headers=headers,
|
|
127
|
+
)
|
|
128
|
+
profiling["http_post"] = time.time() - http_post_start
|
|
129
|
+
|
|
130
|
+
response_status = response.status
|
|
131
|
+
request_id = response.response_headers.get("X-Request-ID", None)
|
|
132
|
+
|
|
133
|
+
parse_response_start = time.time()
|
|
134
|
+
if response.status == 200:
|
|
135
|
+
response_text = await response.get_text_async()
|
|
136
|
+
if response_text:
|
|
137
|
+
try:
|
|
138
|
+
# Parse response as plain dicts for speed
|
|
139
|
+
parsed_response = json.loads(response_text)
|
|
140
|
+
if isinstance(parsed_response, list):
|
|
141
|
+
responses = parsed_response
|
|
142
|
+
else:
|
|
143
|
+
responses = [parsed_response]
|
|
144
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
145
|
+
errors.append(
|
|
146
|
+
{
|
|
147
|
+
"issue": [
|
|
148
|
+
{
|
|
149
|
+
"severity": "error",
|
|
150
|
+
"code": "exception",
|
|
151
|
+
"diagnostics": f"Failed to parse response: {str(e)}",
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
# HTTP error
|
|
158
|
+
response_text = await response.get_text_async()
|
|
159
|
+
errors.append(
|
|
160
|
+
{
|
|
161
|
+
"issue": [
|
|
162
|
+
{
|
|
163
|
+
"severity": "error",
|
|
164
|
+
"code": "exception",
|
|
165
|
+
"diagnostics": response_text or f"HTTP {response.status}",
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
profiling["parse_response"] = time.time() - parse_response_start
|
|
171
|
+
|
|
172
|
+
except requests.exceptions.HTTPError as e:
|
|
173
|
+
raise FhirSenderException(
|
|
174
|
+
request_id=request_id,
|
|
175
|
+
url=resource_uri.url,
|
|
176
|
+
headers=headers,
|
|
177
|
+
json_data=json_payload,
|
|
178
|
+
response_text=response_text,
|
|
179
|
+
response_status_code=response_status,
|
|
180
|
+
exception=e,
|
|
181
|
+
variables=FhirClientLogger.get_variables_to_log(vars(self)),
|
|
182
|
+
message=f"HttpError: {e}",
|
|
183
|
+
elapsed_time=time.time() - merge_start_time,
|
|
184
|
+
) from e
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise FhirSenderException(
|
|
187
|
+
request_id=request_id,
|
|
188
|
+
url=resource_uri.url,
|
|
189
|
+
headers=headers,
|
|
190
|
+
json_data=json_payload,
|
|
191
|
+
response_text=response_text,
|
|
192
|
+
response_status_code=response_status,
|
|
193
|
+
exception=e,
|
|
194
|
+
variables=FhirClientLogger.get_variables_to_log(vars(self)),
|
|
195
|
+
message=f"Unknown Error: {e}",
|
|
196
|
+
elapsed_time=time.time() - merge_start_time,
|
|
197
|
+
) from e
|
|
198
|
+
|
|
199
|
+
# Convert dict responses to proper objects using fast method
|
|
200
|
+
create_objects_start = time.time()
|
|
201
|
+
response_entries: deque[BaseFhirMergeResourceResponseEntry] = deque()
|
|
202
|
+
|
|
203
|
+
for resp_dict in responses:
|
|
204
|
+
response_entries.append(FhirMergeResourceResponseEntry.from_dict_uncompressed(resp_dict))
|
|
205
|
+
|
|
206
|
+
for error_dict in errors:
|
|
207
|
+
response_entries.append(FhirMergeResponseEntryError.from_dict(error_dict, storage_mode=self._storage_mode))
|
|
208
|
+
profiling["create_response_objects"] = time.time() - create_objects_start
|
|
209
|
+
|
|
210
|
+
profiling["total_time"] = time.time() - merge_start_time
|
|
211
|
+
|
|
212
|
+
# Log profiling information if logger is available
|
|
213
|
+
if self._logger:
|
|
214
|
+
self._logger.debug(
|
|
215
|
+
f"merge_bundle_without_storage profiling: "
|
|
216
|
+
f"total={profiling['total_time']:.3f}s, "
|
|
217
|
+
f"build_url={profiling['build_url']:.3f}s, "
|
|
218
|
+
f"get_token={profiling['get_access_token']:.3f}s, "
|
|
219
|
+
f"prepare_payload={profiling['prepare_payload']:.3f}s, "
|
|
220
|
+
f"http_post={profiling['http_post']:.3f}s, "
|
|
221
|
+
f"parse_response={profiling['parse_response']:.3f}s, "
|
|
222
|
+
f"create_objects={profiling['create_response_objects']:.3f}s"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return FhirMergeResourceResponse(
|
|
226
|
+
request_id=request_id,
|
|
227
|
+
url=resource_uri.url,
|
|
228
|
+
responses=response_entries,
|
|
229
|
+
error=None if response_status == 200 else (response_text or f"HTTP {response_status}"),
|
|
230
|
+
access_token=self._access_token,
|
|
231
|
+
status=response_status,
|
|
232
|
+
response_text=response_text,
|
|
233
|
+
)
|
|
234
|
+
|
|
48
235
|
async def merge_bundle_async(
|
|
49
236
|
self,
|
|
50
237
|
id_: str | None,
|
|
@@ -112,7 +299,8 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
112
299
|
response_text: str | None = None
|
|
113
300
|
try:
|
|
114
301
|
async with RetryableAioHttpClient(
|
|
115
|
-
fn_get_session=
|
|
302
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
303
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
116
304
|
refresh_token_func=self._refresh_token_function,
|
|
117
305
|
tracer_request_func=self._trace_request_function,
|
|
118
306
|
retries=self._retry_count,
|
|
@@ -124,9 +312,6 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
124
312
|
log_all_url_results=self._log_all_response_urls,
|
|
125
313
|
access_token=self._access_token,
|
|
126
314
|
access_token_expiry_date=self._access_token_expiry_date,
|
|
127
|
-
persistent_session=self._persistent_session,
|
|
128
|
-
use_persistent_session=self._use_persistent_session,
|
|
129
|
-
close_session_on_exit=self._close_session,
|
|
130
315
|
) as client:
|
|
131
316
|
# should we check if it exists and do a POST then?
|
|
132
317
|
response: RetryableAioHttpResponse = await client.post(
|
|
@@ -136,7 +321,7 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
136
321
|
)
|
|
137
322
|
response_status = response.status
|
|
138
323
|
request_id = response.response_headers.get("X-Request-ID", None)
|
|
139
|
-
self._internal_logger.
|
|
324
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
140
325
|
if response and response.status == 200:
|
|
141
326
|
response_text = await response.get_text_async()
|
|
142
327
|
if response_text:
|
|
@@ -331,7 +516,8 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
331
516
|
response_text: str | None = None
|
|
332
517
|
try:
|
|
333
518
|
async with RetryableAioHttpClient(
|
|
334
|
-
fn_get_session=
|
|
519
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
520
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
335
521
|
refresh_token_func=self._refresh_token_function,
|
|
336
522
|
tracer_request_func=self._trace_request_function,
|
|
337
523
|
retries=self._retry_count,
|
|
@@ -343,9 +529,6 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
343
529
|
log_all_url_results=self._log_all_response_urls,
|
|
344
530
|
access_token=self._access_token,
|
|
345
531
|
access_token_expiry_date=self._access_token_expiry_date,
|
|
346
|
-
close_session_on_exit=self._close_session,
|
|
347
|
-
persistent_session=self._persistent_session,
|
|
348
|
-
use_persistent_session=self._use_persistent_session,
|
|
349
532
|
) as client:
|
|
350
533
|
# should we check if it exists and do a POST then?
|
|
351
534
|
response: RetryableAioHttpResponse = await client.post(
|
|
@@ -355,7 +538,7 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
355
538
|
)
|
|
356
539
|
response_status = response.status
|
|
357
540
|
request_id = response.response_headers.get("X-Request-ID", None)
|
|
358
|
-
self._internal_logger.
|
|
541
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
359
542
|
if response and response.status == 200:
|
|
360
543
|
response_text = await response.get_text_async()
|
|
361
544
|
if response_text:
|
|
@@ -494,11 +677,12 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
494
677
|
access_token: str | None = access_token_result.access_token
|
|
495
678
|
|
|
496
679
|
await AsyncFhirValidator.validate_fhir_resource(
|
|
497
|
-
fn_get_session=
|
|
680
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
498
681
|
json_data=resource.json(),
|
|
499
682
|
resource_name=cast(str | None, resource.get("resourceType")) or self._resource or "",
|
|
500
683
|
validation_server_url=self._validation_server_url,
|
|
501
684
|
access_token=access_token,
|
|
685
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
502
686
|
)
|
|
503
687
|
resource_json_list_clean.append(resource)
|
|
504
688
|
except FhirValidationException as e:
|
|
@@ -519,11 +703,12 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
519
703
|
try:
|
|
520
704
|
with resource.transaction():
|
|
521
705
|
await AsyncFhirValidator.validate_fhir_resource(
|
|
522
|
-
fn_get_session=
|
|
706
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
523
707
|
json_data=resource.json(),
|
|
524
708
|
resource_name=resource.get("resourceType") or self._resource or "",
|
|
525
709
|
validation_server_url=self._validation_server_url,
|
|
526
710
|
access_token=access_token1,
|
|
711
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
527
712
|
)
|
|
528
713
|
resource_json_list_clean.append(resource)
|
|
529
714
|
except FhirValidationException as e:
|
|
@@ -2,8 +2,12 @@ import json
|
|
|
2
2
|
import time
|
|
3
3
|
|
|
4
4
|
from furl import furl
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from opentelemetry.trace import Status, StatusCode
|
|
5
7
|
|
|
6
8
|
from helix_fhir_client_sdk.exceptions.fhir_sender_exception import FhirSenderException
|
|
9
|
+
from helix_fhir_client_sdk.open_telemetry.attribute_names import FhirClientSdkOpenTelemetryAttributeNames
|
|
10
|
+
from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
|
|
7
11
|
from helix_fhir_client_sdk.responses.fhir_client_protocol import FhirClientProtocol
|
|
8
12
|
from helix_fhir_client_sdk.responses.fhir_update_response import FhirUpdateResponse
|
|
9
13
|
from helix_fhir_client_sdk.structures.get_access_token_result import (
|
|
@@ -18,6 +22,8 @@ from helix_fhir_client_sdk.utilities.retryable_aiohttp_response import (
|
|
|
18
22
|
RetryableAioHttpResponse,
|
|
19
23
|
)
|
|
20
24
|
|
|
25
|
+
TRACER = trace.get_tracer(__name__)
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
class FhirPatchMixin(FhirClientProtocol):
|
|
23
29
|
async def send_patch_request_async(self, data: str) -> FhirUpdateResponse:
|
|
@@ -33,96 +39,103 @@ class FhirPatchMixin(FhirClientProtocol):
|
|
|
33
39
|
raise ValueError("update should have only one id")
|
|
34
40
|
if not self._resource:
|
|
35
41
|
raise ValueError("update requires a FHIR resource type")
|
|
36
|
-
self._internal_logger.debug(
|
|
37
|
-
f"Calling patch method on {self._url} with client_id={self._client_id} and scopes={self._auth_scopes}"
|
|
38
|
-
)
|
|
39
|
-
full_uri: furl = furl(self._url)
|
|
40
|
-
full_uri /= self._resource
|
|
41
|
-
full_uri /= self._id
|
|
42
|
-
request_id: str | None = None
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.PATCH) as span:
|
|
44
|
+
span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.URL, self._url or "")
|
|
45
|
+
span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.RESOURCE, self._resource or "")
|
|
46
|
+
try:
|
|
47
|
+
self._internal_logger.debug(
|
|
48
|
+
f"Calling patch method on {self._url} with client_id={self._client_id} and scopes={self._auth_scopes}"
|
|
49
|
+
)
|
|
50
|
+
full_uri: furl = furl(self._url)
|
|
51
|
+
full_uri /= self._resource
|
|
52
|
+
full_uri /= self._id
|
|
53
|
+
request_id: str | None = None
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
headers = {"Content-Type": "application/json-patch+json"}
|
|
48
|
-
headers.update(self._additional_request_headers)
|
|
49
|
-
self._internal_logger.debug(f"Request headers: {headers}")
|
|
50
|
-
access_token_result: GetAccessTokenResult = await self.get_access_token_async()
|
|
51
|
-
access_token: str | None = access_token_result.access_token
|
|
52
|
-
# set access token in request if present
|
|
53
|
-
if access_token:
|
|
54
|
-
headers["Authorization"] = f"Bearer {access_token}"
|
|
55
|
+
start_time: float = time.time()
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
# Set up headers
|
|
58
|
+
headers = {"Content-Type": "application/json-patch+json"}
|
|
59
|
+
headers.update(self._additional_request_headers)
|
|
60
|
+
self._internal_logger.debug(f"Request headers: {headers}")
|
|
61
|
+
access_token_result: GetAccessTokenResult = await self.get_access_token_async()
|
|
62
|
+
access_token: str | None = access_token_result.access_token
|
|
63
|
+
# set access token in request if present
|
|
64
|
+
if access_token:
|
|
65
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# actually make the request
|
|
62
|
-
async with RetryableAioHttpClient(
|
|
63
|
-
fn_get_session=lambda: self.create_http_session(),
|
|
64
|
-
refresh_token_func=self._refresh_token_function,
|
|
65
|
-
tracer_request_func=self._trace_request_function,
|
|
66
|
-
retries=self._retry_count,
|
|
67
|
-
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
68
|
-
use_data_streaming=self._use_data_streaming,
|
|
69
|
-
send_data_as_chunked=self._send_data_as_chunked,
|
|
70
|
-
compress=self._compress,
|
|
71
|
-
throw_exception_on_error=self._throw_exception_on_error,
|
|
72
|
-
log_all_url_results=self._log_all_response_urls,
|
|
73
|
-
access_token=self._access_token,
|
|
74
|
-
access_token_expiry_date=self._access_token_expiry_date,
|
|
75
|
-
persistent_session=self._persistent_session,
|
|
76
|
-
use_persistent_session=self._use_persistent_session,
|
|
77
|
-
close_session_on_exit=self._close_session,
|
|
78
|
-
) as client:
|
|
79
|
-
response: RetryableAioHttpResponse = await client.patch(
|
|
80
|
-
url=full_uri.url, json=deserialized_data, headers=headers
|
|
81
|
-
)
|
|
82
|
-
response_status = response.status
|
|
83
|
-
response_text = await response.get_text_async()
|
|
84
|
-
request_id = response.response_headers.get("X-Request-ID", None)
|
|
85
|
-
self._internal_logger.info(f"X-Request-ID={request_id}")
|
|
67
|
+
response_text: str | None = None
|
|
68
|
+
response_status: int | None = None
|
|
86
69
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
self.
|
|
70
|
+
try:
|
|
71
|
+
deserialized_data = json.loads(data)
|
|
72
|
+
# actually make the request
|
|
73
|
+
async with RetryableAioHttpClient(
|
|
74
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
75
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
76
|
+
refresh_token_func=self._refresh_token_function,
|
|
77
|
+
tracer_request_func=self._trace_request_function,
|
|
78
|
+
retries=self._retry_count,
|
|
79
|
+
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
80
|
+
use_data_streaming=self._use_data_streaming,
|
|
81
|
+
send_data_as_chunked=self._send_data_as_chunked,
|
|
82
|
+
compress=self._compress,
|
|
83
|
+
throw_exception_on_error=self._throw_exception_on_error,
|
|
84
|
+
log_all_url_results=self._log_all_response_urls,
|
|
85
|
+
access_token=self._access_token,
|
|
86
|
+
access_token_expiry_date=self._access_token_expiry_date,
|
|
87
|
+
) as client:
|
|
88
|
+
response: RetryableAioHttpResponse = await client.patch(
|
|
89
|
+
url=full_uri.url, json=deserialized_data, headers=headers
|
|
90
|
+
)
|
|
91
|
+
response_status = response.status
|
|
92
|
+
response_text = await response.get_text_async()
|
|
93
|
+
request_id = response.response_headers.get("X-Request-ID", None)
|
|
94
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
95
|
+
|
|
96
|
+
if response_status == 200:
|
|
97
|
+
if self._logger:
|
|
98
|
+
self._logger.info(f"Successfully updated: {full_uri}")
|
|
99
|
+
elif response_status == 404:
|
|
100
|
+
if self._logger:
|
|
101
|
+
self._logger.info(f"Request resource was not found: {full_uri}")
|
|
102
|
+
else:
|
|
103
|
+
# other HTTP errors
|
|
104
|
+
self._internal_logger.info(f"PATCH response for {full_uri.url}: {response_status}")
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise FhirSenderException(
|
|
107
|
+
request_id=request_id,
|
|
108
|
+
url=full_uri.url,
|
|
109
|
+
headers=headers,
|
|
110
|
+
json_data=data,
|
|
111
|
+
response_text=response_text,
|
|
112
|
+
response_status_code=response_status,
|
|
113
|
+
exception=e,
|
|
114
|
+
variables=FhirClientLogger.get_variables_to_log(vars(self)),
|
|
115
|
+
message=f"Error: {e}",
|
|
116
|
+
elapsed_time=time.time() - start_time,
|
|
117
|
+
) from e
|
|
118
|
+
# check if response is json
|
|
119
|
+
if response_text:
|
|
120
|
+
try:
|
|
121
|
+
responses = json.loads(response_text)
|
|
122
|
+
except ValueError as e:
|
|
123
|
+
responses = {"issue": str(e)}
|
|
93
124
|
else:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
) from e
|
|
109
|
-
# check if response is json
|
|
110
|
-
if response_text:
|
|
111
|
-
try:
|
|
112
|
-
responses = json.loads(response_text)
|
|
113
|
-
except ValueError as e:
|
|
114
|
-
responses = {"issue": str(e)}
|
|
115
|
-
else:
|
|
116
|
-
responses = {}
|
|
117
|
-
return FhirUpdateResponse(
|
|
118
|
-
request_id=request_id,
|
|
119
|
-
url=full_uri.tostr(),
|
|
120
|
-
responses=json.dumps(responses),
|
|
121
|
-
error=json.dumps(responses),
|
|
122
|
-
access_token=access_token,
|
|
123
|
-
status=response_status if response_status else 500,
|
|
124
|
-
resource_type=self._resource,
|
|
125
|
-
)
|
|
125
|
+
responses = {}
|
|
126
|
+
return FhirUpdateResponse(
|
|
127
|
+
request_id=request_id,
|
|
128
|
+
url=full_uri.tostr(),
|
|
129
|
+
responses=json.dumps(responses),
|
|
130
|
+
error=json.dumps(responses),
|
|
131
|
+
access_token=access_token,
|
|
132
|
+
status=response_status if response_status else 500,
|
|
133
|
+
resource_type=self._resource,
|
|
134
|
+
)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
span.record_exception(e)
|
|
137
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
138
|
+
raise
|
|
126
139
|
|
|
127
140
|
def send_patch_request(self, data: str) -> FhirUpdateResponse:
|
|
128
141
|
"""
|
|
@@ -3,7 +3,11 @@ from collections.abc import AsyncGenerator
|
|
|
3
3
|
from compressedfhir.fhir.fhir_resource import FhirResource
|
|
4
4
|
from compressedfhir.fhir.fhir_resource_list import FhirResourceList
|
|
5
5
|
from furl import furl
|
|
6
|
+
from opentelemetry import trace
|
|
7
|
+
from opentelemetry.trace import Status, StatusCode
|
|
6
8
|
|
|
9
|
+
from helix_fhir_client_sdk.open_telemetry.attribute_names import FhirClientSdkOpenTelemetryAttributeNames
|
|
10
|
+
from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
|
|
7
11
|
from helix_fhir_client_sdk.responses.fhir_client_protocol import FhirClientProtocol
|
|
8
12
|
from helix_fhir_client_sdk.responses.fhir_update_response import FhirUpdateResponse
|
|
9
13
|
from helix_fhir_client_sdk.structures.get_access_token_result import (
|
|
@@ -15,6 +19,8 @@ from helix_fhir_client_sdk.utilities.retryable_aiohttp_client import (
|
|
|
15
19
|
)
|
|
16
20
|
from helix_fhir_client_sdk.validators.async_fhir_validator import AsyncFhirValidator
|
|
17
21
|
|
|
22
|
+
TRACER = trace.get_tracer(__name__)
|
|
23
|
+
|
|
18
24
|
|
|
19
25
|
class FhirUpdateMixin(FhirClientProtocol):
|
|
20
26
|
async def update_single_resource_async(self, *, resource: FhirResource) -> FhirUpdateResponse:
|
|
@@ -69,63 +75,71 @@ class FhirUpdateMixin(FhirClientProtocol):
|
|
|
69
75
|
raise ValueError("update should have only one id")
|
|
70
76
|
if not self._resource:
|
|
71
77
|
raise ValueError("update requires a FHIR resource type")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
78
|
+
|
|
79
|
+
with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.UPDATE) as span:
|
|
80
|
+
span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.URL, self._url or "")
|
|
81
|
+
span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.RESOURCE, self._resource or "")
|
|
82
|
+
try:
|
|
83
|
+
full_uri: furl = furl(self._url)
|
|
84
|
+
full_uri /= self._resource
|
|
85
|
+
full_uri /= id_ or self._id
|
|
86
|
+
# set up headers
|
|
87
|
+
headers = {"Content-Type": "application/fhir+json"}
|
|
88
|
+
headers.update(self._additional_request_headers)
|
|
89
|
+
self._internal_logger.debug(f"Request headers: {headers}")
|
|
90
|
+
|
|
91
|
+
access_token_result: GetAccessTokenResult = await self.get_access_token_async()
|
|
92
|
+
access_token: str | None = access_token_result.access_token
|
|
93
|
+
# set access token in request if present
|
|
94
|
+
if access_token:
|
|
95
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
96
|
+
|
|
97
|
+
if self._validation_server_url:
|
|
98
|
+
await AsyncFhirValidator.validate_fhir_resource(
|
|
99
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
100
|
+
json_data=json_data,
|
|
101
|
+
resource_name=self._resource,
|
|
102
|
+
validation_server_url=self._validation_server_url,
|
|
103
|
+
access_token=access_token,
|
|
104
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# actually make the request
|
|
108
|
+
async with RetryableAioHttpClient(
|
|
109
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
110
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
111
|
+
refresh_token_func=self._refresh_token_function,
|
|
112
|
+
tracer_request_func=self._trace_request_function,
|
|
113
|
+
retries=self._retry_count,
|
|
114
|
+
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
115
|
+
use_data_streaming=self._use_data_streaming,
|
|
116
|
+
send_data_as_chunked=self._send_data_as_chunked,
|
|
117
|
+
compress=self._compress,
|
|
118
|
+
throw_exception_on_error=self._throw_exception_on_error,
|
|
119
|
+
log_all_url_results=self._log_all_response_urls,
|
|
120
|
+
access_token=self._access_token,
|
|
121
|
+
access_token_expiry_date=self._access_token_expiry_date,
|
|
122
|
+
) as client:
|
|
123
|
+
response = await client.put(url=full_uri.url, data=json_data, headers=headers)
|
|
124
|
+
request_id = response.response_headers.get("X-Request-ID", None)
|
|
125
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
126
|
+
if response.status == 200:
|
|
127
|
+
if self._logger:
|
|
128
|
+
self._logger.info(f"Successfully updated: {full_uri}")
|
|
129
|
+
|
|
130
|
+
return FhirUpdateResponse(
|
|
131
|
+
request_id=request_id,
|
|
132
|
+
url=full_uri.tostr(),
|
|
133
|
+
responses=await response.get_text_async(),
|
|
134
|
+
error=f"{response.status}" if not response.status == 200 else None,
|
|
135
|
+
access_token=access_token,
|
|
136
|
+
status=response.status,
|
|
137
|
+
resource_type=self._resource,
|
|
138
|
+
)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
span.record_exception(e)
|
|
141
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
142
|
+
raise
|
|
129
143
|
|
|
130
144
|
def update(self, json_data: str) -> FhirUpdateResponse:
|
|
131
145
|
"""
|