helix.fhir.client.sdk 4.2.14__py3-none-any.whl → 4.2.16__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.
@@ -180,7 +180,6 @@ class FhirAuthMixin(FhirClientProtocol):
180
180
  access_token_expiry_date=self._access_token_expiry_date,
181
181
  refresh_token_func=self._refresh_token_function,
182
182
  tracer_request_func=self._trace_request_function,
183
- persistent_session=self._persistent_session,
184
183
  ) as client:
185
184
  if self._auth_wellknown_url:
186
185
  host_name: str = furl(self._auth_wellknown_url).host
@@ -282,7 +281,6 @@ class FhirAuthMixin(FhirClientProtocol):
282
281
  access_token_expiry_date=self._access_token_expiry_date,
283
282
  refresh_token_func=self._refresh_token_function,
284
283
  tracer_request_func=self._trace_request_function,
285
- persistent_session=self._persistent_session,
286
284
  ) as client:
287
285
  response: RetryableAioHttpResponse = await client.post(url=auth_server_url, headers=headers, data=payload)
288
286
  # token = response.text.encode('utf8')
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  import ssl
5
5
  import uuid
6
- from collections.abc import AsyncGenerator
6
+ from collections.abc import AsyncGenerator, Callable
7
7
  from datetime import datetime
8
8
  from logging import Logger
9
9
  from os import environ
