Skip to content

Commit bc28752

Browse files
committed
fix: avoid iterating over a None entry result for an empty search
Also, avoid a deprecation warning for perform_iter() Fixes #185
1 parent a655c40 commit bc28752

File tree

4 files changed

+58
-16
lines changed

4 files changed

+58
-16
lines changed

fhirclient/_utils.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import urllib
22
from typing import Optional
33

4-
import requests
5-
6-
from typing import TYPE_CHECKING, Iterable
4+
from typing import TYPE_CHECKING, Iterator
75

86
if TYPE_CHECKING:
97
from fhirclient.server import FHIRServer
@@ -92,7 +90,7 @@ def _execute_pagination_request(sanitized_url: str, server: 'FHIRServer') -> 'Bu
9290
return Bundle.read_from(sanitized_url, server)
9391

9492

95-
def iter_pages(first_bundle: 'Bundle', server: 'FHIRServer') -> Iterable['Bundle']:
93+
def iter_pages(first_bundle: 'Bundle', server: 'FHIRServer') -> Iterator['Bundle']:
9694
"""
9795
Iterator that yields each page of results as a FHIR Bundle.
9896

fhirclient/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from .server import FHIRServer, FHIRUnauthorizedException, FHIRNotFoundException
33

4-
__version__ = '4.3.0'
4+
__version__ = '4.3.1'
55
__author__ = 'SMART Platforms Team'
66
__license__ = 'APACHE2'
77
__copyright__ = "Copyright 2017 Boston Children's Hospital"

fhirclient/models/fhirsearch.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
try:
1616
from urllib import quote_plus
17-
except Exception as e:
17+
except ImportError:
1818
from urllib.parse import quote_plus
1919

2020
if TYPE_CHECKING:
@@ -117,7 +117,19 @@ def include(self, reference_field, reference_model=None, reverse=False):
117117

118118
self.includes.append((reference_model, reference_field, reverse))
119119
return self
120-
120+
121+
def _read_bundle(self, server) -> 'Bundle':
122+
""" Construct the search URL and execute it against the given server.
123+
124+
:param server: The server against which to perform the search
125+
:returns: A Bundle resource
126+
"""
127+
if server is None:
128+
raise Exception("Need a server to perform search")
129+
130+
from .bundle import Bundle
131+
return Bundle.read_from(self.construct(), server)
132+
121133
def perform(self, server) -> 'Bundle':
122134
""" Construct the search URL and execute it against the given server.
123135
@@ -131,11 +143,7 @@ def perform(self, server) -> 'Bundle':
131143
DeprecationWarning,
132144
)
133145

134-
if server is None:
135-
raise Exception("Need a server to perform search")
136-
137-
from .bundle import Bundle
138-
return Bundle.read_from(self.construct(), server)
146+
return self._read_bundle(server)
139147

140148
# Use forward references to avoid circular imports
141149
def perform_iter(self, server) -> Iterator['Bundle']:
@@ -145,7 +153,7 @@ def perform_iter(self, server) -> Iterator['Bundle']:
145153
:param server: The server against which to perform the search
146154
:returns: An iterator of Bundle instances
147155
"""
148-
return iter_pages(self.perform(server), server)
156+
return iter_pages(self._read_bundle(server), server)
149157

150158
def perform_resources(self, server) -> 'list[Resource]':
151159
""" Performs the search by calling `perform_resources_iter` and returns a list of Resource instances.
@@ -171,8 +179,10 @@ def perform_resources_iter(self, server) -> Iterator['Resource']:
171179
:returns: An iterator of Resource instances
172180
"""
173181
for bundle in self.perform_iter(server):
174-
for entry in bundle.entry:
175-
yield entry.resource
182+
entries = bundle.entry or []
183+
for entry in entries:
184+
if entry.resource:
185+
yield entry.resource
176186

177187

178188
class FHIRSearchParam(object):
@@ -205,7 +215,7 @@ def handle(self):
205215
return handler.handle(self.copy())
206216

207217
def as_parameter(self):
208-
""" Return a string that represents the reciever as "key=value".
218+
""" Return a string that represents the receiver as "key=value".
209219
"""
210220
return '{}={}'.format(self.name, quote_plus(self.value, safe=',<=>'))
211221

tests/models/fhirsearch_perform_iter_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,40 @@ def test_perform_resources_iter_multiple_pages(self):
240240
self.assertEqual(result[1].id, "3124")
241241
self.assertEqual(result[2].id, "3125")
242242

243+
@responses.activate
244+
def test_perform_resources_iter_null_bundle(self):
245+
"""This happens when no results are found for a search."""
246+
# Mock the network response for the initial search request
247+
bundle_content = {
248+
"resourceType": "Bundle",
249+
"type": "searchset",
250+
"entry": None,
251+
}
252+
253+
# Mock the single page response
254+
self.add_mock_response("https://example.com/Bundle?patient=347&_count=1", bundle_content)
255+
256+
# Call perform_resources_iter with the server URL
257+
result = list(self.search.perform_resources_iter(self.mock_server))
258+
self.assertEqual(result, [])
259+
260+
@responses.activate
261+
def test_perform_resources_iter_null_resource(self):
262+
"""This shouldn't happen for search results, but the spec allows .resource to be empty"""
263+
# Mock the network response for the initial search request
264+
bundle_content = {
265+
"resourceType": "Bundle",
266+
"type": "searchset",
267+
"entry": [{}],
268+
}
269+
270+
# Mock the single page response
271+
self.add_mock_response("https://example.com/Bundle?patient=347&_count=1", bundle_content)
272+
273+
# Call perform_resources_iter with the server URL
274+
result = list(self.search.perform_resources_iter(self.mock_server))
275+
self.assertEqual(result, [])
276+
243277

244278
# Network-level Mocking
245279
class MockServer(server.FHIRServer):

0 commit comments

Comments
 (0)