helix.fhir.client.sdk 4.2.20__py3-none-any.whl → 4.2.22__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.
@@ -1,5 +1,4 @@
1
1
  import json
2
- import time
3
2
  from abc import ABC
4
3
  from collections.abc import AsyncGenerator
5
4
  from datetime import UTC, datetime
@@ -124,15 +123,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
124
123
  Yields:
125
124
  FhirGetResponse objects representing retrieved resources
126
125
  """
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
-
136
126
  # Validate graph definition input
137
127
  assert graph_json, "Graph JSON must be provided"
138
128
  graph_definition: GraphDefinition = GraphDefinition.from_dict(graph_json)
@@ -168,7 +158,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
168
158
  cache: RequestCache = input_cache if input_cache is not None else RequestCache()
169
159
  async with cache:
170
160
  # Retrieve start resources based on graph definition
171
- step_start = time.perf_counter()
172
161
  start: str = graph_definition.start
173
162
  parent_response: FhirGetResponse
174
163
  cache_hits: int
@@ -182,18 +171,10 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
182
171
  add_cached_bundles_to_result=add_cached_bundles_to_result,
183
172
  compare_hash=compare_hash,
184
173
  )
185
- profiling["steps"]["get_start_resources"] = time.perf_counter() - step_start
186
174
 
187
175
  # If no parent resources found, yield empty response and exit
188
176
  parent_response_resource_count = parent_response.get_resource_count()
189
177
  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
- )
197
178
  yield parent_response
198
179
  return # no resources to process
199
180
 
@@ -215,7 +196,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
215
196
  )
216
197
 
217
198
  # now process the graph links
218
- step_start = time.perf_counter()
219
199
  child_responses: list[FhirGetResponse] = []
220
200
  parent_link_map: list[tuple[list[GraphDefinitionLink], FhirBundleEntryList]] = []
221
201
 
@@ -224,7 +204,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
224
204
  parent_link_map.append((graph_definition.link, parent_bundle_entries))
225
205
 
226
206
  # Process graph links in parallel
227
- link_processing_count = 0
228
207
  while len(parent_link_map):
229
208
  new_parent_link_map: list[tuple[list[GraphDefinitionLink], FhirBundleEntryList]] = []
230
209
 
@@ -251,38 +230,19 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
251
230
  add_cached_bundles_to_result=add_cached_bundles_to_result,
252
231
  ifModifiedSince=ifModifiedSince,
253
232
  ):
254
- # Track extend operation
255
- extend_start = time.perf_counter()
256
233
  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
262
234
 
263
235
  # Update parent link map for next iteration
264
236
  parent_link_map = new_parent_link_map
265
237
 
266
- profiling["steps"]["process_graph_links"] = time.perf_counter() - step_start
267
- profiling["steps"]["link_processing_iterations"] = link_processing_count
268
-
269
238
  # Combine and process responses
270
- step_start = time.perf_counter()
271
239
  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
- )
277
240
 
278
241
  # Optional resource sorting
279
242
  if sort_resources:
280
- step_start = time.perf_counter()
281
243
  parent_response = parent_response.sort_resources()
282
- profiling["steps"]["sort_resources"] = time.perf_counter() - step_start
283
244
 
284
245
  # Prepare final response based on bundling preferences
285
- step_start = time.perf_counter()
286
246
  full_response: FhirGetResponse
287
247
  if separate_bundle_resources:
288
248
  full_response = FhirGetListByResourceTypeResponse.from_response(other_response=parent_response)
@@ -290,38 +250,10 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
290
250
  full_response = FhirGetListResponse.from_response(other_response=parent_response)
291
251
  else:
292
252
  full_response = parent_response
293
- profiling["steps"]["prepare_final_response"] = time.perf_counter() - step_start
294
253
 
295
254
  # Set response URL
296
255
  full_response.url = url or parent_response.url
297
256
 
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
-
325
257
  # Log cache performance
326
258
  if logger:
327
259
  logger.info(
@@ -344,9 +276,26 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
344
276
  ) -> list[FhirGetResponse]:
345
277
  """
346
278
  Parallel processing function for graph definition links.
