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
@@ -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(
@@ -48,11 +54,12 @@ class FhirMergeMixin(FhirClientProtocol):
48
54
  access_token: str | None = access_token_result.access_token
49
55
 
50
56
  await AsyncFhirValidator.validate_fhir_resource(
51
- fn_get_session=lambda: self.create_http_session(),
57
+ fn_get_session=self._fn_create_http_session or self.create_http_session,
52
58
  json_data=json.dumps(resource_json),
53
59
  resource_name=cast(str | None, resource_json.get("resourceType")) or self._resource or "",
54
60
  validation_server_url=self._validation_server_url,
55
61
  access_token=access_token,
62
+ caller_managed_session=self._fn_create_http_session is not None,
56
63
  )
57
64
  resource_json_list_clean.append(resource_json)
58
65
  except FhirValidationException as e:
@@ -69,11 +76,12 @@ class FhirMergeMixin(FhirClientProtocol):
69
76
  access_token_result1: GetAccessTokenResult = await self.get_access_token_async()
70
77
  access_token1: str | None = access_token_result1.access_token
71
78
  await AsyncFhirValidator.validate_fhir_resource(
72
- fn_get_session=lambda: self.create_http_session(),
79
+ fn_get_session=self._fn_create_http_session or self.create_http_session,
73
80
  json_data=json.dumps(resource_json),
74
81
  resource_name=resource_json.get("resourceType") or self._resource or "",
75
82
  validation_server_url=self._validation_server_url,
76
83
  access_token=access_token1,
84
+ caller_managed_session=self._fn_create_http_session is not None,
77
85
  )
78
86
  resource_json_list_clean.append(resource_json)
79
87
  except FhirValidationException as e:
@@ -105,185 +113,199 @@ class FhirMergeMixin(FhirClientProtocol):
105
113
  assert self._url, "No FHIR server url was set"
106
114
  assert isinstance(json_data_list, list), "This function requires a list"
107
115
 
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}")
116
+ with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.MERGE) as span:
117
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.URL, self._url or "")
118
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.RESOURCE, self._resource or "")
119
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.BATCH_SIZE, batch_size or 0)
120
+ span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.JSON_DATA_COUNT, len(json_data_list))
121
+ try:
122
+ self._internal_logger.debug(
123
+ f"Calling $merge on {self._url} with client_id={self._client_id} and scopes={self._auth_scopes}"
124
+ )
125
+ instance_variables_text = convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))
126
+ if self._internal_logger:
127
+ self._internal_logger.info(f"parameters: {instance_variables_text}")
128
+ else:
129
+ self._internal_logger.info(f"LOGLEVEL (InternalLogger): {self._log_level}")
130
+ self._internal_logger.info(f"parameters: {instance_variables_text}")
117
131
 
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}")
132
+ request_id: str | None = None
133
+ response_status: int | None = None
134
+ full_uri: furl = furl(self._url)
135
+ assert self._resource
136
+ full_uri /= self._resource
137
+ headers = {"Content-Type": "application/fhir+json"}
138
+ headers.update(self._additional_request_headers)
139
+ self._internal_logger.debug(f"Request headers: {headers}")
126
140
 
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}"
141
+ responses: list[dict[str, Any]] = []
142
+ start_time: float = time.time()
143
+ # set access token in request if present
144
+ access_token_result: GetAccessTokenResult = await self.get_access_token_async()
145
+ access_token: str | None = access_token_result.access_token
146
+ if access_token:
147
+ headers["Authorization"] = f"Bearer {access_token}"
134
148
 
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
149
+ try:
150
+ resource_json_list_incoming: list[dict[str, Any]] = [
151
+ json.loads(json_data) for json_data in json_data_list
152
+ ]
153
+ resource_json_list_clean: list[dict[str, Any]]
154
+ errors: list[dict[str, Any]] = []
155
+ if self._validation_server_url:
156
+ resource_json_list_clean = await self.validate_content(
157
+ errors=errors,
158
+ resource_json_list_incoming=resource_json_list_incoming,
159
+ )
160
+ else:
161
+ resource_json_list_clean = resource_json_list_incoming
146
162
 
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
163
+ if len(resource_json_list_clean) > 0:
164
+ chunks: Generator[list[dict[str, Any]], None, None] = ListChunker.divide_into_chunks(
165
+ resource_json_list_clean, chunk_size=batch_size
166
+ )
167
+ chunk: list[dict[str, Any]]
168
+ for chunk in chunks:
169
+ resource_uri: furl = full_uri.copy()
170
+ # if there is only item in the list then send it instead of having it in a list
171
+ json_payload: str = json.dumps(chunk[0]) if len(chunk) == 1 else json.dumps(chunk)
172
+ # json_payload_bytes: str = json_payload
173
+ obj_id = id_ or 1 # TODO: remove this once the node fhir accepts merge without a parameter
174
+ assert obj_id
159
175
 
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")
176
+ if obj_id is not None and str(obj_id).strip():
177
+ resource_uri.path.segments.append(str(obj_id))
178
+ # Always append $merge
179
+ resource_uri.path.segments.append("$merge")
164
180
 
