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.
Files changed (37) hide show
  1. helix_fhir_client_sdk/fhir_auth_mixin.py +17 -10
  2. helix_fhir_client_sdk/fhir_client.py +152 -79
  3. helix_fhir_client_sdk/fhir_delete_mixin.py +62 -48
  4. helix_fhir_client_sdk/fhir_merge_mixin.py +188 -166
  5. helix_fhir_client_sdk/fhir_merge_resources_mixin.py +200 -15
  6. helix_fhir_client_sdk/fhir_patch_mixin.py +97 -84
  7. helix_fhir_client_sdk/fhir_update_mixin.py +71 -57
  8. helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py +147 -49
  9. helix_fhir_client_sdk/open_telemetry/__init__.py +0 -0
  10. helix_fhir_client_sdk/open_telemetry/attribute_names.py +7 -0
  11. helix_fhir_client_sdk/open_telemetry/span_names.py +12 -0
  12. helix_fhir_client_sdk/queue/request_queue_mixin.py +17 -12
  13. helix_fhir_client_sdk/responses/fhir_client_protocol.py +10 -6
  14. helix_fhir_client_sdk/responses/fhir_get_response.py +3 -4
  15. helix_fhir_client_sdk/responses/fhir_response_processor.py +73 -54
  16. helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +49 -28
  17. helix_fhir_client_sdk/responses/get/fhir_get_error_response.py +0 -1
  18. helix_fhir_client_sdk/responses/get/fhir_get_list_by_resource_type_response.py +1 -1
  19. helix_fhir_client_sdk/responses/get/fhir_get_list_response.py +1 -1
  20. helix_fhir_client_sdk/responses/get/fhir_get_response_factory.py +0 -1
  21. helix_fhir_client_sdk/responses/get/fhir_get_single_response.py +1 -1
  22. helix_fhir_client_sdk/responses/merge/fhir_merge_resource_response_entry.py +30 -0
  23. helix_fhir_client_sdk/responses/resource_separator.py +35 -40
  24. helix_fhir_client_sdk/utilities/cache/request_cache.py +32 -43
  25. helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py +185 -154
  26. helix_fhir_client_sdk/utilities/retryable_aiohttp_response.py +2 -1
  27. helix_fhir_client_sdk/validators/async_fhir_validator.py +3 -0
  28. helix_fhir_client_sdk-4.2.19.dist-info/METADATA +200 -0
  29. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/RECORD +36 -29
  30. tests/async/test_benchmark_compress.py +448 -0
  31. tests/async/test_benchmark_merge.py +506 -0
  32. tests/async/test_retryable_client_session_management.py +159 -0
  33. tests/test_fhir_client_clone.py +155 -0
  34. helix_fhir_client_sdk-4.2.3.dist-info/METADATA +0 -115
  35. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/WHEEL +0 -0
  36. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/licenses/LICENSE +0 -0
  37. {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=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,
@@ -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.info(f"X-Request-ID={request_id}")
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=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,
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.info(f"X-Request-ID={request_id}")
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=lambda: self.create_http_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=lambda: self.create_http_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
- 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
- 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
- if response_status == 200:
88
- if self._logger:
89
- self._logger.info(f"Successfully updated: {full_uri}")
90
- elif response_status == 404:
91
- if self._logger:
92
- 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)}
93
124
  else:
94
- # other HTTP errors
95
- self._internal_logger.info(f"PATCH response for {full_uri.url}: {response_status}")
96
- except Exception as e:
97
- raise FhirSenderException(
98
- request_id=request_id,
99
- url=full_uri.url,
100
- headers=headers,
101
- json_data=data,
102
- response_text=response_text,
103
- response_status_code=response_status,
104
- exception=e,
105
- variables=FhirClientLogger.get_variables_to_log(vars(self)),
106
- message=f"Error: {e}",
107
- elapsed_time=time.time() - start_time,
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
- 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
- persistent_session=self._persistent_session,
110
- use_persistent_session=self._use_persistent_session,
111
- close_session_on_exit=self._close_session,
112
- ) as client:
113
- response = await client.put(url=full_uri.url, data=json_data, headers=headers)
114
- request_id = response.response_headers.get("X-Request-ID", None)
115
- self._internal_logger.info(f"X-Request-ID={request_id}")
116
- if response.status == 200:
117
- if self._logger:
118
- self._logger.info(f"Successfully updated: {full_uri}")
119
-
120
- return FhirUpdateResponse(
121
- request_id=request_id,
122
- url=full_uri.tostr(),
123
- responses=await response.get_text_async(),
124
- error=f"{response.status}" if not response.status == 200 else None,
125
- access_token=access_token,
126
- status=response.status,
127
- resource_type=self._resource,
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
  """