helix.fhir.client.sdk 4.2.3__py3-none-any.whl → 4.2.19__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.
Files changed (37) hide show
  1. helix_fhir_client_sdk/fhir_auth_mixin.py +17 -10
  2. helix_fhir_client_sdk/fhir_client.py +152 -79
  3. helix_fhir_client_sdk/fhir_delete_mixin.py +62 -48
  4. helix_fhir_client_sdk/fhir_merge_mixin.py +188 -166
  5. helix_fhir_client_sdk/fhir_merge_resources_mixin.py +200 -15
  6. helix_fhir_client_sdk/fhir_patch_mixin.py +97 -84
  7. helix_fhir_client_sdk/fhir_update_mixin.py +71 -57
  8. helix_fhir_client_sdk/graph/simulated_graph_processor_mixin.py +147 -49
  9. helix_fhir_client_sdk/open_telemetry/__init__.py +0 -0
  10. helix_fhir_client_sdk/open_telemetry/attribute_names.py +7 -0
  11. helix_fhir_client_sdk/open_telemetry/span_names.py +12 -0
  12. helix_fhir_client_sdk/queue/request_queue_mixin.py +17 -12
  13. helix_fhir_client_sdk/responses/fhir_client_protocol.py +10 -6
  14. helix_fhir_client_sdk/responses/fhir_get_response.py +3 -4
  15. helix_fhir_client_sdk/responses/fhir_response_processor.py +73 -54
  16. helix_fhir_client_sdk/responses/get/fhir_get_bundle_response.py +49 -28
  17. helix_fhir_client_sdk/responses/get/fhir_get_error_response.py +0 -1
  18. helix_fhir_client_sdk/responses/get/fhir_get_list_by_resource_type_response.py +1 -1
  19. helix_fhir_client_sdk/responses/get/fhir_get_list_response.py +1 -1
  20. helix_fhir_client_sdk/responses/get/fhir_get_response_factory.py +0 -1
  21. helix_fhir_client_sdk/responses/get/fhir_get_single_response.py +1 -1
  22. helix_fhir_client_sdk/responses/merge/fhir_merge_resource_response_entry.py +30 -0
  23. helix_fhir_client_sdk/responses/resource_separator.py +35 -40
  24. helix_fhir_client_sdk/utilities/cache/request_cache.py +32 -43
  25. helix_fhir_client_sdk/utilities/retryable_aiohttp_client.py +185 -154
  26. helix_fhir_client_sdk/utilities/retryable_aiohttp_response.py +2 -1
  27. helix_fhir_client_sdk/validators/async_fhir_validator.py +3 -0
  28. helix_fhir_client_sdk-4.2.19.dist-info/METADATA +200 -0
  29. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/RECORD +36 -29
  30. tests/async/test_benchmark_compress.py +448 -0
  31. tests/async/test_benchmark_merge.py +506 -0
  32. tests/async/test_retryable_client_session_management.py +159 -0
  33. tests/test_fhir_client_clone.py +155 -0
  34. helix_fhir_client_sdk-4.2.3.dist-info/METADATA +0 -115
  35. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/WHEEL +0 -0
  36. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/licenses/LICENSE +0 -0
  37. {helix_fhir_client_sdk-4.2.3.dist-info → helix_fhir_client_sdk-4.2.19.dist-info}/top_level.txt +0 -0
@@ -11,10 +11,13 @@ from aiohttp.streams import AsyncStreamIterator
11
11
  from compressedfhir.utilities.compressed_dict.v1.compressed_dict_storage_mode import (
12
12
  CompressedDictStorageMode,
13
13
  )
14
+ from opentelemetry import trace
15
+ from opentelemetry.trace import Status, StatusCode
14
16
 
15
17
  from helix_fhir_client_sdk.function_types import (
16
18
  HandleStreamingChunkFunction,
17
19
  )