@@ -155,6 +155,9 @@ class FhirClient(
155
155
 
156
156
  self._create_operation_outcome_for_error: bool | None = False
157
157
 
158
+ # Optional callable to create HTTP sessions. When set, create_http_session() will use this.
159
+ self._fn_create_http_session: Callable[[], ClientSession] | None = None
160
+
158
161
  def action(self, action: str) -> FhirClient:
159
162
  """
160
163
  Set the action
@@ -477,13 +480,48 @@ class FhirClient(
477
480
  self._throw_exception_on_error = throw_exception_on_error
478
481
  return self
479
482
 
480
- def set_persistent_session(self, session: ClientSession | None) -> FhirClient:
483
+ def use_http_session(self, fn_create_http_session: Callable[[], ClientSession] | None) -> FhirClient:
481
484
  """
482
- Sets the persistent session to use for requests
485
+ Sets a custom callable to create HTTP sessions.
486
+
487
+ When set, create_http_session() will use this callable instead of the default
488
+ implementation. This allows for custom session management, connection pooling,
489
+ or persistent session support.
490
+
491
+ **Important**: When you provide a custom session factory, YOU are responsible
492
+ for managing the session lifecycle, including closing it when done. The SDK
493
+ will NOT automatically close user-provided sessions.
494
+
495
+ Example with a persistent session for connection reuse (~4× performance boost):
496
+
497
+ .. code-block:: python
498
+
499
+ import aiohttp
500
+ from helix_fhir_client_sdk.fhir_client import FhirClient
483
501
 
484
- :param session: persistent session
502
+ # Create persistent session
503
+ session = aiohttp.ClientSession()
504
+
505
+ try:
506
+ # Configure FhirClient to use persistent session
507
+ fhir_client = (
508
+ FhirClient()
509
+ .url("http://fhir.example.com")
510
+ .resource("Patient")
511
+ .use_http_session(lambda: session) # User provides session
512
+ )
513
+
514
+ # Multiple requests reuse the same connection
515
+ response1 = await fhir_client.get_async()
516
+ response2 = await fhir_client.clone().resource("Observation").get_async()
517
+
518
+ finally:
519
+ # User must close the session when done
520
+ await session.close()
521
+
522
+ :param fn_create_http_session: callable that returns a ClientSession, or None to use default
485
523
  """
486
- self._persistent_session = session
524
+ self._fn_create_http_session = fn_create_http_session
487
525
  return self
488
526
 
489
527
  # noinspection PyUnusedLocal
@@ -808,9 +846,16 @@ class FhirClient(
808
846
 
809
847
  def create_http_session(self) -> ClientSession:
810
848
  """
811
- Creates an HTTP Session
849
+ Creates an HTTP Session.
812
850
 
851
+ If a custom session factory was set via use_http_session(), it will be used.
852
+ Otherwise, creates a default aiohttp ClientSession with standard configuration.
813
853
  """
854
+ # Use a custom session factory if provided
855
+ if self._fn_create_http_session is not None:
856
+ return self._fn_create_http_session()
857
+
858
+ # Default implementation
814
859
  trace_config = aiohttp.TraceConfig()
815
860
  # trace_config.on_request_start.append(on_request_start)
816
861
  if self._log_level == "DEBUG":
@@ -907,6 +952,7 @@ class FhirClient(
907
952
  fhir_client._trace_request_function = self._trace_request_function
908
953
  fhir_client._log_all_response_urls = self._log_all_response_urls
909
954
  fhir_client._create_operation_outcome_for_error = self._create_operation_outcome_for_error
955
+ fhir_client._fn_create_http_session = self._fn_create_http_session
910
956
  if self._max_concurrent_requests is not None:
911
957
  fhir_client.set_max_concurrent_requests(self._max_concurrent_requests)
912
958
  return fhir_client
@@ -1,7 +1,11 @@
1
1
  import json
2
2
 
3
3
  from furl import furl
4
+ from opentelemetry import trace
5
+ from opentelemetry.trace import Status, StatusCode
4
6
 
7
+ from helix_fhir_client_sdk.open_telemetry.attribute_names import FhirClientSdkOpenTelemetryAttributeNames
8
+ from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
5
9
  from helix_fhir_client_sdk.responses.fhir_client_protocol import FhirClientProtocol
6
10
  from helix_fhir_client_sdk.responses.fhir_delete_response import FhirDeleteResponse
7
11
  from helix_fhir_client_sdk.structures.get_access_token_result import (
@@ -15,6 +19,8 @@ from helix_fhir_client_sdk.utilities.retryable_aiohttp_response import (
15
19
  RetryableAioHttpResponse,
16
20
  )
17
21
 
22
+ TRACER = trace.get_tracer(__name__)
23
+
18
24
 
19
25
  class FhirDeleteMixin(FhirClientProtocol):
20
26
  async def delete_async(self) -> FhirDeleteResponse:
@@ -29,51 +35,59 @@ class FhirDeleteMixin(FhirClientProtocol):
29
35
  raise ValueError("delete requires the ID of FHIR object to delete")
30
36
  if not self._resource:
31
37
  raise ValueError("delete requires a FHIR resource type")
32
- full_uri: furl = furl(self._url)
33
- full_uri /= self._resource
34
- full_uri /= id_list
35
- # setup retry
36
- # set up headers
37
- headers: dict[str, str] = {}
38
- headers.update(self._additional_request_headers)
39
- self._internal_logger.debug(f"Request headers: {headers}")
40
-
41
- access_token_result: GetAccessTokenResult = await self.get_access_token_async()
42
- access_token: str | None = access_token_result.access_token
43
- # set access token in request if present
44
- if access_token:
45
- headers["Authorization"] = f"Bearer {access_token}"
46
-
47
- async with RetryableAioHttpClient(
48
- fn_get_session=lambda: self.create_http_session(),
49
- refresh_token_func=self._refresh_token_function,
50
- retries=self._retry_count,
51
- exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
52
- use_data_streaming=self._use_data_streaming,
53
- compress=False,
54
- throw_exception_on_error=self._throw_exception_on_error,
55
- log_all_url_results=self._log_all_response_urls,
56
- access_token=self._access_token,
57
- access_token_expiry_date=self._access_token_expiry_date,
58
- tracer_request_func=self._trace_request_function,
59
- persistent_session=self._persistent_session,
60
- ) as client:
61
- response: RetryableAioHttpResponse = await client.delete(url=full_uri.tostr(), headers=headers)
62
- request_id = response.response_headers.get("X-Request-ID", None)
63
- self._internal_logger.debug(f"X-Request-ID={request_id}")
64
- if response.status == 200:
65
- if self._logger:
66
- self._logger.info(f"Successfully deleted: {full_uri}")
67
38
 
68
- return FhirDeleteResponse(
69
- request_id=request_id,
70
- url=full_uri.tostr(),
71
- responses=await response.get_text_async(),
72
- error=f"{response.status}" if not response.status == 200 else None,
73
- access_token=access_token,
74
- status=response.status,
75
- resource_type=self._resource,
76
- )
39
+ with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.DELETE) as span:
40
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.URL, self._url or "")
41
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.RESOURCE, self._resource or "")
42
+ try:
43
+ full_uri: furl = furl(self._url)
44
+ full_uri /= self._resource
45
+ full_uri /= id_list
46
+ # setup retry
47
+ # set up headers
48
+ headers: dict[str, str] = {}
49
+ headers.update(self._additional_request_headers)
50
+ self._internal_logger.debug(f"Request headers: {headers}")
51
+
52
+ access_token_result: GetAccessTokenResult = await self.get_access_token_async()
53
+ access_token: str | None = access_token_result.access_token
54
+ # set access token in request if present
55
+ if access_token:
56
+ headers["Authorization"] = f"Bearer {access_token}"
57
+
58
+ async with RetryableAioHttpClient(
59
+ fn_get_session=lambda: self.create_http_session(),
60
+ refresh_token_func=self._refresh_token_function,
61
+ retries=self._retry_count,
62
+ exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
63
+ use_data_streaming=self._use_data_streaming,
64
+ compress=False,
65
+ throw_exception_on_error=self._throw_exception_on_error,
66
+ log_all_url_results=self._log_all_response_urls,
67
+ access_token=self._access_token,
68
+ access_token_expiry_date=self._access_token_expiry_date,
69
+ tracer_request_func=self._trace_request_function,
70
+ ) as client:
71
+ response: RetryableAioHttpResponse = await client.delete(url=full_uri.tostr(), headers=headers)
72
+ request_id = response.response_headers.get("X-Request-ID", None)
73
+ self._internal_logger.debug(f"X-Request-ID={request_id}")
74
+ if response.status == 200:
75
+ if self._logger:
76
+ self._logger.info(f"Successfully deleted: {full_uri}")
77
+
78
+ return FhirDeleteResponse(
79
+ request_id=request_id,
80
+ url=full_uri.tostr(),
81
+ responses=await response.get_text_async(),
82
+ error=f"{response.status}" if not response.status == 200 else None,
83
+ access_token=access_token,
84
+ status=response.status,
85
+ resource_type=self._resource,
86
+ )
87
+ except Exception as e:
88
+ span.record_exception(e)
89
+ span.set_status(Status(StatusCode.ERROR, str(e)))
90
+ raise
77
91
 
78
92
  def delete(self) -> FhirDeleteResponse:
79
93
  """
@@ -126,7 +140,6 @@ class FhirDeleteMixin(FhirClientProtocol):
126
140
  access_token=self._access_token,
127
141
  access_token_expiry_date=self._access_token_expiry_date,
128
142
  tracer_request_func=self._trace_request_function,
129
- persistent_session=self._persistent_session,
130
143
  ) as client:
131
144
  response: RetryableAioHttpResponse = await client.delete(url=full_url, headers=headers)
132
145
  request_id = response.response_headers.get("X-Request-ID", None)
@@ -8,12 +8,16 @@ from typing import (
8
8
 
9
9
  import requests
10
10
  from furl import furl
11
+ from opentelemetry import trace
12
+ from opentelemetry.trace import Status, StatusCode
11
13
 
12
14
  from helix_fhir_client_sdk.dictionary_writer import convert_dict_to_str
13
15
  from helix_fhir_client_sdk.exceptions.fhir_sender_exception import FhirSenderException
14
16
  from helix_fhir_client_sdk.exceptions.fhir_validation_exception import (
15
17
  FhirValidationException,
16
18
  )
19
+ from helix_fhir_client_sdk.open_telemetry.attribute_names import FhirClientSdkOpenTelemetryAttributeNames
20
+ from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
17
21
  from helix_fhir_client_sdk.responses.fhir_client_protocol import FhirClientProtocol
18
22
  from helix_fhir_client_sdk.responses.fhir_merge_response import FhirMergeResponse
19
23
  from helix_fhir_client_sdk.structures.get_access_token_result import (
@@ -30,6 +34,8 @@ from helix_fhir_client_sdk.utilities.retryable_aiohttp_response import (
30
34
  )
31
35
  from helix_fhir_client_sdk.validators.async_fhir_validator import AsyncFhirValidator
32
36
 
37
+ TRACER = trace.get_tracer(__name__)
38
+
33
39
 
34
40
  class FhirMergeMixin(FhirClientProtocol):
35
41
  async def validate_content(
@@ -105,183 +111,198 @@ class FhirMergeMixin(FhirClientProtocol):
105
111
  assert self._url, "No FHIR server url was set"
106
112
  assert isinstance(json_data_list, list), "This function requires a list"
107
113
 
108
- self._internal_logger.debug(
109
- f"Calling $merge on {self._url} with client_id={self._client_id} and scopes={self._auth_scopes}"
110
- )
111
- instance_variables_text = convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))
112
- if self._internal_logger:
113
- self._internal_logger.info(f"parameters: {instance_variables_text}")
114
- else:
115
- self._internal_logger.info(f"LOGLEVEL (InternalLogger): {self._log_level}")
116
- self._internal_logger.info(f"parameters: {instance_variables_text}")
114
+ with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.MERGE) as span:
115
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.URL, self._url or "")
116
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.RESOURCE, self._resource or "")
117
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.BATCH_SIZE, batch_size or 0)
118
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.JSON_DATA_COUNT, len(json_data_list))
119
+ try:
120
+ self._internal_logger.debug(
121
+ f"Calling $merge on {self._url} with client_id={self._client_id} and scopes={self._auth_scopes}"
122
+ )
123
+ instance_variables_text = convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))
124
+ if self._internal_logger:
125
+ self._internal_logger.info(f"parameters: {instance_variables_text}")
126
+ else:
127
+ self._internal_logger.info(f"LOGLEVEL (InternalLogger): {self._log_level}")
128
+ self._internal_logger.info(f"parameters: {instance_variables_text}")
117
129
 
