helix.fhir.client.sdk 4.2.15__py3-none-any.whl → 4.2.17__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/fhir_auth_mixin.py +4 -2
- helix_fhir_client_sdk/fhir_client.py +36 -8
- helix_fhir_client_sdk/fhir_delete_mixin.py +61 -44
- helix_fhir_client_sdk/fhir_merge_mixin.py +188 -163
- helix_fhir_client_sdk/fhir_merge_resources_mixin.py +8 -4
- helix_fhir_client_sdk/fhir_patch_mixin.py +97 -81
- helix_fhir_client_sdk/fhir_update_mixin.py +71 -54
- helix_fhir_client_sdk/open_telemetry/attribute_names.py +3 -0
- helix_fhir_client_sdk/open_telemetry/span_names.py +4 -0
- helix_fhir_client_sdk/queue/request_queue_mixin.py +4 -2
- helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py +34 -5
- helix_fhir_client_sdk/validators/async_fhir_validator.py +3 -0
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.17.dist-info}/METADATA +39 -2
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.17.dist-info}/RECORD +18 -17
- tests/async/test_retryable_client_session_management.py +159 -0
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.17.dist-info}/WHEEL +0 -0
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.17.dist-info}/licenses/LICENSE +0 -0
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.17.dist-info}/top_level.txt +0 -0
|
@@ -170,7 +170,8 @@ class FhirAuthMixin(FhirClientProtocol):
|
|
|
170
170
|
:return: auth server url or None
|
|
171
171
|
"""
|
|
172
172
|
async with RetryableAioHttpClient(
|
|
173
|
-
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,
|
|
174
175
|
exclude_status_codes_from_retry=[404],
|
|
175
176
|
throw_exception_on_error=False,
|
|
176
177
|
use_data_streaming=False,
|
|
@@ -272,7 +273,8 @@ class FhirAuthMixin(FhirClientProtocol):
|
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
async with RetryableAioHttpClient(
|
|
275
|
-
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,
|
|
276
278
|
use_data_streaming=False,
|
|
277
279
|
compress=False,
|
|
278
280
|
exclude_status_codes_from_retry=None,
|
|
@@ -488,6 +488,37 @@ class FhirClient(
|
|
|
488
488
|
implementation. This allows for custom session management, connection pooling,
|
|
489
489
|
or persistent session support.
|
|
490
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
|
+
|
|
491
522
|
:param fn_create_http_session: callable that returns a ClientSession, or None to use default
|
|
492
523
|
"""
|
|
493
524
|
self._fn_create_http_session = fn_create_http_session
|
|
@@ -815,16 +846,13 @@ class FhirClient(
|
|
|
815
846
|
|
|
816
847
|
def create_http_session(self) -> ClientSession:
|
|
817
848
|
"""
|
|
818
|
-
Creates
|
|
849
|
+
Creates the SDK's default HTTP Session.
|
|
819
850
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
"""
|
|
823
|
-
# Use a custom session factory if provided
|
|
824
|
-
if self._fn_create_http_session is not None:
|
|
825
|
-
return self._fn_create_http_session()
|
|
851
|
+
This method always creates a new aiohttp ClientSession with standard configuration.
|
|
852
|
+
The SDK manages the lifecycle of sessions created by this method.
|
|
826
853
|
|
|
827
|
-
|
|
854
|
+
Note: If you want to provide your own session factory, use use_http_session() instead.
|
|
855
|
+
"""
|
|
828
856
|
trace_config = aiohttp.TraceConfig()
|
|
829
857
|
# trace_config.on_request_start.append(on_request_start)
|
|
830
858
|
if self._log_level == "DEBUG":
|
|
@@ -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
38
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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,
|
|
@@ -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=
|
|
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=
|
|
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,182 +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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
# Conditionally add the query parameter
|
|
182
|
+
if self._smart_merge is False:
|
|
183
|
+
resource_uri.add({"smartMerge": "false"})
|
|
168
184
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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)}]
|
|
201
224
|
else:
|
|
202
|
-
responses = [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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(
|
|
208
261
|
request_id=request_id,
|
|
209
262
|
url=resource_uri.url,
|
|
210
|
-
|
|
211
|
-
error=(json.dumps(responses + errors) if response_status != 200 else None),
|
|
212
|
-
access_token=self._access_token,
|
|
213
|
-
status=response_status if response_status else 500,
|
|
263
|
+
headers=headers,
|
|
214
264
|
json_data=json_payload,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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(
|
|
220
274
|
request_id=request_id,
|
|
221
|
-
url=resource_uri.url
|
|
275
|
+
url=resource_uri.url,
|
|
276
|
+
headers=headers,
|
|
222
277
|
json_data=json_payload,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
],
|
|
234
|
-
error=(json.dumps(response_text) if response_text else None),
|
|
235
|
-
access_token=self._access_token,
|
|
236
|
-
status=response.status if response.status else 500,
|
|
237
|
-
)
|
|
238
|
-
except requests.exceptions.HTTPError as e:
|
|
239
|
-
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(
|
|
240
288
|
request_id=request_id,
|
|
241
|
-
url=
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
exception=e,
|
|
247
|
-
variables=FhirClientLogger.get_variables_to_log(vars(self)),
|
|
248
|
-
message=f"HttpError: {e}",
|
|
249
|
-
elapsed_time=time.time() - start_time,
|
|
250
|
-
) from e
|
|
251
|
-
except Exception as e:
|
|
252
|
-
raise FhirSenderException(
|
|
253
|
-
request_id=request_id,
|
|
254
|
-
url=resource_uri.url,
|
|
255
|
-
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,
|
|
256
294
|
json_data=json_payload,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
access_token=self._access_token,
|
|
272
|
-
status=response_status if response_status else 500,
|
|
273
|
-
json_data=json_payload,
|
|
274
|
-
)
|
|
275
|
-
except AssertionError as e:
|
|
276
|
-
if self._logger:
|
|
277
|
-
self._logger.error(
|
|
278
|
-
Exception(
|
|
279
|
-
f"Assertion: FHIR send failed: {str(e)} for resource: {json_data_list}. "
|
|
280
|
-
+ f"variables={convert_dict_to_str(FhirClientLogger.get_variables_to_log(vars(self)))}"
|
|
281
|
-
)
|
|
282
|
-
)
|
|
283
|
-
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
|
|
284
309
|
|
|
285
310
|
def merge(
|
|
286
311
|
self,
|