20
+ from helix_fhir_client_sdk.open_telemetry.span_names import FhirClientSdkOpenTelemetrySpanNames
18
21
  from helix_fhir_client_sdk.responses.bundle_expander import (
19
22
  BundleExpander,
20
23
  BundleExpanderResult,
@@ -37,6 +40,8 @@ from helix_fhir_client_sdk.utilities.retryable_aiohttp_response import (
37
40
  RetryableAioHttpResponse,
38
41
  )
39
42
 
43
+ TRACER = trace.get_tracer(__name__)
44
+
40
45
 
41
46
  class FhirResponseProcessor:
42
47
  """
@@ -93,60 +98,74 @@ class FhirResponseProcessor:
93
98
 
94
99
  :return: An async generator of FhirGetResponse objects.
95
100
  """
96
- # if request is ok (200) then return the data
97
- if response.ok:
98
- async for r in FhirResponseProcessor._handle_response_200(
99
- full_url=full_url,
100
- request_id=request_id,
101
- response=response,
102
- response_headers=response_headers,
103
- fn_handle_streaming_chunk=fn_handle_streaming_chunk,
104
- access_token=access_token,
105
- resources_json=resources_json,
106
- resource=resource,
107
- id_=id_,
108
- logger=logger,
109
- use_data_streaming=use_data_streaming,
110
- chunk_size=chunk_size,
111
- extra_context_to_return=extra_context_to_return,
112
- expand_fhir_bundle=expand_fhir_bundle,
113
- url=url,
114
- separate_bundle_resources=separate_bundle_resources,
115
- storage_mode=storage_mode,
116
- create_operation_outcome_for_error=create_operation_outcome_for_error,
117
- ):
118
- yield r
119
- elif response.status == 404: # not found
120
- async for r in FhirResponseProcessor._handle_response_404(
121
- full_url=full_url,
122
- request_id=request_id,
123
- response=response,
124
- response_headers=response_headers,
125
- extra_context_to_return=extra_context_to_return,
126
- resource=resource,
127
- logger=logger,
128
- id_=id_,
129
- access_token=access_token,
130
- storage_mode=storage_mode,
131
- create_operation_outcome_for_error=create_operation_outcome_for_error,
132
- ):
133
- yield r
134
- else: # unknown response
135
- async for r in FhirResponseProcessor._handle_response_unknown(
136
- full_url=full_url,
137
- request_id=request_id,
138
- response=response,
139
- response_headers=response_headers,
140
- resource=resource,
141
- logger=logger,
142
- access_token=access_token,
143
- extra_context_to_return=extra_context_to_return,
144
- id_=id_,
145
- internal_logger=internal_logger,
146
- storage_mode=storage_mode,
147
- create_operation_outcome_for_error=create_operation_outcome_for_error,
148
- ):
149
- yield r
101
+ span = TRACER.start_span(FhirClientSdkOpenTelemetrySpanNames.HANDLE_RESPONSE)
102
+ try:
103
+ # if request is ok (200) then return the data
104
+ if response.ok:
105
+ async for r in FhirResponseProcessor._handle_response_200(
106
+ full_url=full_url,
107
+ request_id=request_id,
108
+ response=response,
109
+ response_headers=response_headers,
110
+ fn_handle_streaming_chunk=fn_handle_streaming_chunk,
111
+ access_token=access_token,
112
+ resources_json=resources_json,
113
+ resource=resource,
114
+ id_=id_,
115
+ logger=logger,
116
+ use_data_streaming=use_data_streaming,
117
+ chunk_size=chunk_size,
118
+ extra_context_to_return=extra_context_to_return,
119
+ expand_fhir_bundle=expand_fhir_bundle,
120
+ url=url,
121
+ separate_bundle_resources=separate_bundle_resources,
122
+ storage_mode=storage_mode,
123
+ create_operation_outcome_for_error=create_operation_outcome_for_error,
124
+ ):
125
+ yield r
126
+ elif response.status == 404: # not found
127
+ async for r in FhirResponseProcessor._handle_response_404(
128
+ full_url=full_url,
129
+ request_id=request_id,
130
+ response=response,
131
+ response_headers=response_headers,
132
+ extra_context_to_return=extra_context_to_return,
133
+ resource=resource,
134
+ logger=logger,
135
+ id_=id_,
136
+ access_token=access_token,
137
+ storage_mode=storage_mode,
138
+ create_operation_outcome_for_error=create_operation_outcome_for_error,
139
+ ):
140
+ yield r
141
+ else: # unknown response
142
+ async for r in FhirResponseProcessor._handle_response_unknown(
143
+ full_url=full_url,
144
+ request_id=request_id,
145
+ response=response,
146
+ response_headers=response_headers,
147
+ resource=resource,
148
+ logger=logger,
149
+ access_token=access_token,
150
+ extra_context_to_return=extra_context_to_return,
151
+ id_=id_,
152
+ internal_logger=internal_logger,
153
+ storage_mode=storage_mode,
154
+ create_operation_outcome_for_error=create_operation_outcome_for_error,
155
+ ):
156
+ yield r
157
+ # Mark span as successful
158
+ span.set_status(Status(StatusCode.OK))
159
+
160
+ except Exception as e:
161
+ # Record exception in span
162
+ span.record_exception(e)
163
+ span.set_status(Status(StatusCode.ERROR, str(e)))
164
+ raise
165
+
166
+ finally:
167
+ # Ensure span is ended after generator is exhausted or error occurs
168
+ span.end()
150
169
 
151
170
  @staticmethod
152
171
  async def _handle_response_unknown(
@@ -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
@@ -59,7 +60,7 @@ class FhirGetBundleResponse(FhirGetResponse):
59
60
  chunk_number: int | None = None,
60
61
  cache_hits: int | None = None,
61
62
  results_by_url: list[RetryableAioHttpUrlResult],
62
- storage_mode: CompressedDictStorageMode = CompressedDictStorageMode.raw(),
63
+ storage_mode: CompressedDictStorageMode,
63
64
  ) -> None:
64
65
  super().__init__(
65
66
  request_id=request_id,
@@ -311,36 +312,56 @@ class FhirGetBundleResponse(FhirGetResponse):
311
312
  ) -> "FhirGetBundleResponse":
312
313
  """
313
314
  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
315
  """
319
- # remove all entries in the cache from the bundle
316
+ # Build a lookup of cache entries by (resource_type, id)
317
+ cache_map: dict[tuple[str, str], str | None] = {}
320
318
  async for cached_entry in request_cache.get_entries_async():
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
319
+ if cached_entry.from_input_cache and cached_entry.resource_type and cached_entry.id_:
320
+ cache_map[(cached_entry.resource_type, cached_entry.id_)] = (
321
+ cached_entry.raw_hash if compare_hash else None
322
+ )
323
+
324
+ if not cache_map:
325
+ return self
326
+
327
+ resource_hash = ResourceHash() if compare_hash else None
328
+ removed_entries: list[FhirBundleEntry] = []
329
+
330
+ def should_remove(entry: FhirBundleEntry) -> bool:
331
+ resource = entry.resource
332
+ if not resource or resource.id is None or resource.resource_type is None:
333
+ return False
334
+ key = (resource.resource_type, resource.id)
335
+ if key not in cache_map:
336
+ return False
337
+ if not compare_hash:
338
+ return True
339
+ # Compare normalized JSON hash
340
+ if resource_hash is None:
341
+ return False
342
+ try:
343
+ entry_hash = resource_hash.hash_value(json.dumps(json.loads(resource.json()), sort_keys=True))
344
+ return entry_hash == cache_map[key]
345
+ except Exception:
346
+ return False
347
+
348
+ # One pass filter; rebuild list to avoid many deque.remove calls
349
+ kept: list[FhirBundleEntry] = []
350
+ for entry in self._bundle_entries:
351
+ if should_remove(entry):
352
+ removed_entries.append(entry)
353
+ else:
354
+ kept.append(entry)
355
+
356
+ if logger and removed_entries and logger.isEnabledFor(logging.DEBUG):
357
+ for entry in removed_entries:
358
+ if entry.resource:
359
+ logger.debug(
360
+ f"Removing entry from bundle with id {entry.resource.id} and resource "
361
+ f"type {entry.resource.resource_type}"
362
+ )
343
363
 
364
+ self._bundle_entries = FhirBundleEntryList(kept)
344
365
  return self
345
366
 
346
367
  @classmethod
@@ -67,7 +67,6 @@ class FhirGetErrorResponse(FhirGetResponse):
67
67
  storage_mode: CompressedDictStorageMode,
68
68
  create_operation_outcome_for_error: bool | None,
69
69
  ) -> None:
70
- storage_mode or CompressedDictStorageMode.raw()
71
70
  super().__init__(
72
71
  request_id=request_id,
73
72
  url=url,
@@ -57,7 +57,7 @@ class FhirGetListByResourceTypeResponse(FhirGetResponse):
57
57
  chunk_number: int | None = None,
58
58
  cache_hits: int | None = None,
59
59
  results_by_url: list[RetryableAioHttpUrlResult],
60
- storage_mode: CompressedDictStorageMode = CompressedDictStorageMode.raw(),
60
+ storage_mode: CompressedDictStorageMode,
61
61
  ) -> None:
62
62
  super().__init__(
63
63
  request_id=request_id,
@@ -59,7 +59,7 @@ class FhirGetListResponse(FhirGetResponse):
59
59
  chunk_number: int | None = None,
60
60
  cache_hits: int | None = None,
61
61
  results_by_url: list[RetryableAioHttpUrlResult],
62
- storage_mode: CompressedDictStorageMode = CompressedDictStorageMode.raw(),
62
+ storage_mode: CompressedDictStorageMode,
63
63
  ) -> None:
64
64
  super().__init__(
65
65
  request_id=request_id,
@@ -50,7 +50,6 @@ class FhirGetResponseFactory:
50
50
  storage_mode: CompressedDictStorageMode,
51
51
  create_operation_outcome_for_error: bool | None,
52
52
  ) -> FhirGetResponse:
53
- storage_mode = storage_mode or CompressedDictStorageMode.raw()
54
53
  try:
55
54
  if not error and response_text:
56
55
  # test if responses is valid json
@@ -59,7 +59,7 @@ class FhirGetSingleResponse(FhirGetResponse):
59
59
  chunk_number: int | None = None,
60
60
  cache_hits: int | None = None,
61
61
  results_by_url: list[RetryableAioHttpUrlResult],
62
- storage_mode: CompressedDictStorageMode = CompressedDictStorageMode.raw(),
62
+ storage_mode: CompressedDictStorageMode,
63
63
  ) -> None:
64
64
  super().__init__(
65
65
  request_id=request_id,
@@ -75,6 +75,36 @@ class FhirMergeResourceResponseEntry(BaseFhirMergeResourceResponseEntry):
75
75
  status=data.get("status"),
76
76
  )
77
77
 
78
+ @classmethod
79
+ def from_dict_uncompressed(cls, data: dict[str, Any]) -> "FhirMergeResourceResponseEntry":
80
+ """
81
+ Creates a FhirMergeResourceResponseEntry from a dictionary without storage_mode overhead.
82
+ Uses FhirResource.construct for faster object creation.
83
+
84
+ :param data: Dictionary containing the response entry data
85
+ :return: FhirMergeResourceResponseEntry instance
86
+ """
87
+ resource_payload = data.get("resource")
88
+ resource_obj: FhirResource | None = (
89
+ FhirResource.construct(**resource_payload) if isinstance(resource_payload, dict) else None
90
+ )
91
+ return FhirMergeResourceResponseEntry(
92
+ created=data.get("created"),
93
+ updated=data.get("updated"),
94
+ deleted=data.get("deleted"),
95
+ id_=data.get("id"),
96
+ uuid=data.get("uuid"),
97
+ resource_type=data.get("resourceType"),
98
+ source_assigning_authority=data.get("source_assigning_authority"),
99
+ resource_version=data.get("resource_version"),
100
+ message=data.get("message"),
101
+ issue=data.get("issue"),
102
+ error=data.get("error"),
103
+ token=data.get("token"),
104
+ resource=resource_obj,
105
+ status=data.get("status"),
106
+ )
107
+
78
108
  @classmethod
79
109
  @override
80
110
  def from_json(
@@ -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
- Given a list of resources, return a list of resources with the contained resources separated out.
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
- 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
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
- resources_dict.update(extra_context_to_return)
70
- resources_dicts.append(resources_dict)
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=resource_count)
67
+ return ResourceSeparatorResult(resources_dicts=resources_dicts, total_count=total_resource_count)
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  from collections.abc import AsyncGenerator
3
2
  from datetime import datetime
4
3
  from types import TracebackType
@@ -11,7 +10,7 @@ from helix_fhir_client_sdk.utilities.cache.request_cache_entry import RequestCac
11
10
 
12
11
  class RequestCache:
13
12
  """
14
- This is a class that caches requests to the FHIR server using a weak value dictionary.
13
+ This is a class that caches requests to the FHIR server.
15
14
  It is used to avoid multiple requests to the FHIR server when doing a large number
16
15
  of requests for the same resource.
17
16
  """
@@ -20,7 +19,6 @@ class RequestCache:
20
19
  "cache_hits",
21
20
  "cache_misses",
22
21
  "_cache",
23
- "_lock",
24
22
  "_clear_cache_at_the_end",
25
23
  ]
26
24
 
@@ -33,7 +31,6 @@ class RequestCache:
33
31
  self.cache_hits: int = 0
34
32
  self.cache_misses: int = 0
35
33
  self._cache: dict[str, RequestCacheEntry] = initial_dict or {}
