helix.fhir.client.sdk 4.1.67__py3-none-any.whl → 4.2.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- helix_fhir_client_sdk/dictionary_parser.py +4 -0
- helix_fhir_client_sdk/fhir_auth_mixin.py +17 -10
- helix_fhir_client_sdk/fhir_client.py +161 -61
- helix_fhir_client_sdk/fhir_delete_mixin.py +62 -45
- helix_fhir_client_sdk/fhir_merge_mixin.py +188 -163
- helix_fhir_client_sdk/fhir_merge_resources_mixin.py +200 -9
- helix_fhir_client_sdk/fhir_patch_mixin.py +97 -81
- helix_fhir_client_sdk/fhir_update_mixin.py +71 -54
- helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py +5 -174
- helix_fhir_client_sdk/open_telemetry/__init__.py +0 -0
- helix_fhir_client_sdk/open_telemetry/attribute_names.py +7 -0
- helix_fhir_client_sdk/open_telemetry/span_names.py +12 -0
- helix_fhir_client_sdk/queue/request_queue_mixin.py +46 -119
- helix_fhir_client_sdk/responses/fhir_client_protocol.py +9 -1
- helix_fhir_client_sdk/responses/fhir_response_processor.py +73 -54
- helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +0 -2
- helix_fhir_client_sdk/responses/merge/fhir_merge_resource_response_entry.py +30 -0
- helix_fhir_client_sdk/utilities/async_parallel_processor/v1/async_parallel_processor.py +1 -24
- helix_fhir_client_sdk/utilities/cache/request_cache.py +32 -43
- helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py +184 -144
- helix_fhir_client_sdk/utilities/retryable_aiohttp_response.py +2 -1
- helix_fhir_client_sdk/utilities/url_checker.py +46 -12
- helix_fhir_client_sdk/validators/async_fhir_validator.py +3 -0
- helix_fhir_client_sdk-4.2.18.dist-info/METADATA +200 -0
- {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/RECORD +32 -25
- tests/async/test_benchmark_compress.py +448 -0
- tests/async/test_benchmark_merge.py +506 -0
- tests/async/test_retryable_client_session_management.py +159 -0
- tests/test_fhir_client_clone.py +155 -0
- helix_fhir_client_sdk-4.1.67.dist-info/METADATA +0 -115
- {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/WHEEL +0 -0
- {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/licenses/LICENSE +0 -0
- {helix_fhir_client_sdk-4.1.67.dist-info → helix_fhir_client_sdk-4.2.18.dist-info}/top_level.txt +0 -0
|
@@ -52,6 +52,10 @@ class DictionaryParser:
|
|
|
52
52
|
# Iterate through the list, try a .get(part) on each iteration for part (remove the [x] first),
|
|
53
53
|
# and create a new list of the values in result
|
|
54
54
|
elif part.endswith("[x]") and isinstance(result, list):
|
|
55
|
+
if result and isinstance(result[0], list):
|
|
56
|
+
# result is a list of lists - flatten it first
|
|
57
|
+
result = DictionaryParser.flatten(result)
|
|
58
|
+
|
|
55
59
|
result = [value for result_entry in result if (value := result_entry.get(part[:-3]))]
|
|
56
60
|
|
|
57
61
|
# part is looking for a repeating field, but result is currently a dict.
|
|
@@ -8,11 +8,13 @@ from threading import Lock
|
|
|
8
8
|
from typing import TYPE_CHECKING, Any, cast
|
|
9
9
|
|
|
10
10
|
from furl import furl
|
|
11
|
+
from opentelemetry import trace
|
|
11
12
|
|
|
12
13
|
from helix_fhir_client_sdk.function_types import (
|
|
13
14
|
RefreshTokenFunction,
|
|
14
15
|
RefreshTokenResult,
|
|
15
16
|
)
|
|
17
|
+
from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
|
|
16
18
|
from helix_fhir_client_sdk.responses.fhir_client_protocol import FhirClientProtocol
|
|
17
19
|
from helix_fhir_client_sdk.structures.get_access_token_result import (
|
|
18
20
|
GetAccessTokenResult,
|
|
@@ -31,6 +33,9 @@ if TYPE_CHECKING:
|
|
|
31
33
|
from helix_fhir_client_sdk.fhir_client import FhirClient
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
TRACER = trace.get_tracer(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
34
39
|
class FhirAuthMixin(FhirClientProtocol):
|
|
35
40
|
_time_to_live_in_secs_for_cache: int = 10 * 60
|
|
36
41
|
|
|
@@ -119,15 +124,15 @@ class FhirAuthMixin(FhirClientProtocol):
|
|
|
119
124
|
access_token=self._access_token,
|
|
120
125
|
expiry_date=self._access_token_expiry_date,
|
|
121
126
|
)
|
|
127
|
+
with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.GET_ACCESS_TOKEN):
|
|
128
|
+
refresh_token_result: RefreshTokenResult = await self._refresh_token_function(
|
|
129
|
+
url=None, status_code=0, current_token=None, expiry_date=None, retry_count=0
|
|
130
|
+
)
|
|
131
|
+
assert isinstance(refresh_token_result, RefreshTokenResult)
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
assert isinstance(refresh_token_result, RefreshTokenResult)
|
|
127
|
-
|
|
128
|
-
self.set_access_token(refresh_token_result.access_token)
|
|
129
|
-
self.set_access_token_expiry_date(refresh_token_result.expiry_date)
|
|
130
|
-
return GetAccessTokenResult(access_token=self._access_token, expiry_date=self._access_token_expiry_date)
|
|
133
|
+
self.set_access_token(refresh_token_result.access_token)
|
|
134
|
+
self.set_access_token_expiry_date(refresh_token_result.expiry_date)
|
|
135
|
+
return GetAccessTokenResult(access_token=self._access_token, expiry_date=self._access_token_expiry_date)
|
|
131
136
|
|
|
132
137
|
def authenticate_async_wrapper(self) -> RefreshTokenFunction:
|
|
133
138
|
"""
|
|
@@ -165,7 +170,8 @@ class FhirAuthMixin(FhirClientProtocol):
|
|
|
165
170
|
:return: auth server url or None
|
|
166
171
|
"""
|
|
167
172
|
async with RetryableAioHttpClient(
|
|
168
|
-
fn_get_session=
|
|
173
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
174
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
169
175
|
exclude_status_codes_from_retry=[404],
|
|
170
176
|
throw_exception_on_error=False,
|
|
171
177
|
use_data_streaming=False,
|
|
@@ -267,7 +273,8 @@ class FhirAuthMixin(FhirClientProtocol):
|
|
|
267
273
|
}
|
|
268
274
|
|
|
269
275
|
async with RetryableAioHttpClient(
|
|
270
|
-
fn_get_session=
|
|
276
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
277
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
271
278
|
use_data_streaming=False,
|
|
272
279
|
compress=False,
|
|
273
280
|
exclude_status_codes_from_retry=None,
|
|
@@ -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
|
|
@@ -26,6 +26,8 @@ from compressedfhir.utilities.compressed_dict.v1.compressed_dict_storage_mode im
|
|
|
26
26
|
CompressedDictStorageMode,
|
|
27
27
|
)
|
|
28
28
|
from furl import furl
|
|
29
|
+
from opentelemetry import trace
|
|
30
|
+
from opentelemetry.trace import Status, StatusCode
|
|
29
31
|
from requests.adapters import BaseAdapter
|
|
30
32
|
|
|
31
33
|
from helix_fhir_client_sdk.dictionary_writer import convert_dict_to_str
|
|
@@ -46,6 +48,7 @@ from helix_fhir_client_sdk.graph.fhir_graph_mixin import FhirGraphMixin
|
|
|
46
48
|
from helix_fhir_client_sdk.graph.simulated_graph_processor_mixin import (
|
|
47
49
|
SimulatedGraphProcessorMixin,
|
|
48
50
|
)
|
|
51
|
+
from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
|
|
49
52
|
from helix_fhir_client_sdk.queue.request_queue_mixin import RequestQueueMixin
|
|
50
53
|
from helix_fhir_client_sdk.responses.fhir_client_protocol import FhirClientProtocol
|
|
51
54
|
from helix_fhir_client_sdk.responses.fhir_get_response import FhirGetResponse
|
|
@@ -55,6 +58,8 @@ from helix_fhir_client_sdk.structures.get_access_token_result import (
|
|
|
55
58
|
from helix_fhir_client_sdk.utilities.async_runner import AsyncRunner
|
|
56
59
|
from helix_fhir_client_sdk.utilities.fhir_client_logger import FhirClientLogger
|
|
57
60
|
|
|
61
|
+
TRACER = trace.get_tracer(__name__)
|
|
62
|
+
|
|
58
63
|
|
|
59
64
|
class FhirClient(
|
|
60
65
|
SimulatedGraphProcessorMixin,
|
|
@@ -145,9 +150,13 @@ class FhirClient(
|
|
|
145
150
|
self._log_all_response_urls: bool = False
|
|
146
151
|
""" If True, logs all response URLs and status codes. Can take a lot of memory for when there are many responses. """
|
|
147
152
|
|
|
148
|
-
|
|
153
|
+
# Default to "raw" storage mode - no in-memory compression, resources stored as plain Python dicts
|
|
154
|
+
self._storage_mode: CompressedDictStorageMode = CompressedDictStorageMode(storage_type="raw")
|
|
155
|
+
|
|
156
|
+
self._create_operation_outcome_for_error: bool | None = False
|
|
149
157
|
|
|
150
|
-
|
|
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
|
|
151
160
|
|
|
152
161
|
def action(self, action: str) -> FhirClient:
|
|
153
162
|
"""
|
|
@@ -450,9 +459,14 @@ class FhirClient(
|
|
|
450
459
|
|
|
451
460
|
def compress(self, compress: bool) -> FhirClient:
|
|
452
461
|
"""
|
|
453
|
-
Sets the
|
|
462
|
+
Sets whether to use HTTP compression (gzip) when sending request data to the server.
|
|
463
|
+
|
|
464
|
+
This controls compression of the HTTP request body only, not in-memory storage.
|
|
465
|
+
Default is True (compression enabled).
|
|
466
|
+
|
|
467
|
+
To disable all compression, call: .compress(False)
|
|
454
468
|
|
|
455
|
-
:param compress: whether to compress
|
|
469
|
+
:param compress: whether to compress HTTP request body (default: True)
|
|
456
470
|
"""
|
|
457
471
|
self._compress = compress
|
|
458
472
|
return self
|
|
@@ -466,6 +480,50 @@ class FhirClient(
|
|
|
466
480
|
self._throw_exception_on_error = throw_exception_on_error
|
|
467
481
|
return self
|
|
468
482
|
|
|
483
|
+
def use_http_session(self, fn_create_http_session: Callable[[], ClientSession] | None) -> FhirClient:
|
|
484
|
+
"""
|
|
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
|
|
501
|
+
|
|
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
|
|
523
|
+
"""
|
|
524
|
+
self._fn_create_http_session = fn_create_http_session
|
|
525
|
+
return self
|
|
526
|
+
|
|
469
527
|
# noinspection PyUnusedLocal
|
|
470
528
|
@staticmethod
|
|
471
529
|
async def on_request_end(
|
|
@@ -579,33 +637,34 @@ class FhirClient(
|
|
|
579
637
|
|
|
580
638
|
:return: response
|
|
581
639
|
"""
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
640
|
+
with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.GET):
|
|
641
|
+
instance_variables_text = convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))
|
|
642
|
+
if self._logger:
|
|
643
|
+
# self._logger.info(f"LOGLEVEL: {self._log_level}")
|
|
644
|
+
self._logger.debug(f"parameters: {instance_variables_text}")
|
|
645
|
+
else:
|
|
646
|
+
self._internal_logger.debug(f"LOGLEVEL (InternalLogger): {self._log_level}")
|
|
647
|
+
self._internal_logger.debug(f"parameters: {instance_variables_text}")
|
|
648
|
+
ids: list[str] | None = None
|
|
649
|
+
if self._id:
|
|
650
|
+
ids = self._id if isinstance(self._id, list) else [self._id]
|
|
651
|
+
# actually make the request
|
|
652
|
+
full_response: FhirGetResponse | None = None
|
|
653
|
+
async for response in self._get_with_session_async(
|
|
654
|
+
ids=ids,
|
|
655
|
+
fn_handle_streaming_chunk=data_chunk_handler,
|
|
656
|
+
page_number=None,
|
|
657
|
+
id_above=None,
|
|
658
|
+
additional_parameters=None,
|
|
659
|
+
resource_type=None,
|
|
660
|
+
):
|
|
661
|
+
if response:
|
|
662
|
+
if full_response:
|
|
663
|
+
full_response = full_response.append(response)
|
|
664
|
+
else:
|
|
665
|
+
full_response = response
|
|
666
|
+
assert full_response
|
|
667
|
+
return full_response
|
|
609
668
|
|
|
610
669
|
async def get_raw_resources_async(
|
|
611
670
|
self,
|
|
@@ -646,27 +705,37 @@ class FhirClient(
|
|
|
646
705
|
|
|
647
706
|
:return: async generator of responses
|
|
648
707
|
"""
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
self._logger
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
ids
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
708
|
+
span = TRACER.start_span(FhirClientSdkOpenTelemetrySpanNames.GET_STREAMING)
|
|
709
|
+
try:
|
|
710
|
+
instance_variables_text = convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))
|
|
711
|
+
if self._logger:
|
|
712
|
+
# self._logger.info(f"LOGLEVEL: {self._log_level}")
|
|
713
|
+
self._logger.info(f"parameters: {instance_variables_text}")
|
|
714
|
+
else:
|
|
715
|
+
self._internal_logger.info(f"LOGLEVEL (InternalLogger): {self._log_level}")
|
|
716
|
+
self._internal_logger.info(f"parameters: {instance_variables_text}")
|
|
717
|
+
ids: list[str] | None = None
|
|
718
|
+
if self._id:
|
|
719
|
+
ids = self._id if isinstance(self._id, list) else [self._id]
|
|
720
|
+
# actually make the request
|
|
721
|
+
response: FhirGetResponse | None
|
|
722
|
+
async for response in self._get_with_session_async(
|
|
723
|
+
ids=ids,
|
|
724
|
+
fn_handle_streaming_chunk=data_chunk_handler,
|
|
725
|
+
page_number=None,
|
|
726
|
+
id_above=None,
|
|
727
|
+
additional_parameters=None,
|
|
728
|
+
resource_type=None,
|
|
729
|
+
):
|
|
730
|
+
yield response
|
|
731
|
+
except BaseException as exc: # propagate cancellation/errors but keep span informative
|
|
732
|
+
# Record exception in span
|
|
733
|
+
span.record_exception(exc)
|
|
734
|
+
span.set_status(Status(StatusCode.ERROR, str(exc)))
|
|
735
|
+
raise
|
|
736
|
+
finally:
|
|
737
|
+
# Ensure span is ended after generator is exhausted or error occurs
|
|
738
|
+
span.end()
|
|
670
739
|
|
|
671
740
|
def get(self) -> FhirGetResponse:
|
|
672
741
|
"""
|
|
@@ -777,8 +846,12 @@ class FhirClient(
|
|
|
777
846
|
|
|
778
847
|
def create_http_session(self) -> ClientSession:
|
|
779
848
|
"""
|
|
780
|
-
Creates
|
|
849
|
+
Creates the SDK's default HTTP Session.
|
|
850
|
+
|
|
851
|
+
This method always creates a new aiohttp ClientSession with standard configuration.
|
|
852
|
+
The SDK manages the lifecycle of sessions created by this method.
|
|
781
853
|
|
|
854
|
+
Note: If you want to provide your own session factory, use use_http_session() instead.
|
|
782
855
|
"""
|
|
783
856
|
trace_config = aiohttp.TraceConfig()
|
|
784
857
|
# trace_config.on_request_start.append(on_request_start)
|
|
@@ -801,17 +874,17 @@ class FhirClient(
|
|
|
801
874
|
Whether to ask the server to include the total count in the result
|
|
802
875
|
|
|
803
876
|
|
|
804
|
-
:param include_total: whether to include total count
|
|
877
|
+
:param include_total: whether to include the total count
|
|
805
878
|
"""
|
|
806
879
|
self._include_total = include_total
|
|
807
880
|
return self
|
|
808
881
|
|
|
809
882
|
def filter(self, filter_: list[BaseFilter]) -> FhirClient:
|
|
810
883
|
"""
|
|
811
|
-
Allows adding in
|
|
884
|
+
Allows adding in custom filters that derive from BaseFilter
|
|
812
885
|
|
|
813
886
|
|
|
814
|
-
:param filter_: list of custom filter instances that
|
|
887
|
+
:param filter_: list of custom filter instances that derive from BaseFilter.
|
|
815
888
|
"""
|
|
816
889
|
assert isinstance(filter_, list), "This function requires a list"
|
|
817
890
|
self._filters.extend(filter_)
|
|
@@ -852,6 +925,8 @@ class FhirClient(
|
|
|
852
925
|
fhir_client._client_id = self._client_id
|
|
853
926
|
fhir_client._auth_server_url = self._auth_server_url
|
|
854
927
|
fhir_client._login_token = self._login_token
|
|
928
|
+
fhir_client._access_token = self._access_token
|
|
929
|
+
fhir_client._access_token_expiry_date = self._access_token_expiry_date
|
|
855
930
|
fhir_client._refresh_token_function = self._refresh_token_function
|
|
856
931
|
fhir_client._exclude_status_codes_from_retry = self._exclude_status_codes_from_retry
|
|
857
932
|
fhir_client._chunk_size = self._chunk_size
|
|
@@ -864,6 +939,19 @@ class FhirClient(
|
|
|
864
939
|
fhir_client._time_to_live_in_secs_for_cache = self._time_to_live_in_secs_for_cache
|
|
865
940
|
fhir_client._validation_server_url = self._validation_server_url
|
|
866
941
|
fhir_client._smart_merge = self._smart_merge
|
|
942
|
+
fhir_client._compress = self._compress
|
|
943
|
+
fhir_client._storage_mode = self._storage_mode
|
|
944
|
+
fhir_client._send_data_as_chunked = self._send_data_as_chunked
|
|
945
|
+
fhir_client._use_post_for_search = self._use_post_for_search
|
|
946
|
+
fhir_client._maximum_time_to_retry_on_429 = self._maximum_time_to_retry_on_429
|
|
947
|
+
fhir_client._retry_count = self._retry_count
|
|
948
|
+
fhir_client._throw_exception_on_error = self._throw_exception_on_error
|
|
949
|
+
fhir_client._trace_request_function = self._trace_request_function
|
|
950
|
+
fhir_client._log_all_response_urls = self._log_all_response_urls
|
|
951
|
+
fhir_client._create_operation_outcome_for_error = self._create_operation_outcome_for_error
|
|
952
|
+
fhir_client._fn_create_http_session = self._fn_create_http_session
|
|
953
|
+
if self._max_concurrent_requests is not None:
|
|
954
|
+
fhir_client.set_max_concurrent_requests(self._max_concurrent_requests)
|
|
867
955
|
return fhir_client
|
|
868
956
|
|
|
869
957
|
def set_log_all_response_urls(self, value: bool) -> FhirClient:
|
|
@@ -886,18 +974,30 @@ class FhirClient(
|
|
|
886
974
|
|
|
887
975
|
def set_storage_mode(self, value: CompressedDictStorageMode) -> FhirClient:
|
|
888
976
|
"""
|
|
889
|
-
Sets the storage mode
|
|
977
|
+
Sets the in-memory storage mode for FHIR resources.
|
|
978
|
+
|
|
979
|
+
This controls how FHIR resources are stored in memory after being received.
|
|
980
|
+
The default is "raw" (no compression - resources stored as plain Python dicts).
|
|
981
|
+
|
|
982
|
+
Available storage types:
|
|
983
|
+
- "raw": No compression, standard Python dictionaries (default)
|
|
984
|
+
- "compressed": Zlib/gzip compression in memory
|
|
985
|
+
- "msgpack": MessagePack binary serialization
|
|
986
|
+
- "compressed_msgpack": MessagePack + compression
|
|
987
|
+
|
|
988
|
+
Note: This is separate from HTTP compression (controlled by .compress()).
|
|
989
|
+
With default settings, no in-memory compression is applied.
|
|
890
990
|
|
|
891
|
-
:param value: storage mode
|
|
991
|
+
:param value: storage mode (default: raw)
|
|
892
992
|
"""
|
|
893
993
|
self._storage_mode = value
|
|
894
994
|
return self
|
|
895
995
|
|
|
896
|
-
def set_create_operation_outcome_for_error(self, value: bool) -> FhirClient:
|
|
996
|
+
def set_create_operation_outcome_for_error(self, value: bool | None) -> FhirClient:
|
|
897
997
|
"""
|
|
898
998
|
Sets the create_operation_outcome_for_error flag
|
|
899
999
|
|
|
900
|
-
:param value: whether to create operation outcome for error
|
|
1000
|
+
:param value: whether to create an operation outcome for error (True, False, or None)
|
|
901
1001
|
"""
|
|
902
1002
|
self._create_operation_outcome_for_error = value
|
|
903
1003
|
return self
|
|
@@ -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,50 +35,60 @@ 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
|
-
) as client:
|
|
60
|
-
response: RetryableAioHttpResponse = await client.delete(url=full_uri.tostr(), headers=headers)
|
|
61
|
-
request_id = response.response_headers.get("X-Request-ID", None)
|
|
62
|
-
self._internal_logger.info(f"X-Request-ID={request_id}")
|
|
63
|
-
if response.status == 200:
|
|
64
|
-
if self._logger:
|
|
65
|
-
self._logger.info(f"Successfully deleted: {full_uri}")
|
|
66
38
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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=self._fn_create_http_session or self.create_http_session,
|
|
60
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
61
|
+
refresh_token_func=self._refresh_token_function,
|
|
62
|
+
retries=self._retry_count,
|
|
63
|
+
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
64
|
+
use_data_streaming=self._use_data_streaming,
|
|
65
|
+
compress=False,
|
|
66
|
+
throw_exception_on_error=self._throw_exception_on_error,
|
|
67
|
+
log_all_url_results=self._log_all_response_urls,
|
|
68
|
+
access_token=self._access_token,
|
|
69
|
+
access_token_expiry_date=self._access_token_expiry_date,
|
|
70
|
+
tracer_request_func=self._trace_request_function,
|
|
71
|
+
) as client:
|
|
72
|
+
response: RetryableAioHttpResponse = await client.delete(url=full_uri.tostr(), headers=headers)
|
|
73
|
+
request_id = response.response_headers.get("X-Request-ID", None)
|
|
74
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
75
|
+
if response.status == 200:
|
|
76
|
+
if self._logger:
|
|
77
|
+
self._logger.info(f"Successfully deleted: {full_uri}")
|
|
78
|
+
|
|
79
|
+
return FhirDeleteResponse(
|
|
80
|
+
request_id=request_id,
|
|
81
|
+
url=full_uri.tostr(),
|
|
82
|
+
responses=await response.get_text_async(),
|
|
83
|
+
error=f"{response.status}" if not response.status == 200 else None,
|
|
84
|
+
access_token=access_token,
|
|
85
|
+
status=response.status,
|
|
86
|
+
resource_type=self._resource,
|
|
87
|
+
)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
span.record_exception(e)
|
|
90
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
91
|
+
raise
|
|
76
92
|
|
|
77
93
|
def delete(self) -> FhirDeleteResponse:
|
|
78
94
|
"""
|
|
@@ -114,7 +130,8 @@ class FhirDeleteMixin(FhirClientProtocol):
|
|
|
114
130
|
headers["Authorization"] = f"Bearer {access_token}"
|
|
115
131
|
|
|
116
132
|
async with RetryableAioHttpClient(
|
|
117
|
-
fn_get_session=
|
|
133
|
+
fn_get_session=self._fn_create_http_session or self.create_http_session,
|
|
134
|
+
caller_managed_session=self._fn_create_http_session is not None,
|
|
118
135
|
refresh_token_func=self._refresh_token_function,
|
|
119
136
|
retries=self._retry_count,
|
|
120
137
|
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
@@ -128,7 +145,7 @@ class FhirDeleteMixin(FhirClientProtocol):
|
|
|
128
145
|
) as client:
|
|
129
146
|
response: RetryableAioHttpResponse = await client.delete(url=full_url, headers=headers)
|
|
130
147
|
request_id = response.response_headers.get("X-Request-ID", None)
|
|
131
|
-
self._internal_logger.
|
|
148
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
132
149
|
if response.status == 200:
|
|
133
150
|
if self._logger:
|
|
134
151
|
self._logger.info(f"Successfully deleted: {full_uri}")
|