helix.fhir.client.sdk 4.2.18__py3-none-any.whl → 4.2.20__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/graph/simulated_graph_processor_mixin.py +147 -25
- helix_fhir_client_sdk/responses/fhir_get_response.py +2 -3
- helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +62 -28
- helix_fhir_client_sdk/responses/resource_separator.py +35 -40
- {helix_fhir_client_sdk-4.2.18.dist-info → helix_fhir_client_sdk-4.2.20.dist-info}/METADATA +1 -1
- {helix_fhir_client_sdk-4.2.18.dist-info → helix_fhir_client_sdk-4.2.20.dist-info}/RECORD +9 -9
- {helix_fhir_client_sdk-4.2.18.dist-info → helix_fhir_client_sdk-4.2.20.dist-info}/WHEEL +0 -0
- {helix_fhir_client_sdk-4.2.18.dist-info → helix_fhir_client_sdk-4.2.20.dist-info}/licenses/LICENSE +0 -0
- {helix_fhir_client_sdk-4.2.18.dist-info → helix_fhir_client_sdk-4.2.20.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import time
|
|
2
3
|
from abc import ABC
|
|
3
4
|
from collections.abc import AsyncGenerator
|
|
4
5
|
from datetime import UTC, datetime
|
|
@@ -123,6 +124,15 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
123
124
|
Yields:
|
|
124
125
|
FhirGetResponse objects representing retrieved resources
|
|
125
126
|
"""
|
|
127
|
+
|
|
128
|
+
profiling: dict[str, Any] = {
|
|
129
|
+
"function": "process_simulate_graph_async",
|
|
130
|
+
"start_time": time.perf_counter(),
|
|
131
|
+
"steps": {},
|
|
132
|
+
"extend_calls": [],
|
|
133
|
+
"append_calls": [],
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
# Validate graph definition input
|
|
127
137
|
assert graph_json, "Graph JSON must be provided"
|
|
128
138
|
graph_definition: GraphDefinition = GraphDefinition.from_dict(graph_json)
|
|
@@ -158,6 +168,7 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
158
168
|
cache: RequestCache = input_cache if input_cache is not None else RequestCache()
|
|
159
169
|
async with cache:
|
|
160
170
|
# Retrieve start resources based on graph definition
|
|
171
|
+
step_start = time.perf_counter()
|
|
161
172
|
start: str = graph_definition.start
|
|
162
173
|
parent_response: FhirGetResponse
|
|
163
174
|
cache_hits: int
|
|
@@ -171,10 +182,18 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
171
182
|
add_cached_bundles_to_result=add_cached_bundles_to_result,
|
|
172
183
|
compare_hash=compare_hash,
|
|
173
184
|
)
|
|
185
|
+
profiling["steps"]["get_start_resources"] = time.perf_counter() - step_start
|
|
174
186
|
|
|
175
187
|
# If no parent resources found, yield empty response and exit
|
|
176
188
|
parent_response_resource_count = parent_response.get_resource_count()
|
|
177
189
|
if parent_response_resource_count == 0:
|
|
190
|
+
profiling["total_time"] = time.perf_counter() - profiling["start_time"]
|
|
191
|
+
if logger:
|
|
192
|
+
logger.info(
|
|
193
|
+
f"[PROFILING] process_simulate_graph_async: total={profiling['total_time']:.3f}s, "
|
|
194
|
+
f"get_start_resources={profiling['steps'].get('get_start_resources', 0):.3f}s, "
|
|
195
|
+
f"no parent resources found"
|
|
196
|
+
)
|
|
178
197
|
yield parent_response
|
|
179
198
|
return # no resources to process
|
|
180
199
|
|
|
@@ -196,6 +215,7 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
196
215
|
)
|
|
197
216
|
|
|
198
217
|
# now process the graph links
|
|
218
|
+
step_start = time.perf_counter()
|
|
199
219
|
child_responses: list[FhirGetResponse] = []
|
|
200
220
|
parent_link_map: list[tuple[list[GraphDefinitionLink], FhirBundleEntryList]] = []
|
|
201
221
|
|
|
@@ -204,6 +224,7 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
204
224
|
parent_link_map.append((graph_definition.link, parent_bundle_entries))
|
|
205
225
|
|
|
206
226
|
# Process graph links in parallel
|
|
227
|
+
link_processing_count = 0
|
|
207
228
|
while len(parent_link_map):
|
|
208
229
|
new_parent_link_map: list[tuple[list[GraphDefinitionLink], FhirBundleEntryList]] = []
|
|
209
230
|
|
|
@@ -230,19 +251,38 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
230
251
|
add_cached_bundles_to_result=add_cached_bundles_to_result,
|
|
231
252
|
ifModifiedSince=ifModifiedSince,
|
|
232
253
|
):
|
|
254
|
+
# Track extend operation
|
|
255
|
+
extend_start = time.perf_counter()
|
|
233
256
|
child_responses.extend(link_responses)
|
|
257
|
+
extend_time = time.perf_counter() - extend_start
|
|
258
|
+
profiling["extend_calls"].append(
|
|
259
|
+
{"location": "child_responses.extend", "count": len(link_responses), "time": extend_time}
|
|
260
|
+
)
|
|
261
|
+
link_processing_count += 1
|
|
234
262
|
|
|
235
263
|
# Update parent link map for next iteration
|
|
236
264
|
parent_link_map = new_parent_link_map
|
|
237
265
|
|
|
266
|
+
profiling["steps"]["process_graph_links"] = time.perf_counter() - step_start
|
|
267
|
+
profiling["steps"]["link_processing_iterations"] = link_processing_count
|
|
268
|
+
|
|
238
269
|
# Combine and process responses
|
|
270
|
+
step_start = time.perf_counter()
|
|
239
271
|
parent_response = cast(FhirGetBundleResponse, parent_response.extend(child_responses))
|
|
272
|
+
extend_time = time.perf_counter() - step_start
|
|
273
|
+
profiling["steps"]["parent_response.extend"] = extend_time
|
|
274
|
+
profiling["extend_calls"].append(
|
|
275
|
+
{"location": "parent_response.extend", "count": len(child_responses), "time": extend_time}
|
|
276
|
+
)
|
|
240
277
|
|
|
241
278
|
# Optional resource sorting
|
|
242
279
|
if sort_resources:
|
|
280
|
+
step_start = time.perf_counter()
|
|
243
281
|
parent_response = parent_response.sort_resources()
|
|
282
|
+
profiling["steps"]["sort_resources"] = time.perf_counter() - step_start
|
|
244
283
|
|
|
245
284
|
# Prepare final response based on bundling preferences
|
|
285
|
+
step_start = time.perf_counter()
|
|
246
286
|
full_response: FhirGetResponse
|
|
247
287
|
if separate_bundle_resources:
|
|
248
288
|
full_response = FhirGetListByResourceTypeResponse.from_response(other_response=parent_response)
|
|
@@ -250,10 +290,38 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
250
290
|
full_response = FhirGetListResponse.from_response(other_response=parent_response)
|
|
251
291
|
else:
|
|
252
292
|
full_response = parent_response
|
|
293
|
+
profiling["steps"]["prepare_final_response"] = time.perf_counter() - step_start
|
|
253
294
|
|
|
254
295
|
# Set response URL
|
|
255
296
|
full_response.url = url or parent_response.url
|
|
256
297
|
|
|
298
|
+
# Calculate profiling summary
|
|
299
|
+
profiling["total_time"] = time.perf_counter() - profiling["start_time"]
|
|
300
|
+
total_extend_time = sum(call["time"] for call in profiling["extend_calls"])
|
|
301
|
+
total_extend_count = sum(call["count"] for call in profiling["extend_calls"])
|
|
302
|
+
|
|
303
|
+
# Log profiling information
|
|
304
|
+
if logger:
|
|
305
|
+
logger.info(
|
|
306
|
+
f"[PROFILING] process_simulate_graph_async for id={id_}: "
|
|
307
|
+
f"total={profiling['total_time']:.3f}s, "
|
|
308
|
+
f"get_start_resources={profiling['steps'].get('get_start_resources', 0):.3f}s, "
|
|
309
|
+
f"process_graph_links={profiling['steps'].get('process_graph_links', 0):.3f}s, "
|
|
310
|
+
f"parent_response.extend={profiling['steps'].get('parent_response.extend', 0):.3f}s, "
|
|
311
|
+
f"sort_resources={profiling['steps'].get('sort_resources', 0):.3f}s, "
|
|
312
|
+
f"prepare_final_response={profiling['steps'].get('prepare_final_response', 0):.3f}s"
|
|
313
|
+
)
|
|
314
|
+
logger.info(
|
|
315
|
+
f"[PROFILING] process_simulate_graph_async extend operations: "
|
|
316
|
+
f"total_calls={len(profiling['extend_calls'])}, "
|
|
317
|
+
f"total_items={total_extend_count}, "
|
|
318
|
+
f"total_time={total_extend_time:.3f}s"
|
|
319
|
+
)
|
|
320
|
+
for call in profiling["extend_calls"]:
|
|
321
|
+
logger.info(
|
|
322
|
+
f"[PROFILING] extend at {call['location']}: items={call['count']}, time={call['time']:.3f}s"
|
|
323
|
+
)
|
|
324
|
+
|
|
257
325
|
# Log cache performance
|
|
258
326
|
if logger:
|
|
259
327
|
logger.info(
|
|
@@ -276,26 +344,9 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
276
344
|
) -> list[FhirGetResponse]:
|
|
277
345
|
"""
|
|
278
346
|
Parallel processing function for graph definition links.
|
|
279
|
-
|
|
280
|
-
This method is designed to be used with AsyncParallelProcessor to process
|
|
281
|
-
graph links concurrently, improving performance for complex FHIR resource
|
|
282
|
-
graph traversals.
|
|
283
|
-
|
|
284
|
-
Key Responsibilities:
|
|
285
|
-
- Process individual graph links in parallel
|
|
286
|
-
- Track and log processing details
|
|
287
|
-
- Handle resource retrieval for each link
|
|
288
|
-
- Manage parallel processing context
|
|
289
|
-
|
|
290
|
-
Args:
|
|
291
|
-
context: Parallel processing context information
|
|
292
|
-
row: Current GraphDefinitionLink being processed
|
|
293
|
-
parameters: Parameters for link processing
|
|
294
|
-
additional_parameters: Extra parameters for extended processing
|
|
295
|
-
|
|
296
|
-
Returns:
|
|
297
|
-
List of FhirGetResponse objects retrieved during link processing
|
|
298
347
|
"""
|
|
348
|
+
profiling_start = time.perf_counter()
|
|
349
|
+
|
|
299
350
|
# Record the start time for performance tracking
|
|
300
351
|
start_time: datetime = datetime.now()
|
|
301
352
|
|
|
@@ -326,11 +377,8 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
326
377
|
logger=parameters.logger,
|
|
327
378
|
cache=parameters.cache,
|
|
328
379
|
scope_parser=parameters.scope_parser,
|
|
329
|
-
# Handle parent link map from additional parameters
|
|
330
380
|
parent_link_map=(additional_parameters["parent_link_map"] if additional_parameters else []),
|
|
331
|
-
# Determine request size, default to 1 if not specified
|
|
332
381
|
request_size=(additional_parameters["request_size"] if additional_parameters else 1),
|
|
333
|
-
# Track unsupported resources for ID-based search
|
|
334
382
|
id_search_unsupported_resources=(
|
|
335
383
|
additional_parameters["id_search_unsupported_resources"] if additional_parameters else []
|
|
336
384
|
),
|
|
@@ -346,6 +394,8 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
346
394
|
# Record end time for performance tracking
|
|
347
395
|
end_time: datetime = datetime.now()
|
|
348
396
|
|
|
397
|
+
total_time = time.perf_counter() - profiling_start
|
|
398
|
+
|
|
349
399
|
# Log detailed processing information
|
|
350
400
|
if parameters.logger:
|
|
351
401
|
parameters.logger.debug(
|
|
@@ -357,6 +407,11 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
357
407
|
+ f" | duration: {end_time - start_time}"
|
|
358
408
|
+ f" | resource_count: {len(result)}"
|
|
359
409
|
)
|
|
410
|
+
parameters.logger.info(
|
|
411
|
+
f"[PROFILING] process_link_async_parallel_function for path={row.path}: "
|
|
412
|
+
f"total={total_time:.3f}s, "
|
|
413
|
+
f"results={len(result)}"
|
|
414
|
+
)
|
|
360
415
|
|
|
361
416
|
# Return the list of retrieved responses
|
|
362
417
|
return result
|
|
@@ -842,9 +897,18 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
842
897
|
logger: Logger | None,
|
|
843
898
|
compare_hash: bool = True,
|
|
844
899
|
) -> FhirGetResponse | None:
|
|
900
|
+
profiling_start = time.perf_counter()
|
|
901
|
+
http_request_time = 0.0
|
|
902
|
+
http_request_count = 0
|
|
903
|
+
cache_check_time = 0.0
|
|
904
|
+
cache_update_time = 0.0
|
|
905
|
+
append_time = 0.0
|
|
906
|
+
|
|
845
907
|
result: FhirGetResponse | None = None
|
|
846
908
|
non_cached_id_list: list[str] = []
|
|
909
|
+
|
|
847
910
|
# first check to see if we can find these in the cache
|
|
911
|
+
cache_check_start = time.perf_counter()
|
|
848
912
|
if ids:
|
|
849
913
|
for resource_id in ids:
|
|
850
914
|
cache_entry: RequestCacheEntry | None = await cache.get_async(
|
|
@@ -857,9 +921,12 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
857
921
|
if logger:
|
|
858
922
|
logger.info(f"Cache entry not found for {resource_type}/{resource_id} (1by1)")
|
|
859
923
|
non_cached_id_list.append(resource_id)
|
|
924
|
+
cache_check_time = time.perf_counter() - cache_check_start
|
|
860
925
|
|
|
926
|
+
cache_update_start = time.perf_counter()
|
|
861
927
|
for single_id in non_cached_id_list:
|
|
862
928
|
result2: FhirGetResponse
|
|
929
|
+
http_start = time.perf_counter()
|
|
863
930
|
async for result2 in self._get_with_session_async(
|
|
864
931
|
page_number=None,
|
|
865
932
|
ids=[single_id],
|
|
@@ -868,10 +935,15 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
868
935
|
fn_handle_streaming_chunk=None,
|
|
869
936
|
resource_type=resource_type,
|
|
870
937
|
):
|
|
938
|
+
http_request_time += time.perf_counter() - http_start
|
|
939
|
+
http_request_count += 1
|
|
940
|
+
|
|
871
941
|
if result2.resource_type == "OperationOutcome":
|
|
872
942
|
result2 = FhirGetErrorResponse.from_response(other_response=result2)
|
|
873
943
|
if result:
|
|
944
|
+
append_start = time.perf_counter()
|
|
874
945
|
result = result.append(result2)
|
|
946
|
+
append_time += time.perf_counter() - append_start
|
|
875
947
|
else:
|
|
876
948
|
result = result2
|
|
877
949
|
if result2.successful:
|
|
@@ -905,6 +977,21 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
905
977
|
)
|
|
906
978
|
if cache_updated and logger:
|
|
907
979
|
logger.info(f"Inserted {result2.status} for {resource_type}/{single_id} into cache (1by1)")
|
|
980
|
+
cache_update_time = time.perf_counter() - cache_update_start - http_request_time - append_time
|
|
981
|
+
|
|
982
|
+
total_time = time.perf_counter() - profiling_start
|
|
983
|
+
processing_time = total_time - http_request_time - cache_check_time - cache_update_time - append_time
|
|
984
|
+
|
|
985
|
+
if logger and http_request_count > 0:
|
|
986
|
+
logger.info(
|
|
987
|
+
f"[PROFILING] _get_resources_by_id_one_by_one_async for {resource_type}: "
|
|
988
|
+
f"total={total_time:.3f}s, "
|
|
989
|
+
f"http_requests={http_request_time:.3f}s ({http_request_count} calls), "
|
|
990
|
+
f"cache_check={cache_check_time:.3f}s, "
|
|
991
|
+
f"cache_update={cache_update_time:.3f}s, "
|
|
992
|
+
f"append={append_time:.3f}s, "
|
|
993
|
+
f"processing={processing_time:.3f}s"
|
|
994
|
+
)
|
|
908
995
|
|
|
909
996
|
return result
|
|
910
997
|
|
|
@@ -921,6 +1008,13 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
921
1008
|
add_cached_bundles_to_result: bool = True,
|
|
922
1009
|
compare_hash: bool = True,
|
|
923
1010
|
) -> tuple[FhirGetResponse, int]:
|
|
1011
|
+
profiling_start = time.perf_counter()
|
|
1012
|
+
http_request_time = 0.0
|
|
1013
|
+
http_request_count = 0
|
|
1014
|
+
cache_check_time = 0.0
|
|
1015
|
+
cache_update_time = 0.0
|
|
1016
|
+
append_time = 0.0
|
|
1017
|
+
|
|
924
1018
|
assert resource_type
|
|
925
1019
|
if not scope_parser.scope_allows(resource_type=resource_type):
|
|
926
1020
|
if logger:
|
|
@@ -954,14 +1048,13 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
954
1048
|
|
|
955
1049
|
non_cached_id_list: list[str] = []
|
|
956
1050
|
# get any cached resources
|
|
1051
|
+
cache_check_start = time.perf_counter()
|
|
957
1052
|
if id_list:
|
|
958
1053
|
for resource_id in id_list:
|
|
959
1054
|
cache_entry: RequestCacheEntry | None = await cache.get_async(
|
|
960
1055
|
resource_type=resource_type, resource_id=resource_id
|
|
961
1056
|
)
|
|
962
1057
|
if cache_entry:
|
|
963
|
-
# if there is an entry then it means we tried to get it in the past
|
|
964
|
-
# so don't get it again whether we were successful or not
|
|
965
1058
|
if logger:
|
|
966
1059
|
logger.info(
|
|
967
1060
|
f"{cache_entry.status} Returning {resource_type}/{resource_id} from cache (ByParam)"
|
|
@@ -970,6 +1063,7 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
970
1063
|
if logger:
|
|
971
1064
|
logger.info(f"Cache entry not found for {resource_type}/{resource_id} (ByParam)")
|
|
972
1065
|
non_cached_id_list.append(resource_id)
|
|
1066
|
+
cache_check_time = time.perf_counter() - cache_check_start
|
|
973
1067
|
|
|
974
1068
|
all_result: FhirGetResponse | None = None
|
|
975
1069
|
# either we have non-cached ids or this is a query without id but has other parameters
|
|
@@ -981,6 +1075,7 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
981
1075
|
# call the server to get the resources
|
|
982
1076
|
result1: FhirGetResponse
|
|
983
1077
|
result: FhirGetResponse | None
|
|
1078
|
+
http_start = time.perf_counter()
|
|
984
1079
|
async for result1 in self._get_with_session_async(
|
|
985
1080
|
page_number=None,
|
|
986
1081
|
ids=non_cached_id_list,
|
|
@@ -989,6 +1084,8 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
989
1084
|
fn_handle_streaming_chunk=None,
|
|
990
1085
|
resource_type=resource_type,
|
|
991
1086
|
):
|
|
1087
|
+
http_request_time += time.perf_counter() - http_start
|
|
1088
|
+
http_request_count += 1
|
|
992
1089
|
result = result1
|
|
993
1090
|
# if we got a failure then check if we can get it one by one
|
|
994
1091
|
if (not result or result.status != 200) and len(non_cached_id_list) > 1:
|
|
@@ -1001,6 +1098,7 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
1001
1098
|
f" Fetching one by one ids: {non_cached_id_list}"
|
|
1002
1099
|
)
|
|
1003
1100
|
# For some resources if search by _id doesn't work then fetch one by one.
|
|
1101
|
+
one_by_one_start = time.perf_counter()
|
|
1004
1102
|
result = await self._get_resources_by_id_one_by_one_async(
|
|
1005
1103
|
resource_type=resource_type,
|
|
1006
1104
|
ids=non_cached_id_list,
|
|
@@ -1009,6 +1107,9 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
1009
1107
|
logger=logger,
|
|
1010
1108
|
compare_hash=compare_hash,
|
|
1011
1109
|
)
|
|
1110
|
+
one_by_one_time = time.perf_counter() - one_by_one_start
|
|
1111
|
+
http_request_time += one_by_one_time
|
|
1112
|
+
http_request_count += len(non_cached_id_list)
|
|
1012
1113
|
else:
|
|
1013
1114
|
if logger:
|
|
1014
1115
|
logger.info(f"Fetched {resource_type} resources using _id for url {self._url}")
|
|
@@ -1024,11 +1125,14 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
1024
1125
|
|
|
1025
1126
|
# append to the response
|
|
1026
1127
|
if all_result:
|
|
1128
|
+
append_start = time.perf_counter()
|
|
1027
1129
|
all_result = all_result.append(result)
|
|
1130
|
+
append_time += time.perf_counter() - append_start
|
|
1028
1131
|
else:
|
|
1029
1132
|
all_result = result
|
|
1030
1133
|
# If non_cached_id_list is not empty and resource_type does not support ?_id search then fetch it one by one
|
|
1031
1134
|
elif len(non_cached_id_list):
|
|
1135
|
+
one_by_one_start = time.perf_counter()
|
|
1032
1136
|
all_result = await self._get_resources_by_id_one_by_one_async(
|
|
1033
1137
|
resource_type=resource_type,
|
|
1034
1138
|
ids=non_cached_id_list,
|
|
@@ -1037,10 +1141,13 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
1037
1141
|
logger=logger,
|
|
1038
1142
|
compare_hash=compare_hash,
|
|
1039
1143
|
)
|
|
1144
|
+
http_request_time += time.perf_counter() - one_by_one_start
|
|
1145
|
+
http_request_count += len(non_cached_id_list)
|
|
1040
1146
|
|
|
1041
1147
|
# This list tracks the non-cached ids that were found
|
|
1042
1148
|
found_non_cached_id_list: list[str] = []
|
|
1043
1149
|
# Cache the fetched entries
|
|
1150
|
+
cache_update_start = time.perf_counter()
|
|
1044
1151
|
if all_result:
|
|
1045
1152
|
non_cached_bundle_entry: FhirBundleEntry
|
|
1046
1153
|
for non_cached_bundle_entry in all_result.get_bundle_entries():
|
|
@@ -1074,7 +1181,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
1074
1181
|
logger.debug(f"Inserted {resource_type}/{non_cached_resource_id} into cache (ByParam)")
|
|
1075
1182
|
found_non_cached_id_list.append(non_cached_resource_id)
|
|
1076
1183
|
|
|
1077
|
-
# now add all the non-cached ids that were NOT found to the cache too so we don't look for them again
|
|
1078
1184
|
for non_cached_id in non_cached_id_list:
|
|
1079
1185
|
if non_cached_id not in found_non_cached_id_list:
|
|
1080
1186
|
cache_updated = await cache.add_async(
|
|
@@ -1089,6 +1195,7 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
1089
1195
|
)
|
|
1090
1196
|
if cache_updated and logger:
|
|
1091
1197
|
logger.info(f"Inserted 404 for {resource_type}/{non_cached_id} into cache (ByParam)")
|
|
1198
|
+
cache_update_time = time.perf_counter() - cache_update_start
|
|
1092
1199
|
|
|
1093
1200
|
bundle_response: FhirGetBundleResponse = (
|
|
1094
1201
|
FhirGetBundleResponse.from_response(other_response=all_result)
|
|
@@ -1130,6 +1237,21 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
|
|
|
1130
1237
|
storage_mode=self._storage_mode,
|
|
1131
1238
|
)
|
|
1132
1239
|
)
|
|
1240
|
+
|
|
1241
|
+
total_time = time.perf_counter() - profiling_start
|
|
1242
|
+
processing_time = total_time - http_request_time - cache_check_time - cache_update_time - append_time
|
|
1243
|
+
|
|
1244
|
+
if logger and http_request_count > 0:
|
|
1245
|
+
logger.info(
|
|
1246
|
+
f"[PROFILING] _get_resources_by_parameters_async for {resource_type}: "
|
|
1247
|
+
f"total={total_time:.3f}s, "
|
|
1248
|
+
f"http_requests={http_request_time:.3f}s ({http_request_count} calls), "
|
|
1249
|
+
f"cache_check={cache_check_time:.3f}s, "
|
|
1250
|
+
f"cache_update={cache_update_time:.3f}s, "
|
|
1251
|
+
f"append={append_time:.3f}s, "
|
|
1252
|
+
f"processing={processing_time:.3f}s"
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1133
1255
|
return bundle_response, cache.cache_hits
|
|
1134
1256
|
|
|
1135
1257
|
# noinspection PyPep8Naming
|
|
@@ -170,9 +170,8 @@ class FhirGetResponse:
|
|
|
170
170
|
"""
|
|
171
171
|
result: FhirGetResponse = self._extend(others=others)
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
result.chunk_number = latest_chunk_number[0]
|
|
173
|
+
if others and others[-1].chunk_number:
|
|
174
|
+
result.chunk_number = others[-1].chunk_number
|
|
176
175
|
return result
|
|
177
176
|
|
|
178
177
|
@abstractmethod
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import logging
|
|
2
3
|
from collections.abc import AsyncGenerator, Generator
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from logging import Logger
|
|
@@ -132,8 +133,21 @@ class FhirGetBundleResponse(FhirGetResponse):
|
|
|
132
133
|
:param others: list of FhirGetResponse objects
|
|
133
134
|
:return: self
|
|
134
135
|
"""
|
|
136
|
+
if not others:
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
all_entries: list[FhirBundleEntry] = []
|
|
135
140
|
for other_response in others:
|
|
136
|
-
|
|
141
|
+
other_response_entries: FhirBundleEntryList = other_response.get_bundle_entries()
|
|
142
|
+
if len(other_response_entries) > 0:
|
|
143
|
+
all_entries.extend(other_response_entries)
|
|
144
|
+
|
|
145
|
+
# Now extend with all entries at once instead of one response at a time
|
|
146
|
+
if all_entries:
|
|
147
|
+
if self._bundle_entries is None:
|
|
148
|
+
self._bundle_entries = FhirBundleEntryList(all_entries)
|
|
149
|
+
else:
|
|
150
|
+
self._bundle_entries.extend(all_entries)
|
|
137
151
|
|
|
138
152
|
return self
|
|
139
153
|
|
|
@@ -311,36 +325,56 @@ class FhirGetBundleResponse(FhirGetResponse):
|
|
|
311
325
|
) -> "FhirGetBundleResponse":
|
|
312
326
|
"""
|
|
313
327
|
Removes the entries in the cache from the bundle
|
|
314
|
-
|
|
315
|
-
:param request_cache: RequestCache object to remove the entries from
|
|
316
|
-
:param compare_hash: if True, compare the hash of the resource with the hash in the cache
|
|
317
|
-
:return: self
|
|
318
328
|
"""
|
|
319
|
-
#
|
|
329
|
+
# Build a lookup of cache entries by (resource_type, id)
|
|
330
|
+
cache_map: dict[tuple[str, str], str | None] = {}
|
|
320
331
|
async for cached_entry in request_cache.get_entries_async():
|
|
321
|
-
if cached_entry.from_input_cache:
|
|
322
|
-
|
|
323
|
-
if
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
332
|
+
if cached_entry.from_input_cache and cached_entry.resource_type and cached_entry.id_:
|
|
333
|
+
cache_map[(cached_entry.resource_type, cached_entry.id_)] = (
|
|
334
|
+
cached_entry.raw_hash if compare_hash else None
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if not cache_map:
|
|
338
|
+
return self
|
|
339
|
+
|
|
340
|
+
resource_hash = ResourceHash() if compare_hash else None
|
|
341
|
+
removed_entries: list[FhirBundleEntry] = []
|
|
342
|
+
|
|
343
|
+
def should_remove(entry: FhirBundleEntry) -> bool:
|
|
344
|
+
resource = entry.resource
|
|
345
|
+
if not resource or resource.id is None or resource.resource_type is None:
|
|
346
|
+
return False
|
|
347
|
+
key = (resource.resource_type, resource.id)
|
|
348
|
+
if key not in cache_map:
|
|
349
|
+
return False
|
|
350
|
+
if not compare_hash:
|
|
351
|
+
return True
|
|
352
|
+
# Compare normalized JSON hash
|
|
353
|
+
if resource_hash is None:
|
|
354
|
+
return False
|
|
355
|
+
try:
|
|
356
|
+
entry_hash = resource_hash.hash_value(json.dumps(json.loads(resource.json()), sort_keys=True))
|
|
357
|
+
return entry_hash == cache_map[key]
|
|
358
|
+
except Exception:
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
# One pass filter; rebuild list to avoid many deque.remove calls
|
|
362
|
+
kept: list[FhirBundleEntry] = []
|
|
363
|
+
for entry in self._bundle_entries:
|
|
364
|
+
if should_remove(entry):
|
|
365
|
+
removed_entries.append(entry)
|
|
366
|
+
else:
|
|
367
|
+
kept.append(entry)
|
|
368
|
+
|
|
369
|
+
if logger and removed_entries and logger.isEnabledFor(logging.DEBUG):
|
|
370
|
+
for entry in removed_entries:
|
|
371
|
+
if entry.resource:
|
|
372
|
+
logger.debug(
|
|
373
|
+
f"Removing entry from bundle with id {entry.resource.id} and resource "
|
|
374
|
+
f"type {entry.resource.resource_type}"
|
|
375
|
+
)
|
|
343
376
|
|
|
377
|
+
self._bundle_entries = FhirBundleEntryList(kept)
|
|
344
378
|
return self
|
|
345
379
|
|
|
346
380
|
@classmethod
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
-
from copy import deepcopy
|
|
3
2
|
from typing import Any, cast
|
|
4
3
|
|
|
5
4
|
|
|
@@ -27,46 +26,42 @@ class ResourceSeparator:
|
|
|
27
26
|
extra_context_to_return: dict[str, Any] | None,
|
|
28
27
|
) -> ResourceSeparatorResult:
|
|
29
28
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
:param resources: The resources list.
|
|
33
|
-
:param access_token: The access token.
|
|
34
|
-
:param url: The URL.
|
|
35
|
-
:param extra_context_to_return: The extra context to return.
|
|
36
|
-
|
|
37
|
-
:return: None
|
|
29
|
+
Separate contained resources without copying or mutating input resources.
|
|
38
30
|
"""
|
|
39
31
|
resources_dicts: list[dict[str, str | None | list[dict[str, Any]]]] = []
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
for contained_resource in
|
|
60
|
-
|
|
61
|
-
if
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
32
|
+
total_resource_count: int = 0
|
|
33
|
+
|
|
34
|
+
for parent_resource in resources:
|
|
35
|
+
resource_type_value = parent_resource.get("resourceType")
|
|
36
|
+
if not resource_type_value:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
resource_type_key = str(resource_type_value).lower()
|
|
40
|
+
resource_map: dict[str, str | None | list[dict[str, Any]]] = {}
|
|
41
|
+
|
|
42
|
+
# Add parent resource
|
|
43
|
+
parent_list = cast(list[dict[str, Any]], resource_map.setdefault(resource_type_key, []))
|
|
44
|
+
parent_list.append(parent_resource)
|
|
45
|
+
total_resource_count += 1
|
|
46
|
+
|
|
47
|
+
# Add contained resources (if present) without mutating parent
|
|
48
|
+
contained_list = parent_resource.get("contained")
|
|
49
|
+
if isinstance(contained_list, list) and contained_list:
|
|
50
|
+
total_resource_count += len(contained_list)
|
|
51
|
+
for contained_resource in contained_list:
|
|
52
|
+
contained_type_value = contained_resource.get("resourceType")
|
|
53
|
+
if not contained_type_value:
|
|
54
|
+
continue
|
|
55
|
+
contained_type_key = str(contained_type_value).lower()
|
|
56
|
+
contained_list_out = cast(list[dict[str, Any]], resource_map.setdefault(contained_type_key, []))
|
|
57
|
+
contained_list_out.append(contained_resource)
|
|
58
|
+
|
|
59
|
+
# Context
|
|
60
|
+
resource_map["token"] = access_token
|
|
61
|
+
resource_map["url"] = url
|
|
68
62
|
if extra_context_to_return:
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
resource_map.update(extra_context_to_return)
|
|
64
|
+
|
|
65
|
+
resources_dicts.append(resource_map)
|
|
71
66
|
|
|
72
|
-
return ResourceSeparatorResult(resources_dicts=resources_dicts, total_count=
|
|
67
|
+
return ResourceSeparatorResult(resources_dicts=resources_dicts, total_count=total_resource_count)
|
|
@@ -32,7 +32,7 @@ helix_fhir_client_sdk/graph/fhir_graph_mixin.py,sha256=z0j9FmO2bOnmzgQmczfkWC70u
|
|
|
32
32
|
helix_fhir_client_sdk/graph/graph_definition.py,sha256=FTa1GLjJ6oooAhNw7SPk-Y8duB-5WtJtnwADao-afaI,3878
|
|
33
33
|
helix_fhir_client_sdk/graph/graph_link_parameters.py,sha256=3rknHL6SBgpT2A1fr-AikEFrR_9nIJUotZ82XFzROLo,599
|
|
34
34
|
helix_fhir_client_sdk/graph/graph_target_parameters.py,sha256=fdYQpPZxDnyWyevuwDwxeTXOJoE2PgS5QhPaXpwtFcU,705
|
|
35
|
-
helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py,sha256=
|
|
35
|
+
helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py,sha256=CNXlqjkdrebypCY4JHmz1XbGT3kpcSpich9ourE76H0,66430
|
|
36
36
|
helix_fhir_client_sdk/graph/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
37
|
helix_fhir_client_sdk/graph/test/test_graph_mixin.py,sha256=LNd4LVjryVLgzWeTXMDpsbdauXl7u3LMfj9irnNfb_k,5469
|
|
38
38
|
helix_fhir_client_sdk/graph/test/test_simulate_graph_processor_mixin.py,sha256=EQDfhqJfUrP6SptXRP7ayEN7g5cZQMA00ccXzeXiSXM,46312
|
|
@@ -46,15 +46,15 @@ helix_fhir_client_sdk/responses/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
|
46
46
|
helix_fhir_client_sdk/responses/bundle_expander.py,sha256=ilR5eMgciSgzsdQvKB6bHtn9jtpvn3uS-EBz-hrahzo,1065
|
|
47
47
|
helix_fhir_client_sdk/responses/fhir_client_protocol.py,sha256=IWM7LNQ1ZbgaySLGqpCwWAKyArT5HgBdNhkmSEivNMo,7100
|
|
48
48
|
helix_fhir_client_sdk/responses/fhir_delete_response.py,sha256=0K11vyfZ0LtL-G61NHzDqHrZgEHjMVZr00VWpWcktZA,2941
|
|
49
|
-
helix_fhir_client_sdk/responses/fhir_get_response.py,sha256=
|
|
49
|
+
helix_fhir_client_sdk/responses/fhir_get_response.py,sha256=v0ndKFb6o8PNqYYxGlLETzQnxGmwK3YympH4gotXpY4,17550
|
|
50
50
|
helix_fhir_client_sdk/responses/fhir_merge_response.py,sha256=uvUjEGJgMDlAkcc5_LjgAsTFZqREQMlV79sbxUZwtwE,2862
|
|
51
51
|
helix_fhir_client_sdk/responses/fhir_response_processor.py,sha256=fOSvqWjVI1BA6aDxxu9aqEvcKxtnFRmU2hMrgtmUCUM,34056
|
|
52
52
|
helix_fhir_client_sdk/responses/fhir_update_response.py,sha256=_6zZz85KQP69WFxejlX8BBWAKWtzsMGSJjR_zqhl_m4,2727
|
|
53
53
|
helix_fhir_client_sdk/responses/get_result.py,sha256=hkbZeu9h-01ZZckAuckn6UDR9GXGgRAIiKEN6ELRj80,1252
|
|
54
54
|
helix_fhir_client_sdk/responses/paging_result.py,sha256=tpmfdgrtaAmmViVxlw-EBHoe0PVjSQW9zicwRmhUVpI,1360
|
|
55
|
-
helix_fhir_client_sdk/responses/resource_separator.py,sha256=
|
|
55
|
+
helix_fhir_client_sdk/responses/resource_separator.py,sha256=7Ic0_SPNKCBAk6l07Ke2-AoObkBMnKdmtbbtBBCtVjE,2606
|
|
56
56
|
helix_fhir_client_sdk/responses/get/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
|
-
helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py,sha256=
|
|
57
|
+
helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py,sha256=PA1bzR4DkF91uTBM5tZCZ_AB0ovE49EJjq4O-1-Ya90,19357
|
|
58
58
|
helix_fhir_client_sdk/responses/get/fhir_get_error_response.py,sha256=oNdKs_r7K4qRHD7fyeDhZz3v0wGTT2esAPPtDhxOyeI,12325
|
|
59
59
|
helix_fhir_client_sdk/responses/get/fhir_get_list_by_resource_type_response.py,sha256=ssfb1IB2QTvqwWRzFs_VqPKH6mwcnWNo3iVh82M-Jso,13775
|
|
60
60
|
helix_fhir_client_sdk/responses/get/fhir_get_list_response.py,sha256=KT5g6MjB9yWWUaSZpx1jK9Tm2yVmcFyZMHBBnDDAPtU,11858
|
|
@@ -130,7 +130,7 @@ helix_fhir_client_sdk/validators/async_fhir_validator.py,sha256=i1BC98hZF6JhMQQz
|
|
|
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.20.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
|
|
@@ -213,7 +213,7 @@ tests_integration/test_emr_server_auth.py,sha256=2I4QUAspQN89uGf6JB2aVuYaBeDnRJz
|
|
|
213
213
|
tests_integration/test_firely_fhir.py,sha256=ll6-plwQrKfdrEyfbw0wLTC1jB-Qei1Mj-81tYTl5eQ,697
|
|
214
214
|
tests_integration/test_merge_vs_smart_merge_behavior.py,sha256=LrIuyxzw0YLaTjcRtG0jzy0M6xSv9qebmdBtMPDcacQ,3733
|
|
215
215
|
tests_integration/test_staging_server_graph.py,sha256=5RfMxjhdX9o4-n_ZRvze4Sm8u8NjRijRLDpqiz8qD_0,7132
|
|
216
|
-
helix_fhir_client_sdk-4.2.
|
|
217
|
-
helix_fhir_client_sdk-4.2.
|
|
218
|
-
helix_fhir_client_sdk-4.2.
|
|
219
|
-
helix_fhir_client_sdk-4.2.
|
|
216
|
+
helix_fhir_client_sdk-4.2.20.dist-info/METADATA,sha256=A2csXjl7i8RJGVOVOi3hJgkOm68CGpDUNudPitzq_EA,7210
|
|
217
|
+
helix_fhir_client_sdk-4.2.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
218
|
+
helix_fhir_client_sdk-4.2.20.dist-info/top_level.txt,sha256=BRnDS6ceQxs-4u2jXznATObgP8G2cGAerlH0ZS4sJ6M,46
|
|
219
|
+
helix_fhir_client_sdk-4.2.20.dist-info/RECORD,,
|
|
File without changes
|
{helix_fhir_client_sdk-4.2.18.dist-info → helix_fhir_client_sdk-4.2.20.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{helix_fhir_client_sdk-4.2.18.dist-info → helix_fhir_client_sdk-4.2.20.dist-info}/top_level.txt
RENAMED
|
File without changes
|