118
- request_id: str | None = None
119
- response_status: int | None = None
120
- full_uri: furl = furl(self._url)
121
- assert self._resource
122
- full_uri /= self._resource
123
- headers = {"Content-Type": "application/fhir+json"}
124
- headers.update(self._additional_request_headers)
125
- self._internal_logger.debug(f"Request headers: {headers}")
130
+ request_id: str | None = None
131
+ response_status: int | None = None
132
+ full_uri: furl = furl(self._url)
133
+ assert self._resource
134
+ full_uri /= self._resource
135
+ headers = {"Content-Type": "application/fhir+json"}
136
+ headers.update(self._additional_request_headers)
137
+ self._internal_logger.debug(f"Request headers: {headers}")
126
138
 
127
- responses: list[dict[str, Any]] = []
128
- start_time: float = time.time()
129
- # set access token in request if present
130
- access_token_result: GetAccessTokenResult = await self.get_access_token_async()
131
- access_token: str | None = access_token_result.access_token
132
- if access_token:
133
- headers["Authorization"] = f"Bearer {access_token}"
139
+ responses: list[dict[str, Any]] = []
140
+ start_time: float = time.time()
141
+ # set access token in request if present
142
+ access_token_result: GetAccessTokenResult = await self.get_access_token_async()
143
+ access_token: str | None = access_token_result.access_token
144
+ if access_token:
145
+ headers["Authorization"] = f"Bearer {access_token}"
134
146
 