165
- # Conditionally add the query parameter
166
- if self._smart_merge is False:
167
- resource_uri.add({"smartMerge": "false"})
181
+ # Conditionally add the query parameter
182
+ if self._smart_merge is False:
183
+ resource_uri.add({"smartMerge": "false"})
168
184
 
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
- use_persistent_session=self._use_persistent_session,
186
- close_session_on_exit=self._close_session,
187
- ) as client:
188
- # should we check if it exists and do a POST then?
189
- response: RetryableAioHttpResponse = await client.post(
190
- url=resource_uri.url,
191
- data=json_payload,
192
- headers=headers,
193
- )
194
- response_status = response.status
195
- request_id = response.response_headers.get("X-Request-ID", None)
196
- self._internal_logger.info(f"X-Request-ID={request_id}")
197
- if response and response.status == 200:
198
- response_text = await response.get_text_async()
199
- if response_text:
200
- try:
201
- raw_response: list[dict[str, Any]] | dict[str, Any] = json.loads(response_text)
202
- if isinstance(raw_response, list):
203
- responses = raw_response
185
+ response_text: str | None = None
186
+ try:
187
+ async with RetryableAioHttpClient(
188
+ fn_get_session=self._fn_create_http_session or self.create_http_session,
189
+ caller_managed_session=self._fn_create_http_session is not None,
190
+ refresh_token_func=self._refresh_token_function,
191
+ tracer_request_func=self._trace_request_function,
192
+ retries=self._retry_count,
193
+ exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
194
+ use_data_streaming=self._use_data_streaming,
195
+ send_data_as_chunked=self._send_data_as_chunked,
196
+ compress=self._compress,
197
+ throw_exception_on_error=self._throw_exception_on_error,
198
+ log_all_url_results=self._log_all_response_urls,
199
+ access_token=self._access_token,
200
+ access_token_expiry_date=self._access_token_expiry_date,
201
+ ) as client:
202
+ # should we check if it exists and do a POST then?
203
+ response: RetryableAioHttpResponse = await client.post(
204
+ url=resource_uri.url,
205
+ data=json_payload,
206
+ headers=headers,
207
+ )
208
+ response_status = response.status
209
+ request_id = response.response_headers.get("X-Request-ID", None)
210
+ self._internal_logger.debug(f"X-Request-ID={request_id}")
211
+ if response and response.status == 200:
212
+ response_text = await response.get_text_async()
213
+ if response_text:
214
+ try:
215
+ raw_response: list[dict[str, Any]] | dict[str, Any] = json.loads(
216
+ response_text
217
+ )
218
+ if isinstance(raw_response, list):
219
+ responses = raw_response
220
+ else:
221
+ responses = [raw_response]
222
+ except ValueError as e:
223
+ responses = [{"issue": str(e)}]
204
224
  else:
205
- responses = [raw_response]
206
- except ValueError as e:
207
- responses = [{"issue": str(e)}]
208
- else:
209
- responses = []
210
- yield FhirMergeResponse(
225
+ responses = []
226
+ yield FhirMergeResponse(
227
+ request_id=request_id,
228
+ url=resource_uri.url,
229
+ responses=responses + errors,
230
+ error=(json.dumps(responses + errors) if response_status != 200 else None),
231
+ access_token=self._access_token,
232
+ status=response_status if response_status else 500,
233
+ json_data=json_payload,
234
+ )
235
+ else: # other HTTP errors
236
+ self._internal_logger.info(
237
+ f"POST response for {resource_uri.url}: {response.status}"
238
+ )
239
+ response_text = await response.get_text_async()
240
+ yield FhirMergeResponse(
241
+ request_id=request_id,
242
+ url=resource_uri.url or self._url or "",
243
+ json_data=json_payload,
244
+ responses=[
245
+ {
246
+ "issue": [
247
+ {
248
+ "severity": "error",
249
+ "code": "exception",
250
+ "diagnostics": response_text,
251
+ }
252
+ ]
253
+ }
254
+ ],
255
+ error=(json.dumps(response_text) if response_text else None),
256
+ access_token=self._access_token,
257
+ status=response.status if response.status else 500,
258
+ )
259
+ except requests.exceptions.HTTPError as e:
260
+ raise FhirSenderException(
211
261
  request_id=request_id,
212
262
  url=resource_uri.url,
213
- responses=responses + errors,
214
- error=(json.dumps(responses + errors) if response_status != 200 else None),
215
- access_token=self._access_token,
216
- status=response_status if response_status else 500,
263
+ headers=headers,
217
264
  json_data=json_payload,
218
- )
219
- else: # other HTTP errors
220
- self._internal_logger.info(f"POST response for {resource_uri.url}: {response.status}")
221
- response_text = await response.get_text_async()
222
- yield FhirMergeResponse(
265
+ response_text=response_text,
266
+ response_status_code=response_status,
267
+ exception=e,
268
+ variables=FhirClientLogger.get_variables_to_log(vars(self)),
269
+ message=f"HttpError: {e}",
270
+ elapsed_time=time.time() - start_time,
271
+ ) from e
272
+ except Exception as e:
273
+ raise FhirSenderException(
223
274
  request_id=request_id,
224
- url=resource_uri.url or self._url or "",
275
+ url=resource_uri.url,
276
+ headers=headers,
225
277
  json_data=json_payload,
226
- responses=[
227
- {
228
- "issue": [
229
- {
230
- "severity": "error",
231
- "code": "exception",
232
- "diagnostics": response_text,
233
- }
234
- ]
235
- }
236
- ],
237
- error=(json.dumps(response_text) if response_text else None),
238
- access_token=self._access_token,
239
- status=response.status if response.status else 500,
240
- )
241
- except requests.exceptions.HTTPError as e:
242
- raise FhirSenderException(
278
+ response_text=response_text,
279
+ response_status_code=response_status,
280
+ exception=e,
281
+ variables=FhirClientLogger.get_variables_to_log(vars(self)),
282
+ message=f"Unknown Error: {e}",
283
+ elapsed_time=time.time() - start_time,
284
+ ) from e
285
+ else:
286
+ json_payload = json.dumps(json_data_list)
287
+ yield FhirMergeResponse(
243
288
  request_id=request_id,
244
- url=resource_uri.url,
245
- headers=headers,
246
- json_data=json_payload,
247
- response_text=response_text,
248
- response_status_code=response_status,
249
- exception=e,
250
- variables=FhirClientLogger.get_variables_to_log(vars(self)),
251
- message=f"HttpError: {e}",
252
- elapsed_time=time.time() - start_time,
253
- ) from e
254
- except Exception as e:
255
- raise FhirSenderException(
256
- request_id=request_id,
257
- url=resource_uri.url,
258
- headers=headers,
289
+ url=full_uri.url,
290
+ responses=responses + errors,
291
+ error=(json.dumps(responses + errors) if response_status != 200 else None),
292
+ access_token=self._access_token,
293
+ status=response_status if response_status else 500,
259
294
  json_data=json_payload,
260
- response_text=response_text,
261
- response_status_code=response_status,
262
- exception=e,
263
- variables=FhirClientLogger.get_variables_to_log(vars(self)),
264
- message=f"Unknown Error: {e}",
265
- elapsed_time=time.time() - start_time,
266
- ) from e
267
- else:
268
- json_payload = json.dumps(json_data_list)
269
- yield FhirMergeResponse(
270
- request_id=request_id,
271
- url=full_uri.url,
272
- responses=responses + errors,
273
- error=(json.dumps(responses + errors) if response_status != 200 else None),
274
- access_token=self._access_token,
275
- status=response_status if response_status else 500,
276
- json_data=json_payload,
277
- )
278
- except AssertionError as e:
279
- if self._logger:
280
- self._logger.error(
281
- Exception(
282
- f"Assertion: FHIR send failed: {str(e)} for resource: {json_data_list}. "
283
- + f"variables={convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))}"
284
- )
285
- )
286
- raise e
295
+ )
296
+ except AssertionError as e:
297
+ if self._logger:
298
+ self._logger.error(
299
+ Exception(
300
+ f"Assertion: FHIR send failed: {str(e)} for resource: {json_data_list}. "
301
+ + f"variables={convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))}"
302
+ )
303
+ )
304
+ raise e
305
+ except Exception as e:
306
+ span.record_exception(e)
307
+ span.set_status(Status(StatusCode.ERROR, str(e)))
308
+ raise
287
309
 
288
310
  def merge(
289
311
  self,