-
Notifications
You must be signed in to change notification settings - Fork 211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Pagination Features for FHIRClient and Bundle Classes #174
Merged
+656
−316
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import urllib | ||
from typing import Optional | ||
|
||
import requests | ||
|
||
from typing import TYPE_CHECKING, Iterable | ||
|
||
if TYPE_CHECKING: | ||
from fhirclient.server import FHIRServer | ||
from fhirclient.models.bundle import Bundle | ||
|
||
|
||
# Use forward references to avoid circular imports | ||
def _fetch_next_page(bundle: 'Bundle', server: 'FHIRServer') -> Optional['Bundle']: | ||
""" | ||
Fetch the next page of results using the `next` link provided in the bundle. | ||
|
||
Args: | ||
bundle (Bundle): The FHIR Bundle containing the `next` link. | ||
server (FHIRServer): The FHIR server instance for handling requests and authentication. | ||
|
||
Returns: | ||
Optional[Bundle]: The next page of results as a FHIR Bundle, or None if no "next" link is found. | ||
""" | ||
if next_link := _get_next_link(bundle): | ||
return _execute_pagination_request(next_link, server) | ||
return None | ||
|
||
|
||
def _get_next_link(bundle: 'Bundle') -> Optional[str]: | ||
""" | ||
Extract the `next` link from the Bundle's links. | ||
|
||
Args: | ||
bundle (Bundle): The FHIR Bundle containing pagination links. | ||
|
||
Returns: | ||
Optional[str]: The URL of the next page if available, None otherwise. | ||
""" | ||
if not bundle.link: | ||
return None | ||
|
||
for link in bundle.link: | ||
if link.relation == "next": | ||
return _sanitize_next_link(link.url) | ||
return None | ||
|
||
|
||
def _sanitize_next_link(next_link: str) -> str: | ||
""" | ||
Sanitize the `next` link by validating its scheme and hostname against the origin server. | ||
|
||
This function ensures the `next` link URL uses a valid scheme (`http` or `https`) and that it contains a | ||
hostname. This provides a basic safeguard against malformed URLs without overly restricting flexibility. | ||
|
||
Args: | ||
next_link (str): The raw `next` link URL. | ||
|
||
Returns: | ||
str: The validated URL. | ||
|
||
Raises: | ||
ValueError: If the URL's scheme is not `http` or `https`, or if the hostname does not match the origin server. | ||
""" | ||
|
||
parsed_url = urllib.parse.urlparse(next_link) | ||
|
||
# Validate scheme and netloc (domain) | ||
if parsed_url.scheme not in ["http", "https"]: | ||
raise ValueError("Invalid URL scheme in `next` link.") | ||
if not parsed_url.netloc: | ||
raise ValueError("Invalid URL domain in `next` link.") | ||
|
||
return next_link | ||
|
||
|
||
def _execute_pagination_request(sanitized_url: str, server: 'FHIRServer') -> 'Bundle': | ||
""" | ||
Execute the request to retrieve the next page using the sanitized URL via Bundle.read_from. | ||
|
||
Args: | ||
sanitized_url (str): The sanitized URL to fetch the next page. | ||
server (FHIRServer): The FHIR server instance to perform the request. | ||
|
||
Returns: | ||
Bundle: The next page of results as a FHIR Bundle. | ||
|
||
Raises: | ||
HTTPError: If the request fails due to network issues or server errors. | ||
""" | ||
from fhirclient.models.bundle import Bundle | ||
return Bundle.read_from(sanitized_url, server) | ||
|
||
|
||
def iter_pages(first_bundle: 'Bundle', server: 'FHIRServer') -> Iterable['Bundle']: | ||
""" | ||
Iterator that yields each page of results as a FHIR Bundle. | ||
|
||
Args: | ||
first_bundle (Optional[Bundle]): The first Bundle to start pagination. | ||
server (FHIRServer): The FHIR server instance to perform the request. | ||
|
||
Yields: | ||
Bundle: Each page of results as a FHIR Bundle. | ||
""" | ||
# Since _fetch_next_page can return None | ||
bundle: Optional[Bundle] = first_bundle | ||
while bundle: | ||
yield bundle | ||
bundle = _fetch_next_page(bundle, server) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,4 +35,5 @@ testpaths = "tests" | |
tests = [ | ||
"pytest >= 2.5", | ||
"pytest-cov", | ||
"responses", | ||
] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the spec, links are the uri type, which can be relative.
I assume they mean relative to the base FHIR URL (like how References can be
Patient/abc
and that can resolve tohttp://my-ehr-domain/fhir/r4/Patient/abc
). But I suppose they could also allow an absolute domain-less URL? Like/fhir/r4/Bundle/abc
. I'm not sure. All the FHIR spec examples have full URLs.And I suspect we haven't seen those in the wild? So maybe it's not worth supporting yet - but maybe leave a TODO comment here about it? Just so in the future if someone complains about their EHR's Bundles not being handled correctly, there's a breadcrumb.