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.
Files changed (33) hide show
  1. helix_fhir_client_sdk/dictionary_parser.py +4 -0
  2. helix_fhir_client_sdk/fhir_auth_mixin.py +17 -10
  3. helix_fhir_client_sdk/fhir_client.py +161 -61
  4. helix_fhir_client_sdk/fhir_delete_mixin.py +62 -45
  5. helix_fhir_client_sdk/fhir_merge_mixin.py +188 -163
  6. helix_fhir_client_sdk/fhir_merge_resources_mixin.py +200 -9
  7. helix_fhir_client_sdk/fhir_patch_mixin.py +97 -81
  8. helix_fhir_client_sdk/fhir_update_mixin.py +71 -54
  9. helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py +5 -174
  10. helix_fhir_client_sdk/open_telemetry/__init__.py +0 -0
  11. helix_fhir_client_sdk/open_telemetry/attribute_names.py +7 -0
  12. helix_fhir_client_sdk/open_telemetry/span_names.py +12 -0
  13. helix_fhir_client_sdk/queue/request_queue_mixin.py +46 -119
  14. helix_fhir_client_sdk/responses/fhir_client_protocol.py +9 -1
  15. helix_fhir_client_sdk/responses/fhir_response_processor.py +73 -54
  16. helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +0 -2
  17. helix_fhir_client_sdk/responses/merge/fhir_merge_resource_response_entry.py +30 -0
  18. helix_fhir_client_sdk/utilities/async_parallel_processor/v1/async_parallel_processor.py +1 -24
  19. helix_fhir_client_sdk/utilities/cache/request_cache.py +32 -43
  20. helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py +184 -144
  21. helix_fhir_client_sdk/utilities/retryable_aiohttp_response.py +2 -1
  22. helix_fhir_client_sdk/utilities/url_checker.py +46 -12
  23. helix_fhir_client_sdk/validators/async_fhir_validator.py +3 -0
  24. helix_fhir_client_sdk-4.2.18.dist-info/METADATA +200 -0
  25. {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/RECORD +32 -25
  26. tests/async/test_benchmark_compress.py +448 -0
  27. tests/async/test_benchmark_merge.py +506 -0
  28. tests/async/test_retryable_client_session_management.py +159 -0
  29. tests/test_fhir_client_clone.py +155 -0
  30. helix_fhir_client_sdk-4.1.67.dist-info/METADATA +0 -115
  31. {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/WHEEL +0 -0
  32. {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/licenses/LICENSE +0 -0
  33. {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=lambda: self.create_http_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.info(f"X-Request-ID={request_id}")
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=lambda: self.create_http_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.info(f"X-Request-ID={request_id}")
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=lambda: self.create_http_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=lambda: self.create_http_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
- start_time: float = time.time()
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
- # Set up headers
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
- response_text: str | None = None
57
- response_status: int | None = None
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
- try:
60
- deserialized_data = json.loads(data)
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
- if response_status == 200:
85
- if self._logger:
86
- self._logger.info(f"Successfully updated: {full_uri}")
87
- elif response_status == 404:
88
- if self._logger:
89
- self._logger.info(f"Request resource was not found: {full_uri}")
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
- # other HTTP errors
92
- self._internal_logger.info(f"PATCH response for {full_uri.url}: {response_status}")
93
- except Exception as e:
94
- raise FhirSenderException(
95
- request_id=request_id,
96
- url=full_uri.url,
97
- headers=headers,
98
- json_data=data,
99
- response_text=response_text,
100
- response_status_code=response_status,
101
- exception=e,
102
- variables=FhirClientLogger.get_variables_to_log(vars(self)),
103
- message=f"Error: {e}",
104
- elapsed_time=time.time() - start_time,
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
- full_uri: furl = furl(self._url)
73
- full_uri /= self._resource
74
- full_uri /= id_ or self._id
75
- # set up headers
76
- headers = {"Content-Type": "application/fhir+json"}
77
- headers.update(self._additional_request_headers)
78
- self._internal_logger.debug(f"Request headers: {headers}")
79
-
80
- access_token_result: GetAccessTokenResult = await self.get_access_token_async()
81
- access_token: str | None = access_token_result.access_token
82
- # set access token in request if present
83
- if access_token:
84
- headers["Authorization"] = f"Bearer {access_token}"
85
-
86
- if self._validation_server_url:
87
- await AsyncFhirValidator.validate_fhir_resource(
88
- fn_get_session=lambda: self.create_http_session(),
89
- json_data=json_data,
90
- resource_name=self._resource,
91
- validation_server_url=self._validation_server_url,
92
- access_token=access_token,
93
- )
94
-
95
- # actually make the request
96
- async with RetryableAioHttpClient(
97
- fn_get_session=lambda: self.create_http_session(),
98
- refresh_token_func=self._refresh_token_function,
99
- tracer_request_func=self._trace_request_function,
100
- retries=self._retry_count,
101
- exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
102
- use_data_streaming=self._use_data_streaming,
103
- send_data_as_chunked=self._send_data_as_chunked,
104
- compress=self._compress,
105
- throw_exception_on_error=self._throw_exception_on_error,
106
- log_all_url_results=self._log_all_response_urls,
107
- access_token=self._access_token,
108
- access_token_expiry_date=self._access_token_expiry_date,
109
- ) as client:
110
- response = await client.put(url=full_uri.url, data=json_data, headers=headers)
111
- request_id = response.response_headers.get("X-Request-ID", None)
112
- self._internal_logger.info(f"X-Request-ID={request_id}")
113
- if response.status == 200:
114
- if self._logger:
115
- self._logger.info(f"Successfully updated: {full_uri}")
116
-
117
- return FhirUpdateResponse(
118
- request_id=request_id,
119
- url=full_uri.tostr(),
120
- responses=await response.get_text_async(),
121
- error=f"{response.status}" if not response.status == 200 else None,
122
- access_token=access_token,
123
- status=response.status,
124
- resource_type=self._resource,
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
  """