helix.fhir.client.sdk 4.2.15__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.
- helix_fhir_client_sdk/fhir_client.py +31 -0
- helix_fhir_client_sdk/fhir_delete_mixin.py +58 -43
- helix_fhir_client_sdk/fhir_merge_mixin.py +183 -161
- helix_fhir_client_sdk/fhir_patch_mixin.py +96 -81
- helix_fhir_client_sdk/fhir_update_mixin.py +69 -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/utilities/retryable_aiohttp_client.py +31 -5
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.16.dist-info}/METADATA +39 -2
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.16.dist-info}/RECORD +14 -13
- tests/async/test_retryable_client_session_management.py +155 -0
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.16.dist-info}/WHEEL +0 -0
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.16.dist-info}/licenses/LICENSE +0 -0
- {helix_fhir_client_sdk-4.2.15.dist-info → helix_fhir_client_sdk-4.2.16.dist-info}/top_level.txt +0 -0
|
@@ -2,8 +2,12 @@ import json
|
|
|
2
2
|
import time
|
|
3
3
|
|
|
4
4
|
from furl import furl
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from opentelemetry.trace import Status, StatusCode
|
|
5
7
|
|
|
6
8
|
from helix_fhir_client_sdk.exceptions.fhir_sender_exception import FhirSenderException
|
|
9
|
+
from helix_fhir_client_sdk.open_telemetry.attribute_names import FhirClientSdkOpenTelemetryAttributeNames
|
|
10
|
+
from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
|
|
7
11
|
from helix_fhir_client_sdk.responses.fhir_client_protocol import FhirClientProtocol
|
|
8
12
|
from helix_fhir_client_sdk.responses.fhir_update_response import FhirUpdateResponse
|
|
9
13
|
from helix_fhir_client_sdk.structures.get_access_token_result import (
|
|
@@ -18,6 +22,8 @@ from helix_fhir_client_sdk.utilities.retryable_aiohttp_response import (
|
|
|
18
22
|
RetryableAioHttpResponse,
|
|
19
23
|
)
|
|
20
24
|
|
|
25
|
+
TRACER = trace.get_tracer(__name__)
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
class FhirPatchMixin(FhirClientProtocol):
|
|
23
29
|
async def send_patch_request_async(self, data: str) -> FhirUpdateResponse:
|
|
@@ -33,93 +39,102 @@ class FhirPatchMixin(FhirClientProtocol):
|
|
|
33
39
|
raise ValueError("update should have only one id")
|
|
34
40
|
if not self._resource:
|
|
35
41
|
raise ValueError("update requires a FHIR resource type")
|
|
36
|
-
self._internal_logger.debug(
|
|
37
|
-
f"Calling patch method on {self._url} with client_id={self._client_id} and scopes={self._auth_scopes}"
|
|
38
|
-
)
|
|
39
|
-
full_uri: furl = furl(self._url)
|
|
40
|
-
full_uri /= self._resource
|
|
41
|
-
full_uri /= self._id
|
|
42
|
-
request_id: str | None = None
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.PATCH) as span:
|
|
44
|
+
span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.URL, self._url or "")
|
|
45
|
+
span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.RESOURCE, self._resource or "")
|
|
46
|
+
try:
|
|
47
|
+
self._internal_logger.debug(
|
|
48
|
+
f"Calling patch method on {self._url} with client_id={self._client_id} and scopes={self._auth_scopes}"
|
|
49
|
+
)
|
|
50
|
+
full_uri: furl = furl(self._url)
|
|
51
|
+
full_uri /= self._resource
|
|
52
|
+
full_uri /= self._id
|
|
53
|
+
request_id: str | None = None
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
headers = {"Content-Type": "application/json-patch+json"}
|
|
48
|
-
headers.update(self._additional_request_headers)
|
|
49
|
-
self._internal_logger.debug(f"Request headers: {headers}")
|
|
50
|
-
access_token_result: GetAccessTokenResult = await self.get_access_token_async()
|
|
51
|
-
access_token: str | None = access_token_result.access_token
|
|
52
|
-
# set access token in request if present
|
|
53
|
-
if access_token:
|
|
54
|
-
headers["Authorization"] = f"Bearer {access_token}"
|
|
55
|
+
start_time: float = time.time()
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
# Set up headers
|
|
58
|
+
headers = {"Content-Type": "application/json-patch+json"}
|
|
59
|
+
headers.update(self._additional_request_headers)
|
|
60
|
+
self._internal_logger.debug(f"Request headers: {headers}")
|
|
61
|
+
access_token_result: GetAccessTokenResult = await self.get_access_token_async()
|
|
62
|
+
access_token: str | None = access_token_result.access_token
|
|
63
|
+
# set access token in request if present
|
|
64
|
+
if access_token:
|
|
65
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# actually make the request
|
|
62
|
-
async with RetryableAioHttpClient(
|
|
63
|
-
fn_get_session=lambda: self.create_http_session(),
|
|
64
|
-
refresh_token_func=self._refresh_token_function,
|
|
65
|
-
tracer_request_func=self._trace_request_function,
|
|
66
|
-
retries=self._retry_count,
|
|
67
|
-
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
68
|
-
use_data_streaming=self._use_data_streaming,
|
|
69
|
-
send_data_as_chunked=self._send_data_as_chunked,
|
|
70
|
-
compress=self._compress,
|
|
71
|
-
throw_exception_on_error=self._throw_exception_on_error,
|
|
72
|
-
log_all_url_results=self._log_all_response_urls,
|
|
73
|
-
access_token=self._access_token,
|
|
74
|
-
access_token_expiry_date=self._access_token_expiry_date,
|
|
75
|
-
) as client:
|
|
76
|
-
response: RetryableAioHttpResponse = await client.patch(
|
|
77
|
-
url=full_uri.url, json=deserialized_data, headers=headers
|
|
78
|
-
)
|
|
79
|
-
response_status = response.status
|
|
80
|
-
response_text = await response.get_text_async()
|
|
81
|
-
request_id = response.response_headers.get("X-Request-ID", None)
|
|
82
|
-
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
67
|
+
response_text: str | None = None
|
|
68
|
+
response_status: int | None = None
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
self.
|
|
70
|
+
try:
|
|
71
|
+
deserialized_data = json.loads(data)
|
|
72
|
+
# actually make the request
|
|
73
|
+
async with RetryableAioHttpClient(
|
|
74
|
+
fn_get_session=lambda: self.create_http_session(),
|
|
75
|
+
refresh_token_func=self._refresh_token_function,
|
|
76
|
+
tracer_request_func=self._trace_request_function,
|
|
77
|
+
retries=self._retry_count,
|
|
78
|
+
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
79
|
+
use_data_streaming=self._use_data_streaming,
|
|
80
|
+
send_data_as_chunked=self._send_data_as_chunked,
|
|
81
|
+
compress=self._compress,
|
|
82
|
+
throw_exception_on_error=self._throw_exception_on_error,
|
|
83
|
+
log_all_url_results=self._log_all_response_urls,
|
|
84
|
+
access_token=self._access_token,
|
|
85
|
+
access_token_expiry_date=self._access_token_expiry_date,
|
|
86
|
+
) as client:
|
|
87
|
+
response: RetryableAioHttpResponse = await client.patch(
|
|
88
|
+
url=full_uri.url, json=deserialized_data, headers=headers
|
|
89
|
+
)
|
|
90
|
+
response_status = response.status
|
|
91
|
+
response_text = await response.get_text_async()
|
|
92
|
+
request_id = response.response_headers.get("X-Request-ID", None)
|
|
93
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
94
|
+
|
|
95
|
+
if response_status == 200:
|
|
96
|
+
if self._logger:
|
|
97
|
+
self._logger.info(f"Successfully updated: {full_uri}")
|
|
98
|
+
elif response_status == 404:
|
|
99
|
+
if self._logger:
|
|
100
|
+
self._logger.info(f"Request resource was not found: {full_uri}")
|
|
101
|
+
else:
|
|
102
|
+
# other HTTP errors
|
|
103
|
+
self._internal_logger.info(f"PATCH response for {full_uri.url}: {response_status}")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise FhirSenderException(
|
|
106
|
+
request_id=request_id,
|
|
107
|
+
url=full_uri.url,
|
|
108
|
+
headers=headers,
|
|
109
|
+
json_data=data,
|
|
110
|
+
response_text=response_text,
|
|
111
|
+
response_status_code=response_status,
|
|
112
|
+
exception=e,
|
|
113
|
+
variables=FhirClientLogger.get_variables_to_log(vars(self)),
|
|
114
|
+
message=f"Error: {e}",
|
|
115
|
+
elapsed_time=time.time() - start_time,
|
|
116
|
+
) from e
|
|
117
|
+
# check if response is json
|
|
118
|
+
if response_text:
|
|
119
|
+
try:
|
|
120
|
+
responses = json.loads(response_text)
|
|
121
|
+
except ValueError as e:
|
|
122
|
+
responses = {"issue": str(e)}
|
|
90
123
|
else:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
) from e
|
|
106
|
-
# check if response is json
|
|
107
|
-
if response_text:
|
|
108
|
-
try:
|
|
109
|
-
responses = json.loads(response_text)
|
|
110
|
-
except ValueError as e:
|
|
111
|
-
responses = {"issue": str(e)}
|
|
112
|
-
else:
|
|
113
|
-
responses = {}
|
|
114
|
-
return FhirUpdateResponse(
|
|
115
|
-
request_id=request_id,
|
|
116
|
-
url=full_uri.tostr(),
|
|
117
|
-
responses=json.dumps(responses),
|
|
118
|
-
error=json.dumps(responses),
|
|
119
|
-
access_token=access_token,
|
|
120
|
-
status=response_status if response_status else 500,
|
|
121
|
-
resource_type=self._resource,
|
|
122
|
-
)
|
|
124
|
+
responses = {}
|
|
125
|
+
return FhirUpdateResponse(
|
|
126
|
+
request_id=request_id,
|
|
127
|
+
url=full_uri.tostr(),
|
|
128
|
+
responses=json.dumps(responses),
|
|
129
|
+
error=json.dumps(responses),
|
|
130
|
+
access_token=access_token,
|
|
131
|
+
status=response_status if response_status else 500,
|
|
132
|
+
resource_type=self._resource,
|
|
133
|
+
)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
span.record_exception(e)
|
|
136
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
137
|
+
raise
|
|
123
138
|
|
|
124
139
|
def send_patch_request(self, data: str) -> FhirUpdateResponse:
|
|
125
140
|
"""
|
|
@@ -3,7 +3,11 @@ from collections.abc import AsyncGenerator
|
|
|
3
3
|
from compressedfhir.fhir.fhir_resource import FhirResource
|
|
4
4
|
from compressedfhir.fhir.fhir_resource_list import FhirResourceList
|
|
5
5
|
from furl import furl
|
|
6
|
+
from opentelemetry import trace
|
|
7
|
+
from opentelemetry.trace import Status, StatusCode
|
|
6
8
|
|
|
9
|
+
from helix_fhir_client_sdk.open_telemetry.attribute_names import FhirClientSdkOpenTelemetryAttributeNames
|
|
10
|
+
from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
|
|
7
11
|
from helix_fhir_client_sdk.responses.fhir_client_protocol import FhirClientProtocol
|
|
8
12
|
from helix_fhir_client_sdk.responses.fhir_update_response import FhirUpdateResponse
|
|
9
13
|
from helix_fhir_client_sdk.structures.get_access_token_result import (
|
|
@@ -15,6 +19,8 @@ from helix_fhir_client_sdk.utilities.retryable_aiohttp_client import (
|
|
|
15
19
|
)
|
|
16
20
|
from helix_fhir_client_sdk.validators.async_fhir_validator import AsyncFhirValidator
|
|
17
21
|
|
|
22
|
+
TRACER = trace.get_tracer(__name__)
|
|
23
|
+
|
|
18
24
|
|
|
19
25
|
class FhirUpdateMixin(FhirClientProtocol):
|
|
20
26
|
async def update_single_resource_async(self, *, resource: FhirResource) -> FhirUpdateResponse:
|
|
@@ -69,60 +75,69 @@ class FhirUpdateMixin(FhirClientProtocol):
|
|
|
69
75
|
raise ValueError("update should have only one id")
|
|
70
76
|
if not self._resource:
|
|
71
77
|
raise ValueError("update requires a FHIR resource type")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
78
|
+
|
|
79
|
+
with TRACER.start_as_current_span(FhirClientSdkOpenTelemetrySpanNames.UPDATE) as span:
|
|
80
|
+
span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.URL, self._url or "")
|
|
81
|
+
span.set_attribute(FhirClientSdkOpenTelemetryAttributeNames.RESOURCE, self._resource or "")
|
|
82
|
+
try:
|
|
83
|
+
full_uri: furl = furl(self._url)
|
|
84
|
+
full_uri /= self._resource
|
|
85
|
+
full_uri /= id_ or self._id
|
|
86
|
+
# set up headers
|
|
87
|
+
headers = {"Content-Type": "application/fhir+json"}
|
|
88
|
+
headers.update(self._additional_request_headers)
|
|
89
|
+
self._internal_logger.debug(f"Request headers: {headers}")
|
|
90
|
+
|
|
91
|
+
access_token_result: GetAccessTokenResult = await self.get_access_token_async()
|
|
92
|
+
access_token: str | None = access_token_result.access_token
|
|
93
|
+
# set access token in request if present
|
|
94
|
+
if access_token:
|
|
95
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
96
|
+
|
|
97
|
+
if self._validation_server_url:
|
|
98
|
+
await AsyncFhirValidator.validate_fhir_resource(
|
|
99
|
+
fn_get_session=lambda: self.create_http_session(),
|
|
100
|
+
json_data=json_data,
|
|
101
|
+
resource_name=self._resource,
|
|
102
|
+
validation_server_url=self._validation_server_url,
|
|
103
|
+
access_token=access_token,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# actually make the request
|
|
107
|
+
async with RetryableAioHttpClient(
|
|
108
|
+
fn_get_session=lambda: self.create_http_session(),
|
|
109
|
+
refresh_token_func=self._refresh_token_function,
|
|
110
|
+
tracer_request_func=self._trace_request_function,
|
|
111
|
+
retries=self._retry_count,
|
|
112
|
+
exclude_status_codes_from_retry=self._exclude_status_codes_from_retry,
|
|
113
|
+
use_data_streaming=self._use_data_streaming,
|
|
114
|
+
send_data_as_chunked=self._send_data_as_chunked,
|
|
115
|
+
compress=self._compress,
|
|
116
|
+
throw_exception_on_error=self._throw_exception_on_error,
|
|
117
|
+
log_all_url_results=self._log_all_response_urls,
|
|
118
|
+
access_token=self._access_token,
|
|
119
|
+
access_token_expiry_date=self._access_token_expiry_date,
|
|
120
|
+
) as client:
|
|
121
|
+
response = await client.put(url=full_uri.url, data=json_data, headers=headers)
|
|
122
|
+
request_id = response.response_headers.get("X-Request-ID", None)
|
|
123
|
+
self._internal_logger.debug(f"X-Request-ID={request_id}")
|
|
124
|
+
if response.status == 200:
|
|
125
|
+
if self._logger:
|
|
126
|
+
self._logger.info(f"Successfully updated: {full_uri}")
|
|
127
|
+
|
|
128
|
+
return FhirUpdateResponse(
|
|
129
|
+
request_id=request_id,
|
|
130
|
+
url=full_uri.tostr(),
|
|
131
|
+
responses=await response.get_text_async(),
|
|
132
|
+
error=f"{response.status}" if not response.status == 200 else None,
|
|
133
|
+
access_token=access_token,
|
|
134
|
+
status=response.status,
|
|
135
|
+
resource_type=self._resource,
|
|
136
|
+
)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
span.record_exception(e)
|
|
139
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
140
|
+
raise
|
|
126
141
|
|
|
127
142
|
def update(self, json_data: str) -> FhirUpdateResponse:
|
|
128
143
|
"""
|
|
@@ -2,3 +2,6 @@ class FhirClientSdkOpenTelemetryAttributeNames:
|
|
|
2
2
|
"""Constants for OpenTelemetry attribute names used in the FHIR Client SDK."""
|
|
3
3
|
|
|
4
4
|
URL: str = "fhir.client_sdk.url"
|
|
5
|
+
RESOURCE: str = "fhir.client_sdk.resource"
|
|
6
|
+
JSON_DATA_COUNT: str = "fhir.client_sdk.json_data.count"
|
|
7
|
+
BATCH_SIZE: str = "fhir.client_sdk.batch.size"
|
|
@@ -6,3 +6,7 @@ class FhirClientSdkOpenTelemetrySpanNames:
|
|
|
6
6
|
GET_ACCESS_TOKEN: str = "fhir.client_sdk.access_token.get"
|
|
7
7
|
HTTP_GET: str = "fhir.client_sdk.http.get"
|
|
8
8
|
HANDLE_RESPONSE: str = "fhir.client_sdk.handle_response"
|
|
9
|
+
DELETE: str = "fhir.client_sdk.delete"
|
|
10
|
+
UPDATE: str = "fhir.client_sdk.update"
|
|
11
|
+
PATCH: str = "fhir.client_sdk.patch"
|
|
12
|
+
MERGE: str = "fhir.client_sdk.merge"
|
|
@@ -47,8 +47,30 @@ class RetryableAioHttpClient:
|
|
|
47
47
|
access_token_expiry_date: datetime | None,
|
|
48
48
|
) -> None:
|
|
49
49
|
"""
|
|
50
|
-
RetryableClient provides a way to make HTTP calls with automatic retry and automatic refreshing of access tokens
|
|
50
|
+
RetryableClient provides a way to make HTTP calls with automatic retry and automatic refreshing of access tokens.
|
|
51
51
|
|
|
52
|
+
Session Lifecycle Management:
|
|
53
|
+
- If fn_get_session is None (default): The SDK creates and manages the session lifecycle.
|
|
54
|
+
The session will be automatically closed when exiting the context manager.
|
|
55
|
+
- If fn_get_session is provided: The caller is responsible for managing the session lifecycle.
|
|
56
|
+
The SDK will NOT close the session - the caller must close it themselves.
|
|
57
|
+
|
|
58
|
+
:param retries: Number of retry attempts for failed requests
|
|
59
|
+
:param timeout_in_seconds: Timeout for HTTP requests
|
|
60
|
+
:param backoff_factor: Factor for exponential backoff between retries
|
|
61
|
+
:param retry_status_codes: HTTP status codes that trigger a retry
|
|
62
|
+
:param refresh_token_func: Function to refresh authentication tokens
|
|
63
|
+
:param tracer_request_func: Function to trace/log requests
|
|
64
|
+
:param fn_get_session: Optional callable that returns a ClientSession. If provided,
|
|
65
|
+
the caller is responsible for closing the session.
|
|
66
|
+
:param exclude_status_codes_from_retry: Status codes to exclude from retry logic
|
|
67
|
+
:param use_data_streaming: Whether to stream response data
|
|
68
|
+
:param compress: Whether to compress request data
|
|
69
|
+
:param send_data_as_chunked: Whether to use chunked transfer encoding
|
|
70
|
+
:param throw_exception_on_error: Whether to raise exceptions on HTTP errors
|
|
71
|
+
:param log_all_url_results: Whether to log all URL results
|
|
72
|
+
:param access_token: Access token for authentication
|
|
73
|
+
:param access_token_expiry_date: Expiry date of the access token
|
|
52
74
|
"""
|
|
53
75
|
self.retries: int = retries
|
|
54
76
|
self.timeout_in_seconds: float | None = timeout_in_seconds
|
|
@@ -58,6 +80,8 @@ class RetryableAioHttpClient:
|
|
|
58
80
|
)
|
|
59
81
|
self.refresh_token_func_async: RefreshTokenFunction | None = refresh_token_func
|
|
60
82
|
self.trace_function_async: TraceRequestFunction | None = tracer_request_func
|
|
83
|
+
# Automatically determine if a session is caller-managed based on whether fn_get_session is provided
|
|
84
|
+
self._caller_managed_session: bool = fn_get_session is not None
|
|
61
85
|
self.fn_get_session: Callable[[], ClientSession] = (
|
|
62
86
|
fn_get_session if fn_get_session is not None else lambda: ClientSession()
|
|
63
87
|
)
|
|
@@ -81,7 +105,9 @@ class RetryableAioHttpClient:
|
|
|
81
105
|
exc_val: BaseException | None,
|
|
82
106
|
exc_tb: type[BaseException] | None | None,
|
|
83
107
|
) -> None:
|
|
84
|
-
|
|
108
|
+
# Only close the session if SDK created it (fn_get_session was not provided)
|
|
109
|
+
# If the caller provided fn_get_session, they are responsible for closing the session
|
|
110
|
+
if not self._caller_managed_session and self.session is not None:
|
|
85
111
|
await self.session.close()
|
|
86
112
|
|
|
87
113
|
@staticmethod
|
|
@@ -115,7 +141,7 @@ class RetryableAioHttpClient:
|
|
|
115
141
|
try:
|
|
116
142
|
if headers:
|
|
117
143
|
kwargs["headers"] = headers
|
|
118
|
-
# if there is no data then remove from kwargs so as not to confuse aiohttp
|
|
144
|
+
# if there is no data, then remove from kwargs so as not to confuse aiohttp
|
|
119
145
|
if "data" in kwargs and kwargs["data"] is None:
|
|
120
146
|
del kwargs["data"]
|
|
121
147
|
# compression and chunked can only be enabled if there is content sent
|
|
@@ -399,7 +425,7 @@ class RetryableAioHttpClient:
|
|
|
399
425
|
if retry_after_text:
|
|
400
426
|
# noinspection PyBroadException
|
|
401
427
|
try:
|
|
402
|
-
if retry_after_text.isnumeric(): # it is number of seconds
|
|
428
|
+
if retry_after_text.isnumeric(): # it is a number of seconds
|
|
403
429
|
await asyncio.sleep(int(retry_after_text))
|
|
404
430
|
else:
|
|
405
431
|
wait_till: datetime = datetime.strptime(retry_after_text, "%a, %d %b %Y %H:%M:%S GMT")
|
|
@@ -413,7 +439,7 @@ class RetryableAioHttpClient:
|
|
|
413
439
|
if time_diff > 0:
|
|
414
440
|
await asyncio.sleep(time_diff)
|
|
415
441
|
except Exception:
|
|
416
|
-
# if there was some exception parsing the Retry-After header, sleep for 60 seconds
|
|
442
|
+
# if there was some exception, parsing the Retry-After header, sleep for 60 seconds
|
|
417
443
|
await asyncio.sleep(60)
|
|
418
444
|
else:
|
|
419
445
|
await asyncio.sleep(60)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: helix.fhir.client.sdk
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.16
|
|
4
4
|
Summary: helix.fhir.client.sdk
|
|
5
5
|
Home-page: https://github.com/icanbwell/helix.fhir.client.sdk
|
|
6
6
|
Author: Imran Qureshi
|
|
@@ -111,9 +111,46 @@ response: Optional[FhirGetResponse] = await FhirGetResponse.from_async_generator
|
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
# Data Streaming
|
|
114
|
-
For FHIR servers that support data streaming (e.g., b.well FHIR server), you can just set the `use_data_streaming` parameter to stream the data as it
|
|
114
|
+
For FHIR servers that support data streaming (e.g., b.well FHIR server), you can just set the `use_data_streaming` parameter to stream the data as it is received.
|
|
115
115
|
The data will be streamed in AsyncGenerators as described above.
|
|
116
116
|
|
|
117
|
+
# Persistent Sessions (Connection Reuse)
|
|
118
|
+
By default, the SDK creates a new HTTP session for each request. For better performance (~4× faster),
|
|
119
|
+
you can use persistent sessions to reuse connections across multiple requests.
|
|
120
|
+
|
|
121
|
+
**Important**: When you provide a custom session factory using `use_http_session()`, YOU are responsible
|
|
122
|
+
for managing the session lifecycle, including closing it when done. The SDK will NOT automatically close
|
|
123
|
+
user-provided sessions.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import aiohttp
|
|
127
|
+
from helix_fhir_client_sdk.fhir_client import FhirClient
|
|
128
|
+
|
|
129
|
+
# Create a persistent session for connection reuse
|
|
130
|
+
session = aiohttp.ClientSession()
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Configure FhirClient to use persistent session
|
|
134
|
+
fhir_client = (
|
|
135
|
+
FhirClient()
|
|
136
|
+
.url("https://fhir.example.com")
|
|
137
|
+
.resource("Patient")
|
|
138
|
+
.use_http_session(lambda: session) # User provides session factory
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Multiple requests reuse the same connection (~4× performance boost)
|
|
142
|
+
response1 = await fhir_client.get_async()
|
|
143
|
+
response2 = await fhir_client.clone().resource("Observation").get_async()
|
|
144
|
+
|
|
145
|
+
finally:
|
|
146
|
+
# User must close the session when done
|
|
147
|
+
await session.close()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Session Lifecycle Rules**:
|
|
151
|
+
- **No custom factory** (default): SDK creates and closes the session automatically
|
|
152
|
+
- **Custom factory provided**: User is responsible for closing the session
|
|
153
|
+
|
|
117
154
|
# Storage Compression
|
|
118
155
|
The FHIR client SDK supports two types of compression:
|
|
119
156
|
|
|
@@ -3,12 +3,12 @@ helix_fhir_client_sdk/dictionary_parser.py,sha256=WrGkVAxMlUvVycRVrX7UZt2oP2e_Vk
|
|
|
3
3
|
helix_fhir_client_sdk/dictionary_writer.py,sha256=V7Bx9Z69s0LRYF6Lc6Xp0d-Gj0BnAVKA1vBuwf3JORE,1486
|
|
4
4
|
helix_fhir_client_sdk/fhir_auth_mixin.py,sha256=L-_fwQbyojv6AoRUYkPSXWEsW7NRInvJ3wwYgINEtJs,14351
|
|
5
5
|
helix_fhir_client_sdk/fhir_bundle_appender.py,sha256=t1hs7p_vXKC9MUFyUnN9dTuDhRF-kw-kkgVFtGHv9QQ,11749
|
|
6
|
-
helix_fhir_client_sdk/fhir_client.py,sha256=
|
|
7
|
-
helix_fhir_client_sdk/fhir_delete_mixin.py,sha256=
|
|
8
|
-
helix_fhir_client_sdk/fhir_merge_mixin.py,sha256=
|
|
6
|
+
helix_fhir_client_sdk/fhir_client.py,sha256=dHfkruJZ1uPM4cmKcnn4HqI0BCDP3IwIqXSLvCu4IsY,37346
|
|
7
|
+
helix_fhir_client_sdk/fhir_delete_mixin.py,sha256=IRcJ5AJ7yrsc1HbjqYW5-jAcIEwG__9nEqwbvvC8qJ0,7532
|
|
8
|
+
helix_fhir_client_sdk/fhir_merge_mixin.py,sha256=h2lvpLDs5nvt9F1bcpd110LFuFXI2HW-uKrUs5TbW7I,17783
|
|
9
9
|
helix_fhir_client_sdk/fhir_merge_resources_mixin.py,sha256=QO2SeZa4Co69f_2YCz0_ss2dJb4xhVMkRdzbjLIJ-iI,35799
|
|
10
|
-
helix_fhir_client_sdk/fhir_patch_mixin.py,sha256=
|
|
11
|
-
helix_fhir_client_sdk/fhir_update_mixin.py,sha256=
|
|
10
|
+
helix_fhir_client_sdk/fhir_patch_mixin.py,sha256=QLTsqhFLGi4gC_qQblnCA4skdiLw8h4QRlzvaeyFkl4,7249
|
|
11
|
+
helix_fhir_client_sdk/fhir_update_mixin.py,sha256=_Yx9yg809N8EXY07XlwHxSJLp-xIzpNg5erL92LOeww,7370
|
|
12
12
|
helix_fhir_client_sdk/function_types.py,sha256=x95j6ix3Xa9b276Q741xX1jguqBuFT6EBLDw35_EoVM,3916
|
|
13
13
|
helix_fhir_client_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
helix_fhir_client_sdk/well_known_configuration.py,sha256=hwKpqZoJHkHuCEOowoXk07ywEMMhr_rcmQHNKCUEgVk,221
|
|
@@ -38,8 +38,8 @@ helix_fhir_client_sdk/graph/test/test_graph_mixin.py,sha256=LNd4LVjryVLgzWeTXMDp
|
|
|
38
38
|
helix_fhir_client_sdk/graph/test/test_simulate_graph_processor_mixin.py,sha256=EQDfhqJfUrP6SptXRP7ayEN7g5cZQMA00ccXzeXiSXM,46312
|
|
39
39
|
helix_fhir_client_sdk/graph/test/test_simulate_graph_processor_mixin_caching.py,sha256=WFzKKHtKDcOOXjXRMKkRyJ64whoJoo9M9ST88ayvEbY,16176
|
|
40
40
|
helix_fhir_client_sdk/open_telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
-
helix_fhir_client_sdk/open_telemetry/attribute_names.py,sha256=
|
|
42
|
-
helix_fhir_client_sdk/open_telemetry/span_names.py,sha256=
|
|
41
|
+
helix_fhir_client_sdk/open_telemetry/attribute_names.py,sha256=mcPcgpaRe-hZDmPu8gLQo51E_-rllAk2OXMC9uK6EmM,328
|
|
42
|
+
helix_fhir_client_sdk/open_telemetry/span_names.py,sha256=sEuzUXxE9pSoAZti2YVifBqbo3r4SLTPlIUW4F2EuP0,548
|
|
43
43
|
helix_fhir_client_sdk/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
44
|
helix_fhir_client_sdk/queue/request_queue_mixin.py,sha256=Q5ZyadT2nMX6TATsiy0FLXzzCpTbVjN0Gh-B_F0RGCk,21684
|
|
45
45
|
helix_fhir_client_sdk/responses/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -104,7 +104,7 @@ helix_fhir_client_sdk/utilities/hash_util.py,sha256=YNUy7-IC_OtC0l-T45UO9UkA-_ps
|
|
|
104
104
|
helix_fhir_client_sdk/utilities/list_chunker.py,sha256=2h2k5CCFmOhICaugOx6UI-9dh4q5w1lVdF7WQLX0LCM,1456
|
|
105
105
|
helix_fhir_client_sdk/utilities/ndjson_chunk_streaming_parser.py,sha256=3TCYfWVCEpJbqRxqlSDsGnFnraO4T9bxzYdShvu6Pos,1954
|
|
106
106
|
helix_fhir_client_sdk/utilities/practitioner_generator.py,sha256=gneCAXNDNEphBY-Nc2nMQBbEWJgHcjvv3S8JQ75yiJI,3778
|
|
107
|
-
helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py,sha256=
|
|
107
|
+
helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py,sha256=8DdzqTmIY1hWlxmEXOxycBpa72wTo-vsAtcLO4hpnmQ,22605
|
|
108
108
|
helix_fhir_client_sdk/utilities/retryable_aiohttp_response.py,sha256=DvNX6WO1m2Hz6LoI5CwSPDECPd8oDsqRCVsyq_Oxf-0,3542
|
|
109
109
|
helix_fhir_client_sdk/utilities/retryable_aiohttp_url_result.py,sha256=Gdmvn6qIM2JF0YOhobQUHY41fCxvYyaths_CZs0iJfo,616
|
|
110
110
|
helix_fhir_client_sdk/utilities/url_checker.py,sha256=_JRSIvu7WNXh2OA79HJbEEiomGT-quGhAUGh44-9824,3580
|
|
@@ -130,7 +130,7 @@ helix_fhir_client_sdk/validators/async_fhir_validator.py,sha256=Bgiw5atbc5YzBYpk
|
|
|
130
130
|
helix_fhir_client_sdk/validators/fhir_validator.py,sha256=HWBldSEB9yeKIcnLcV8R-LoTzwT_OMu8SchtUUBKzys,2331
|
|
131
131
|
helix_fhir_client_sdk/validators/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
132
132
|
helix_fhir_client_sdk/validators/test/test_async_fhir_validator.py,sha256=RmSowjPUdZee5nYuYujghxWyqJ20cu7U0lJFtFT-ZBs,3285
|
|
133
|
-
helix_fhir_client_sdk-4.2.
|
|
133
|
+
helix_fhir_client_sdk-4.2.16.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
134
134
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
135
135
|
tests/logger_for_test.py,sha256=UC-7F6w6fDsUIYf37aRnvUdiUUVk8qkJEUSuO17NQnI,1525
|
|
136
136
|
tests/test_fhir_client_clone.py,sha256=c5y1rWJ32nBSUnK1FfyymY005dNowd4Nf1xrbuQolNk,5368
|
|
@@ -153,6 +153,7 @@ tests/async/test_async_real_fhir_server_get_patients.py,sha256=0oMnUJg1KEspJ5_4e
|
|
|
153
153
|
tests/async/test_async_real_fhir_server_get_patients_error.py,sha256=_s7chLogAg0yKgGpsq1o9_dDHBrzGaRWBAo8agFTN6U,1914
|
|
154
154
|
tests/async/test_benchmark_compress.py,sha256=q1gDG7qXvof-3uVAqJlZAW7uO8cR0vEeDfzl-iwIEtY,16470
|
|
155
155
|
tests/async/test_benchmark_merge.py,sha256=ME0Pow_IXpIaVGWvq3ii7dGltXcz-3DGxz2gGF4LmYQ,19830
|
|
156
|
+
tests/async/test_retryable_client_session_management.py,sha256=cOAE0wGkh3cv0AS187nujeial3gGEu1VlOJ5b9-LaCI,5360
|
|
156
157
|
tests/async/fhir_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
157
158
|
tests/async/fhir_server/test_async_real_fhir_server_get_graph_large.py,sha256=fM2MPF47nDF2Qwj2AkdTZ2CfvgUYGN4AVIS253KC9MQ,9430
|
|
158
159
|
tests/async/fhir_server/test_async_real_fhir_server_get_patients_large.py,sha256=rXRF8E8Al7XANCmef1d_WqxSA9TVQjVC7B41OZaEQlY,5583
|
|
@@ -212,7 +213,7 @@ tests_integration/test_emr_server_auth.py,sha256=2I4QUAspQN89uGf6JB2aVuYaBeDnRJz
|
|
|
212
213
|
tests_integration/test_firely_fhir.py,sha256=ll6-plwQrKfdrEyfbw0wLTC1jB-Qei1Mj-81tYTl5eQ,697
|
|
213
214
|
tests_integration/test_merge_vs_smart_merge_behavior.py,sha256=LrIuyxzw0YLaTjcRtG0jzy0M6xSv9qebmdBtMPDcacQ,3733
|
|
214
215
|
tests_integration/test_staging_server_graph.py,sha256=5RfMxjhdX9o4-n_ZRvze4Sm8u8NjRijRLDpqiz8qD_0,7132
|
|
215
|
-
helix_fhir_client_sdk-4.2.
|
|
216
|
-
helix_fhir_client_sdk-4.2.
|
|
217
|
-
helix_fhir_client_sdk-4.2.
|
|
218
|
-
helix_fhir_client_sdk-4.2.
|
|
216
|
+
helix_fhir_client_sdk-4.2.16.dist-info/METADATA,sha256=AHcaKMqC3ICIbjD_Fr8CXs8MxOXLzdyh3A76J-SVYMk,7210
|
|
217
|
+
helix_fhir_client_sdk-4.2.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
218
|
+
helix_fhir_client_sdk-4.2.16.dist-info/top_level.txt,sha256=BRnDS6ceQxs-4u2jXznATObgP8G2cGAerlH0ZS4sJ6M,46
|
|
219
|
+
helix_fhir_client_sdk-4.2.16.dist-info/RECORD,,
|