135
- try:
136
- resource_json_list_incoming: list[dict[str, Any]] = [json.loads(json_data) for json_data in json_data_list]
137
- resource_json_list_clean: list[dict[str, Any]]
138
- errors: list[dict[str, Any]] = []
139
- if self._validation_server_url:
140
- resource_json_list_clean = await self.validate_content(
141
- errors=errors,
142
- resource_json_list_incoming=resource_json_list_incoming,
143
- )
144
- else:
145
- resource_json_list_clean = resource_json_list_incoming
147
+ try:
148
+ resource_json_list_incoming: list[dict[str, Any]] = [
149
+ json.loads(json_data) for json_data in json_data_list
150
+ ]
151
+ resource_json_list_clean: list[dict[str, Any]]
152
+ errors: list[dict[str, Any]] = []
153
+ if self._validation_server_url:
154
+ resource_json_list_clean = await self.validate_content(
155
+ errors=errors,
156
+ resource_json_list_incoming=resource_json_list_incoming,
157
+ )
158
+ else:
159
+ resource_json_list_clean = resource_json_list_incoming
146
160
 
147
- if len(resource_json_list_clean) > 0:
148
- chunks: Generator[list[dict[str, Any]], None, None] = ListChunker.divide_into_chunks(
149
- resource_json_list_clean, chunk_size=batch_size
150
- )
151
- chunk: list[dict[str, Any]]
152
- for chunk in chunks:
153
- resource_uri: furl = full_uri.copy()
154
- # if there is only item in the list then send it instead of having it in a list
155
- json_payload: str = json.dumps(chunk[0]) if len(chunk) == 1 else json.dumps(chunk)
156
- # json_payload_bytes: str = json_payload
157
- obj_id = id_ or 1 # TODO: remove this once the node fhir accepts merge without a parameter
158
- assert obj_id
161
+ if len(resource_json_list_clean) > 0:
162
+ chunks: Generator[list[dict[str, Any]], None, None] = ListChunker.divide_into_chunks(
163
+ resource_json_list_clean, chunk_size=batch_size
164
+ )
165
+ chunk: list[dict[str, Any]]
166
+ for chunk in chunks:
167
+ resource_uri: furl = full_uri.copy()
168
+ # if there is only item in the list then send it instead of having it in a list
169
+ json_payload: str = json.dumps(chunk[0]) if len(chunk) == 1 else json.dumps(chunk)
170
+ # json_payload_bytes: str = json_payload
171
+ obj_id = id_ or 1 # TODO: remove this once the node fhir accepts merge without a parameter
172
+ assert obj_id
159
173
 
160
- if obj_id is not None and str(obj_id).strip():
161
- resource_uri.path.segments.append(str(obj_id))
162
- # Always append $merge
163
- resource_uri.path.segments.append("$merge")
174
+ if obj_id is not None and str(obj_id).strip():
175
+ resource_uri.path.segments.append(str(obj_id))
176
+ # Always append $merge
177
+ resource_uri.path.segments.append("$merge")
164
178
 
165
- # Conditionally add the query parameter
166
- if self._smart_merge is False:
167
- resource_uri.add({"smartMerge": "false"})
179
+ # Conditionally add the query parameter
180
+ if self._smart_merge is False:
181
+ resource_uri.add({"smartMerge": "false"})
168
182
 
