helix.fhir.client.sdk 4.1.67__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/dictionary_parser.py +4 -0
- helix_fhir_client_sdk/fhir_auth_mixin.py +17 -10
- helix_fhir_client_sdk/fhir_client.py +161 -61
- helix_fhir_client_sdk/fhir_delete_mixin.py +62 -45
- helix_fhir_client_sdk/fhir_merge_mixin.py +188 -163
- helix_fhir_client_sdk/fhir_merge_resources_mixin.py +200 -9
- helix_fhir_client_sdk/fhir_patch_mixin.py +97 -81
- helix_fhir_client_sdk/fhir_update_mixin.py +71 -54
- helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py +5 -174
- 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 +46 -119
- helix_fhir_client_sdk/responses/fhir_client_protocol.py +9 -1
- helix_fhir_client_sdk/responses/fhir_response_processor.py +73 -54
- helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +0 -2
- helix_fhir_client_sdk/responses/merge/fhir_merge_resource_response_entry.py +30 -0
- helix_fhir_client_sdk/utilities/async_parallel_processor/v1/async_parallel_processor.py +1 -24
- helix_fhir_client_sdk/utilities/cache/request_cache.py +32 -43
- helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py +184 -144
- helix_fhir_client_sdk/utilities/retryable_aiohttp_response.py +2 -1
- helix_fhir_client_sdk/utilities/url_checker.py +46 -12
- 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.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/RECORD +32 -25
- 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.1.67.dist-info/METADATA +0 -115
- {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/WHEEL +0 -0
- {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/licenses/LICENSE +0 -0
- {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.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,
|
|
@@ -133,7 +321,7 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
133
321
|
)
|
|
134
322
|
response_status = response.status
|
|
135
323
|
request_id = response.response_headers.get("X-Request-ID", None)
|
|
136
|
-
self._internal_logger.
|
|
324
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
137
325
|
if response and response.status == 200:
|
|
138
326
|
response_text = await response.get_text_async()
|
|
139
327
|
if response_text:
|
|
@@ -328,7 +516,8 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
328
516
|
response_text: str | None = None
|
|
329
517
|
try:
|
|
330
518
|
async with RetryableAioHttpClient(
|
|
331
|
-
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,
|
|
332
521
|
refresh_token_func=self._refresh_token_function,
|
|
333
522
|
tracer_request_func=self._trace_request_function,
|
|
334
523
|
retries=self._retry_count,
|
|
@@ -349,7 +538,7 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
349
538
|
)
|
|
350
539
|
response_status = response.status
|
|
351
540
|
request_id = response.response_headers.get("X-Request-ID", None)
|
|
352
|
-
self._internal_logger.
|
|
541
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
353
542
|
if response and response.status == 200:
|
|
354
543
|
response_text = await response.get_text_async()
|
|
355
544
|
if response_text:
|
|
@@ -488,11 +677,12 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
488
677
|
access_token: str | None = access_token_result.access_token
|
|
489
678
|
|
|
490
679
|
await AsyncFhirValidator.validate_fhir_resource(
|
|
491
|
-
fn_get_session=
|
|
680
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
492
681
|
json_data=resource.json(),
|
|
493
682
|
resource_name=cast(str | None, resource.get("resourceType")) or self._resource or "",
|
|
494
683
|
validation_server_url=self._validation_server_url,
|
|
495
684
|
access_token=access_token,
|
|
685
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
496
686
|
)
|
|
497
687
|
resource_json_list_clean.append(resource)
|
|
498
688
|
except FhirValidationException as e:
|
|
@@ -513,11 +703,12 @@ class FhirMergeResourcesMixin(FhirClientProtocol):
|
|
|
513
703
|
try:
|
|
514
704
|
with resource.transaction():
|
|
515
705
|
await AsyncFhirValidator.validate_fhir_resource(
|
|
516
|
-
fn_get_session=
|
|
706
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
517
707
|
json_data=resource.json(),
|
|
518
708
|
resource_name=resource.get("resourceType") or self._resource or "",
|
|
519
709
|
validation_server_url=self._validation_server_url,
|
|
520
710
|
access_token=access_token1,
|
|
711
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
521
712
|
)
|
|
522
713
|
resource_json_list_clean.append(resource)
|
|
523
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,93 +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
|
-
) as client:
|
|
76
|
-
response: RetryableAioHttpResponse = await client.patch(
|
|
77
|
-
url=full_uri.url, json=deserialized_data, headers=headers
|
|
78
|
-
)
|
|
79
|
-
response_status = response.status
|
|
80
|
-
response_text = await response.get_text_async()
|
|
81
|
-
request_id = response.response_headers.get("X-Request-ID", None)
|
|
82
|
-
self._internal_logger.info(f"X-Request-ID={request_id}")
|
|
67
|
+
response_text: str | None = None
|
|
68
|
+
response_status: int | None = None
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
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)}
|
|
90
124
|
else:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
) from e
|
|
106
|
-
# check if response is json
|
|
107
|
-
if response_text:
|
|
108
|
-
try:
|
|
109
|
-
responses = json.loads(response_text)
|
|
110
|
-
except ValueError as e:
|
|
111
|
-
responses = {"issue": str(e)}
|
|
112
|
-
else:
|
|
113
|
-
responses = {}
|
|
114
|
-
return FhirUpdateResponse(
|
|
115
|
-
request_id=request_id,
|
|
116
|
-
url=full_uri.tostr(),
|
|
117
|
-
responses=json.dumps(responses),
|
|
118
|
-
error=json.dumps(responses),
|
|
119
|
-
access_token=access_token,
|
|
120
|
-
status=response_status if response_status else 500,
|
|
121
|
-
resource_type=self._resource,
|
|
122
|
-
)
|
|
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
|
|
123
139
|
|
|
124
140
|
def send_patch_request(self, data: str) -> FhirUpdateResponse:
|
|
125
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,60 +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
|
-
self.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
126
143
|
|
|
127
144
|
def update(self, json_data: str) -> FhirUpdateResponse:
|
|
128
145
|
"""
|