347
- """
348
- profiling_start = time.perf_counter()
349
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
+ """
350
299
  # Record the start time for performance tracking
351
300
  start_time: datetime = datetime.now()
352
301
 
@@ -377,8 +326,11 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
377
326
  logger=parameters.logger,
378
327
  cache=parameters.cache,
379
328
  scope_parser=parameters.scope_parser,
329
+ # Handle parent link map from additional parameters
380
330
  parent_link_map=(additional_parameters["parent_link_map"] if additional_parameters else []),
331
+ # Determine request size, default to 1 if not specified
381
332
  request_size=(additional_parameters["request_size"] if additional_parameters else 1),
333
+ # Track unsupported resources for ID-based search
382
334
  id_search_unsupported_resources=(
383
335
  additional_parameters["id_search_unsupported_resources"] if additional_parameters else []
384
336
  ),
@@ -394,8 +346,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
394
346
  # Record end time for performance tracking
395
347
  end_time: datetime = datetime.now()
396
348
 
397
- total_time = time.perf_counter() - profiling_start
398
-
399
349
  # Log detailed processing information
400
350
  if parameters.logger:
401
351
  parameters.logger.debug(
@@ -407,11 +357,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
407
357
  + f" | duration: {end_time - start_time}"
408
358
  + f" | resource_count: {len(result)}"
409
359
  )
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
- )
415
360
 
416
361
  # Return the list of retrieved responses
417
362
  return result
@@ -897,18 +842,9 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
897
842
  logger: Logger | None,
898
843
  compare_hash: bool = True,
899
844
  ) -> 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
-
907
845
  result: FhirGetResponse | None = None
908
846
  non_cached_id_list: list[str] = []
909
-
910
847
  # first check to see if we can find these in the cache
911
- cache_check_start = time.perf_counter()
912
848
  if ids:
913
849
  for resource_id in ids:
914
850
  cache_entry: RequestCacheEntry | None = await cache.get_async(
@@ -921,12 +857,9 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
921
857
  if logger:
922
858
  logger.info(f"Cache entry not found for {resource_type}/{resource_id} (1by1)")
923
859
  non_cached_id_list.append(resource_id)
924
- cache_check_time = time.perf_counter() - cache_check_start
925
860
 
926
- cache_update_start = time.perf_counter()
927
861
  for single_id in non_cached_id_list:
928
862
  result2: FhirGetResponse
929
- http_start = time.perf_counter()
930
863
  async for result2 in self._get_with_session_async(
931
864
  page_number=None,
932
865
  ids=[single_id],
@@ -935,15 +868,10 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
935
868
  fn_handle_streaming_chunk=None,
936
869
  resource_type=resource_type,
937
870
  ):
938
- http_request_time += time.perf_counter() - http_start
939
- http_request_count += 1
940
-
941
871
  if result2.resource_type == "OperationOutcome":
942
872
  result2 = FhirGetErrorResponse.from_response(other_response=result2)
943
873
  if result:
944
- append_start = time.perf_counter()
945
874
  result = result.append(result2)
946
- append_time += time.perf_counter() - append_start
947
875
  else:
948
876
  result = result2
949
877
  if result2.successful:
@@ -977,21 +905,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
977
905
  )
978
906
  if cache_updated and logger:
979
907
  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
- )
995
908
 
996
909
  return result
997
910
 
@@ -1008,13 +921,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1008
921
  add_cached_bundles_to_result: bool = True,
1009
922
  compare_hash: bool = True,
1010
923
  ) -> 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
-
1018
924
  assert resource_type
1019
925
  if not scope_parser.scope_allows(resource_type=resource_type):
1020
926
  if logger:
@@ -1048,13 +954,14 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1048
954
 
1049
955
  non_cached_id_list: list[str] = []
1050
956
  # get any cached resources
1051
- cache_check_start = time.perf_counter()
1052
957
  if id_list:
1053
958
  for resource_id in id_list:
1054
959
  cache_entry: RequestCacheEntry | None = await cache.get_async(
1055
960
  resource_type=resource_type, resource_id=resource_id
1056
961
  )
1057
962
  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
1058
965
  if logger:
1059
966
  logger.info(
1060
967
  f"{cache_entry.status} Returning {resource_type}/{resource_id} from cache (ByParam)"
@@ -1063,7 +970,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1063
970
  if logger:
1064
971
  logger.info(f"Cache entry not found for {resource_type}/{resource_id} (ByParam)")
1065
972
  non_cached_id_list.append(resource_id)
1066
- cache_check_time = time.perf_counter() - cache_check_start
1067
973
 
1068
974
  all_result: FhirGetResponse | None = None
1069
975
  # either we have non-cached ids or this is a query without id but has other parameters
@@ -1075,7 +981,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1075
981
  # call the server to get the resources
1076
982
  result1: FhirGetResponse
1077
983
  result: FhirGetResponse | None
1078
- http_start = time.perf_counter()
1079
984
  async for result1 in self._get_with_session_async(
1080
985
  page_number=None,
1081
986
  ids=non_cached_id_list,
@@ -1084,8 +989,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1084
989
  fn_handle_streaming_chunk=None,
1085
990
  resource_type=resource_type,
1086
991
  ):
1087
- http_request_time += time.perf_counter() - http_start
1088
- http_request_count += 1
1089
992
  result = result1
1090
993
  # if we got a failure then check if we can get it one by one
1091
994
  if (not result or result.status != 200) and len(non_cached_id_list) > 1:
@@ -1098,7 +1001,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1098
1001
  f" Fetching one by one ids: {non_cached_id_list}"
1099
1002
  )
1100
1003
  # For some resources if search by _id doesn't work then fetch one by one.
1101
- one_by_one_start = time.perf_counter()
1102
1004
  result = await self._get_resources_by_id_one_by_one_async(
1103
1005
  resource_type=resource_type,
1104
1006
  ids=non_cached_id_list,
@@ -1107,9 +1009,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1107
1009
  logger=logger,
1108
1010
  compare_hash=compare_hash,
1109
1011
  )
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)
1113
1012
  else:
1114
1013
  if logger:
1115
1014
  logger.info(f"Fetched {resource_type} resources using _id for url {self._url}")
@@ -1125,14 +1024,11 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1125
1024
 
1126
1025
  # append to the response
1127
1026
  if all_result:
1128
- append_start = time.perf_counter()
1129
1027
  all_result = all_result.append(result)
1130
- append_time += time.perf_counter() - append_start
1131
1028
  else:
1132
1029
  all_result = result
1133
1030
  # If non_cached_id_list is not empty and resource_type does not support ?_id search then fetch it one by one
1134
1031
  elif len(non_cached_id_list):
1135
- one_by_one_start = time.perf_counter()
1136
1032
  all_result = await self._get_resources_by_id_one_by_one_async(
1137
1033
  resource_type=resource_type,
1138
1034
  ids=non_cached_id_list,
@@ -1141,13 +1037,10 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1141
1037
  logger=logger,
1142
1038
  compare_hash=compare_hash,
1143
1039
  )
1144
- http_request_time += time.perf_counter() - one_by_one_start
1145
- http_request_count += len(non_cached_id_list)
1146
1040
 
1147
1041
  # This list tracks the non-cached ids that were found
1148
1042
  found_non_cached_id_list: list[str] = []
1149
1043
  # Cache the fetched entries
1150
- cache_update_start = time.perf_counter()
1151
1044
  if all_result:
1152
1045
  non_cached_bundle_entry: FhirBundleEntry
1153
1046
  for non_cached_bundle_entry in all_result.get_bundle_entries():
@@ -1181,6 +1074,7 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1181
1074
  logger.debug(f"Inserted {resource_type}/{non_cached_resource_id} into cache (ByParam)")
1182
1075
  found_non_cached_id_list.append(non_cached_resource_id)
1183
1076
 
1077
+ # now add all the non-cached ids that were NOT found to the cache too so we don't look for them again
1184
1078
  for non_cached_id in non_cached_id_list:
1185
1079
  if non_cached_id not in found_non_cached_id_list:
1186
1080
  cache_updated = await cache.add_async(
@@ -1195,7 +1089,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1195
1089
  )
1196
1090
  if cache_updated and logger:
1197
1091
  logger.info(f"Inserted 404 for {resource_type}/{non_cached_id} into cache (ByParam)")
1198
- cache_update_time = time.perf_counter() - cache_update_start
1199
1092
 
1200
1093
  bundle_response: FhirGetBundleResponse = (
1201
1094
  FhirGetBundleResponse.from_response(other_response=all_result)
@@ -1237,21 +1130,6 @@ class SimulatedGraphProcessorMixin(ABC, FhirClientProtocol):
1237
1130
  storage_mode=self._storage_mode,
1238
1131
  )
1239
1132
  )
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
-
1255
1133
  return bundle_response, cache.cache_hits
1256
1134
 
1257
1135
  # noinspection PyPep8Naming
@@ -170,8 +170,9 @@ class FhirGetResponse:
170
170
  """
171
171
  result: FhirGetResponse = self._extend(others=others)
172
172
 
173
- if others and others[-1].chunk_number:
174
- result.chunk_number = others[-1].chunk_number
173
+ latest_chunk_number: list[int] = sorted([o.chunk_number for o in others if o.chunk_number], reverse=True)
174
+ if len(latest_chunk_number) > 0:
175
+ result.chunk_number = latest_chunk_number[0]
175
176
  return result
176
177
 
177
178
  @abstractmethod
@@ -1,5 +1,4 @@
1
1
  import json
2
- import logging
3
2
  from collections.abc import AsyncGenerator, Generator
4
3
  from datetime import datetime
5
4
  from logging import Logger
@@ -133,21 +132,8 @@ class FhirGetBundleResponse(FhirGetResponse):
133
132
  :param others: list of FhirGetResponse objects
134
133
  :return: self
135
134
  """
136
- if not others:
137
- return self
138
-
139
- all_entries: list[FhirBundleEntry] = []
140
135
  for other_response in others:
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)
136
+ self.append(other_response=other_response)
151
137
 
152
138
  return self
153
139
 
@@ -325,56 +311,36 @@ class FhirGetBundleResponse(FhirGetResponse):
325
311
  ) -> "FhirGetBundleResponse":
326
312
  """
327
313
  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
328
318
  """
329
- # Build a lookup of cache entries by (resource_type, id)
330
- cache_map: dict[tuple[str, str], str | None] = {}
319
+ # remove all entries in the cache from the bundle
331
320
  async for cached_entry in request_cache.get_entries_async():
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
- )
321
+ if cached_entry.from_input_cache:
322
+ for entry in self._bundle_entries:
323
+ if (
324
+ entry.resource
325
+ and entry.resource.id is not None # only remove if resource has an id
326
+ and entry.resource.id == cached_entry.id_
327
+ and entry.resource.resource_type == cached_entry.resource_type
328
+ and (
329
+ not compare_hash
330
+ or (
331
+ ResourceHash().hash_value(json.dumps(json.loads(entry.resource.json()), sort_keys=True))
332
+ == cached_entry.raw_hash
333
+ )
334
+ )
335
+ ):
336
+ if logger:
337
+ logger.debug(
338
+ f"Removing entry from bundle with id {entry.resource.id} and resource "
339
+ f"type {entry.resource.resource_type}"
340
+ )
341
+ self._bundle_entries.remove(entry)
342
+ break
376
343
 
377
- self._bundle_entries = FhirBundleEntryList(kept)
378
344
  return self
379
345
 
380
346
  @classmethod
@@ -1,4 +1,5 @@
1
1
  import dataclasses
2
+ from copy import deepcopy
2
3
  from typing import Any, cast
3
4
 
4
5
 
@@ -26,42 +27,46 @@ class ResourceSeparator:
26
27
  extra_context_to_return: dict[str, Any] | None,
27
28
  ) -> ResourceSeparatorResult:
28
29
  """
29
- Separate contained resources without copying or mutating input resources.
30
- """
31
- resources_dicts: list[dict[str, str | None | list[dict[str, Any]]]] = []
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]]] = {}
30
+ Given a list of resources, return a list of resources with the contained resources separated out.
41
31
 
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
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.
46
36
 
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
37
+ :return: None
38
+ """
39
+ resources_dicts: list[dict[str, str | None | list[dict[str, Any]]]] = []
40
+ resource_count: int = 0
41
+ resource: dict[str, Any]
42
+ for resource in resources:
43
+ # make a copy so we are not changing the original resource
44
+ cloned_resource: dict[str, Any] = deepcopy(resource)
45
+ # This dict will hold the separated resources where the key is resourceType
46
+ # have to split these here otherwise when Spark loads them
47
+ # it can't handle that items in the entry array can have different schemas
48
+ resources_dict: dict[str, str | None | list[dict[str, Any]]] = {}
49
+ # add the parent resource to the resources_dict
50
+ resource_type = str(cloned_resource["resourceType"]).lower()
51
+ if resource_type not in resources_dict:
52
+ resources_dict[resource_type] = []
53
+ if isinstance(resources_dict[resource_type], list):
54
+ cast(list[dict[str, Any]], resources_dict[resource_type]).append(cloned_resource)
55
+ resource_count += 1
56
+ # now see if this resource has a contained array and if so, add those to the resources_dict
57
+ if "contained" in cloned_resource:
58
+ contained_resources = cloned_resource.pop("contained")
59
+ for contained_resource in contained_resources:
60
+ resource_type = str(contained_resource["resourceType"]).lower()
61
+ if resource_type not in resources_dict:
62
+ resources_dict[resource_type] = []
63
+ if isinstance(resources_dict[resource_type], list):
64
+ cast(list[dict[str, Any]], resources_dict[resource_type]).append(contained_resource)
65
+ resource_count += 1
66
+ resources_dict["token"] = access_token
67
+ resources_dict["url"] = url
62
68
  if extra_context_to_return:
63
- resource_map.update(extra_context_to_return)
64
-
65
- resources_dicts.append(resource_map)
69
+ resources_dict.update(extra_context_to_return)
70
+ resources_dicts.append(resources_dict)
66
71
 
67
- return ResourceSeparatorResult(resources_dicts=resources_dicts, total_count=total_resource_count)
72
+ return ResourceSeparatorResult(resources_dicts=resources_dicts, total_count=resource_count)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: helix.fhir.client.sdk
3
- Version: 4.2.20
3
+ Version: 4.2.22
4
4
  Summary: helix.fhir.client.sdk
5
5
  Home-page: https://github.com/icanbwell/helix.fhir.client.sdk
6
6
  Author: Imran Qureshi
@@ -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=CNXlqjkdrebypCY4JHmz1XbGT3kpcSpich9ourE76H0,66430
35
+ helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py,sha256=h8K4peHcoY_4_Ln4Vz54Ls87W7bXegm4SZV2s2L4cCM,60302
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=v0ndKFb6o8PNqYYxGlLETzQnxGmwK3YympH4gotXpY4,17550
49
+ helix_fhir_client_sdk/responses/fhir_get_response.py,sha256=3PXvFoMZ7ix2ZzucIjY-49RL3foLR07dD57BdulyMGI,17657
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=7Ic0_SPNKCBAk6l07Ke2-AoObkBMnKdmtbbtBBCtVjE,2606
55
+ helix_fhir_client_sdk/responses/resource_separator.py,sha256=jugaEkJYunx8VGVFCLwWNSjrBlI8DDm61LzSx9oR8iE,3230
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=PA1bzR4DkF91uTBM5tZCZ_AB0ovE49EJjq4O-1-Ya90,19357
57
+ helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py,sha256=H-e-Rp_cwU508uJqQfDDCoJR5vN9-5DYsaW7K0yTsxM,18183
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.20.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
133
+ helix_fhir_client_sdk-4.2.22.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.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,,
216
+ helix_fhir_client_sdk-4.2.22.dist-info/METADATA,sha256=rixhdJJj1F1Cmo582ikppItEzirbEiOnYjYsuHW79F4,7210
217
+ helix_fhir_client_sdk-4.2.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
218
+ helix_fhir_client_sdk-4.2.22.dist-info/top_level.txt,sha256=BRnDS6ceQxs-4u2jXznATObgP8G2cGAerlH0ZS4sJ6M,46
219
+ helix_fhir_client_sdk-4.2.22.dist-info/RECORD,,