169
- response_text: str | None = None
170
- try:
171
- async with RetryableAioHttpClient(
172
- fn_get_session=lambda: self.create_http_session(),
173
- refresh_token_func=self._refresh_token_function,
174
- tracer_request_func=self._trace_request_function,
175
- retries=self._retry_count,
176
- exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
177
- use_data_streaming=self._use_data_streaming,
178
- send_data_as_chunked=self._send_data_as_chunked,
179
- compress=self._compress,
180
- throw_exception_on_error=self._throw_exception_on_error,
181
- log_all_url_results=self._log_all_response_urls,
182
- access_token=self._access_token,
183
- access_token_expiry_date=self._access_token_expiry_date,
184
- persistent_session=self._persistent_session,
185
- ) as client:
186
- # should we check if it exists and do a POST then?
187
- response: RetryableAioHttpResponse = await client.post(
188
- url=resource_uri.url,
189
- data=json_payload,
190
- headers=headers,
191
- )
192
- response_status = response.status
193
- request_id = response.response_headers.get("X-Request-ID", None)
194
- self._internal_logger.debug(f"X-Request-ID={request_id}")
195
- if response and response.status == 200:
196
- response_text = await response.get_text_async()
197
- if response_text:
198
- try:
199
- raw_response: list[dict[str, Any]] | dict[str, Any] = json.loads(response_text)
200
- if isinstance(raw_response, list):
201
- responses = raw_response
183
+ response_text: str | None = None
184
+ try:
185
+ async with RetryableAioHttpClient(
186
+ fn_get_session=lambda: self.create_http_session(),
187
+ refresh_token_func=self._refresh_token_function,
188
+ tracer_request_func=self._trace_request_function,
189
+ retries=self._retry_count,
190
+ exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
191
+ use_data_streaming=self._use_data_streaming,
192
+ send_data_as_chunked=self._send_data_as_chunked,
193
+ compress=self._compress,
194
+ throw_exception_on_error=self._throw_exception_on_error,
195
+ log_all_url_results=self._log_all_response_urls,
196
+ access_token=self._access_token,
197
+ access_token_expiry_date=self._access_token_expiry_date,
198
+ ) as client:
199
+ # should we check if it exists and do a POST then?
200
+ response: RetryableAioHttpResponse = await client.post(
201
+ url=resource_uri.url,
202
+ data=json_payload,
203
+ headers=headers,
204
+ )
205
+ response_status = response.status
206
+ request_id = response.response_headers.get("X-Request-ID", None)
207
+ self._internal_logger.debug(f"X-Request-ID={request_id}")
208
+ if response and response.status == 200:
209
+ response_text = await response.get_text_async()
210
+ if response_text:
211
+ try:
212
+ raw_response: list[dict[str, Any]] | dict[str, Any] = json.loads(
213
+ response_text
214
+ )
215
+ if isinstance(raw_response, list):
216
+ responses = raw_response
217
+ else:
218
+ responses = [raw_response]
219
+ except ValueError as e:
220
+ responses = [{"issue": str(e)}]
202
221
  else:
203
- responses = [raw_response]
204
- except ValueError as e:
205
- responses = [{"issue": str(e)}]
206
- else:
207
- responses = []
208
- yield FhirMergeResponse(
222
+ responses = []
223
+ yield FhirMergeResponse(
224
+ request_id=request_id,
225
+ url=resource_uri.url,
226
+ responses=responses + errors,
227
+ error=(json.dumps(responses + errors) if response_status != 200 else None),
228
+ access_token=self._access_token,
229
+ status=response_status if response_status else 500,
230
+ json_data=json_payload,
231
+ )
232
+ else: # other HTTP errors
233
+ self._internal_logger.info(
234
+ f"POST response for {resource_uri.url}: {response.status}"
235
+ )
236
+ response_text = await response.get_text_async()
237
+ yield FhirMergeResponse(
238
+ request_id=request_id,
239
+ url=resource_uri.url or self._url or "",
240
+ json_data=json_payload,
241
+ responses=[
242
+ {
243
+ "issue": [
244
+ {
245
+ "severity": "error",
246
+ "code": "exception",
247
+ "diagnostics": response_text,
248
+ }
249
+ ]
250
+ }
251
+ ],
252
+ error=(json.dumps(response_text) if response_text else None),
253
+ access_token=self._access_token,
254
+ status=response.status if response.status else 500,
255
+ )
256
+ except requests.exceptions.HTTPError as e:
257
+ raise FhirSenderException(
209
258
  request_id=request_id,
210
259
  url=resource_uri.url,
211
- responses=responses + errors,
212
- error=(json.dumps(responses + errors) if response_status != 200 else None),
213
- access_token=self._access_token,
214
- status=response_status if response_status else 500,
260
+ headers=headers,
215
261
  json_data=json_payload,
216
- )
217
- else: # other HTTP errors
218
- self._internal_logger.info(f"POST response for {resource_uri.url}: {response.status}")
219
- response_text = await response.get_text_async()
220
- yield FhirMergeResponse(
262
+ response_text=response_text,
263
+ response_status_code=response_status,
264
+ exception=e,
265
+ variables=FhirClientLogger.get_variables_to_log(vars(self)),
266
+ message=f"HttpError: {e}",
267
+ elapsed_time=time.time() - start_time,
268
+ ) from e
269
+ except Exception as e:
270
+ raise FhirSenderException(
221
271
  request_id=request_id,
222
- url=resource_uri.url or self._url or "",
272
+ url=resource_uri.url,
273
+ headers=headers,
223
274
  json_data=json_payload,
224
- responses=[
225
- {
226
- "issue": [
227
- {
228
- "severity": "error",
229
- "code": "exception",
230
- "diagnostics": response_text,
231
- }
232
- ]
233
- }
234
- ],
235
- error=(json.dumps(response_text) if response_text else None),
236
- access_token=self._access_token,
237
- status=response.status if response.status else 500,
238
- )
239
- except requests.exceptions.HTTPError as e:
240
- raise FhirSenderException(
275
+ response_text=response_text,
276
+ response_status_code=response_status,
277
+ exception=e,
278
+ variables=FhirClientLogger.get_variables_to_log(vars(self)),
279
+ message=f"Unknown Error: {e}",
280
+ elapsed_time=time.time() - start_time,
281
+ ) from e
282
+ else:
283
+ json_payload = json.dumps(json_data_list)
284
+ yield FhirMergeResponse(
241
285
  request_id=request_id,
242
- url=resource_uri.url,
243
- headers=headers,
244
- json_data=json_payload,
245
- response_text=response_text,
246
- response_status_code=response_status,
247
- exception=e,
248
- variables=FhirClientLogger.get_variables_to_log(vars(self)),
249
- message=f"HttpError: {e}",
250
- elapsed_time=time.time() - start_time,
251
- ) from e
252
- except Exception as e:
253
- raise FhirSenderException(
254
- request_id=request_id,
255
- url=resource_uri.url,
256
- headers=headers,
286
+ url=full_uri.url,
287
+ responses=responses + errors,
288
+ error=(json.dumps(responses + errors) if response_status != 200 else None),
289
+ access_token=self._access_token,
290
+ status=response_status if response_status else 500,
257
291
  json_data=json_payload,
258
- response_text=response_text,
259
- response_status_code=response_status,
260
- exception=e,
261
- variables=FhirClientLogger.get_variables_to_log(vars(self)),
262
- message=f"Unknown Error: {e}",
263
- elapsed_time=time.time() - start_time,
264
- ) from e
265
- else:
266
- json_payload = json.dumps(json_data_list)
267
- yield FhirMergeResponse(
268
- request_id=request_id,
269
- url=full_uri.url,
270
- responses=responses + errors,
271
- error=(json.dumps(responses + errors) if response_status != 200 else None),
272
- access_token=self._access_token,
273
- status=response_status if response_status else 500,
274
- json_data=json_payload,
275
- )
276
- except AssertionError as e:
277
- if self._logger:
278
- self._logger.error(
279
- Exception(
280
- f"Assertion: FHIR send failed: {str(e)} for resource: {json_data_list}. "
281
- + f"variables={convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))}"
282
- )
283
- )
284
- raise e
292
+ )
293
+ except AssertionError as e:
294
+ if self._logger:
295
+ self._logger.error(
296
+ Exception(
297
+ f"Assertion: FHIR send failed: {str(e)} for resource: {json_data_list}. "
298
+ + f"variables={convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))}"
299
+ )
300
+ )
301
+ raise e
302
+ except Exception as e:
303
+ span.record_exception(e)
304
+ span.set_status(Status(StatusCode.ERROR, str(e)))
305
+ raise
285
306
 
286
307
  def merge(
287
308
  self,