36
- self._lock: asyncio.Lock = asyncio.Lock()
37
34
  self._clear_cache_at_the_end: bool | None = clear_cache_at_the_end
38
35
 
39
36
  async def __aenter__(self) -> "RequestCache":
@@ -42,8 +39,7 @@ class RequestCache:
42
39
  It returns the RequestCache instance.
43
40
  """
44
41
  if self._clear_cache_at_the_end:
45
- async with self._lock:
46
- self._cache.clear()
42
+ self._cache.clear()
47
43
  return self
48
44
 
49
45
  async def __aexit__(
@@ -57,8 +53,7 @@ class RequestCache:
57
53
  It clears the cache.
58
54
  """
59
55
  if self._clear_cache_at_the_end:
60
- async with self._lock:
61
- self._cache.clear()
56
+ self._cache.clear()
62
57
 
63
58
  if exc_value is not None:
64
59
  raise exc_value.with_traceback(traceback)
@@ -75,15 +70,14 @@ class RequestCache:
75
70
  """
76
71
  key: str = f"{resource_type}/{resource_id}"
77
72
 
78
- async with self._lock:
79
- cached_entry = self._cache.get(key)
73
+ cached_entry = self._cache.get(key)
80
74
 
81
- if cached_entry is not None:
82
- self.cache_hits += 1
83
- return cached_entry
75
+ if cached_entry is not None:
76
+ self.cache_hits += 1
77
+ return cached_entry
84
78
 
85
- self.cache_misses += 1
86
- return None
79
+ self.cache_misses += 1
80
+ return None
87
81
 
88
82
  async def add_async(
89
83
  self,
@@ -111,43 +105,40 @@ class RequestCache:
111
105
  """
112
106
  key: str = f"{resource_type}/{resource_id}"
113
107
 
114
- async with self._lock:
115
- # Create the cache entry
116
- cache_entry = RequestCacheEntry(
117
- id_=resource_id,
118
- resource_type=resource_type,
119
- status=status,
120
- bundle_entry=bundle_entry,
121
- last_modified=last_modified,
122
- etag=etag,
123
- from_input_cache=from_input_cache,
124
- raw_hash=raw_hash,
125
- )
126
-
127
- # Add to the weak value dictionary
128
- self._cache[key] = cache_entry
108
+ # Create the cache entry
109
+ cache_entry = RequestCacheEntry(
110
+ id_=resource_id,
111
+ resource_type=resource_type,
112
+ status=status,
113
+ bundle_entry=bundle_entry,
114
+ last_modified=last_modified,
115
+ etag=etag,
116
+ from_input_cache=from_input_cache,
117
+ raw_hash=raw_hash,
118
+ )
119
+
120
+ # Add to the dictionary
121
+ self._cache[key] = cache_entry
129
122
 
130
- return True
123
+ return True
131
124
 
132
125
  async def remove_async(self, *, resource_key: str) -> bool:
133
126
  """
134
127
  This method remove the given data from the cache.
135
128
  :param resource_key: resource key contains both resourceType and resourceId. Eg: Patient/123
136
129
  """
137
- async with self._lock:
138
- if resource_key not in self._cache:
139
- return False
130
+ if resource_key not in self._cache:
131
+ return False
140
132
 
141
- del self._cache[resource_key]
133
+ del self._cache[resource_key]
142
134
 
143
- return True
135
+ return True
144
136
 
145
137
  async def clear_async(self) -> None:
146
138
  """
147
139
  This method clears the cache.
148
140
  """
149
- async with self._lock:
150
- self._cache.clear()
141
+ self._cache.clear()
151
142
 
152
143
  async def get_entries_async(self) -> AsyncGenerator[RequestCacheEntry, None]:
153
144
  """
@@ -155,9 +146,8 @@ class RequestCache:
155
146
 
156
147
  :return: The keys in the cache.
157
148
  """
158
- async with self._lock:
159
- for entry in self._cache.values():
160
- yield entry
149
+ for entry in self._cache.values():
150
+ yield entry
161
151
 
162
152
  async def get_keys_async(self) -> list[str]:
163
153
  """
@@ -165,8 +155,7 @@ class RequestCache:
165
155
 
166
156
  :return: The entries in the cache.
167
157
  """
168
- async with self._lock:
169
- return list(self._cache.keys())
158
+ return list(self._cache.keys())
170
159
 
171
160
  def __len__(self) -> int:
172
161
  """