From 7f4bb2e2943aa56b0c79cf09769d932ebd46cc39 Mon Sep 17 00:00:00 2001 From: Paul F Bugni Date: Tue, 14 May 2024 17:26:30 -0700 Subject: [PATCH] initial implementation for insertion of additional downstream results into an upstream request. far from complete - needs testing and some incomplete functions to be defined. --- confidential_backend/api/external_fhir.py | 100 ++++++++++++++++++++++ confidential_backend/api/fhir.py | 34 ++++++-- 2 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 confidential_backend/api/external_fhir.py diff --git a/confidential_backend/api/external_fhir.py b/confidential_backend/api/external_fhir.py new file mode 100644 index 00000000..8b9c9ae3 --- /dev/null +++ b/confidential_backend/api/external_fhir.py @@ -0,0 +1,100 @@ +"""Module for external FHIR implementation. + +External FHIR refers to an additional FHIR endpoint, not to be confused +with the upstream endpoint, named on launch as the `iss` parameter, as part +of the standard SoF protocol. + +External FHIR is outside the SoF protocol, used only when business logic +demands a secondary store must be used or considered. + +The two FHIR stores are unique silos, and therefore, mappings between the two +often require an additional mapping to determine, for example, the respective +ID for a Patient in each respective system. + +In order for the front-end apps to remain SoF compliant, expecting a single +FHIR endpoint for all requests, when configured to include an EXTERNAL_FHIR +endpoint - requests to the upstream FHIR are supplimented with resources from +the external source, but only when it makes sense. + +Rules for when to include a resource from the External source: +- when a single resource is requested by ID, only if the upstream source + yielded no results, will the search also be attempted from the external source +- when a resource type is requested naming a `subject` as a parameter, it + is assumed the subject parameter refers to the upstream Patient.id, + requiring a Patient.id map lookup and substitution. +- given the complexity of paging through result sets, and masquerading as + part of the upstream request, downstream results will NOT be included if + the upstream has more than a single page. + +""" + +def downstream_request(upstream_response, relative_path, **upstream_request_args): + """Combine downstream response with upstream as per rules + + See rules at top of module; determine if downstream request should + be modified or executed, then combine with upstream response + + :param upstream_response: JSON from upstream server, or None, in case of 404 + :param relative_path: the request path, minus server API portion + :param upstream_request_args: all args included in upstream request + :returns: JSON of response, inserted into upstream response if applicable + + """ + + def next_link_in_bundle(response_json): + """Check response - if a bundle with a next link, return True""" + if response_json and response_json['resourceType'] == 'Bundle': + # check for `relation: next` url in bundle links + for item in response_json.get("link", []): + if item.get("relation", "") == "next": + return True + + # multiple pages of results? don't insert more in paginated as + # we don't yet have the complex logic to keep pages in sync between the + # two servers. if the upstream results include a `next` page link, + # simply return upstream results + if next_link_in_bundle(upstream_response): + return upstream_response + + downstream_server = current_app.config['EXTERNAL_FHIR_API'] + request_args = upstream_request_args.copy() + request_args['url'] = '/'.join((downstream_server, relative_path)) + + # if request appears in ResourceType/ID format, return upstream + # result unless empty, as the request is assumed to be for a single + parts = relative_path.split('/') + if len(parts) == 2: + if upstream_response: + return upstream_response + if parts[0] == 'Patient': + # requires a map lookup for downstream version of patient.id + downstream_patient_id = patient_id_map(parts[1]) + request_args['url'] = '/'.join((downstream_server, parts[0], downsream_patient_id)) + response = requests.request(**request_args) + response.raise_for_status() + return response.json() + + if 'subject' in request_args['params']: + # must translate Subject.id to downstream version + upstream_patient_id = request_args['params']['subject'] + downstream_patient_id = patient_id_map(upstream_patient_id) + request_args['params']['subject'] = downstream_patient_id + + response = requests.request(**request_args) + try: + response.raise_for_status() + if next_link_in_bundle(response.json()): + current_app.logger.error( + "Paginated results un-reachable in downstream server " + f" {request_args['url']}/{request_args['params']}") + # insert response JSON into upstream results + collated = collate_results(upstream_response, response.json()) + return collated + except requests.exceptions.HTTPError as e: + if response.status_code == 404: + # no results downstream, just return upstream + return upstream_response + current_app.logger.error( + f"Failed request on downstream server {request_args['url']}" + f"/{request_args['params']} ;;; {e}") + raise e diff --git a/confidential_backend/api/fhir.py b/confidential_backend/api/fhir.py index 2ad2b8a1..e975db00 100644 --- a/confidential_backend/api/fhir.py +++ b/confidential_backend/api/fhir.py @@ -85,12 +85,28 @@ def route_fhir(relative_path, session_id): f'upstream headers (outgoing to {upstream_fhir_url}): ' f'{upstream_headers} ;;; params: {request.args} ;;; json: {request.json}') - upstream_response = requests.request( - url=upstream_fhir_url, - method=request.method, - headers=upstream_headers, - params=request.args, - json=request.json, - ) - upstream_response.raise_for_status() - return upstream_response.json() + upstream_request_args = { + 'url':upstream_fhir_url, + 'method':request.method, + 'headers':upstream_headers, + 'params':request.args, + 'json':request.json, + } + upstream_response = requests.request(**upstream_request_args) + include_downstream_requests = current_app.config.get('EXTERNAL_FHIR_API') + try: + upstream_response.raise_for_status() + resulting_json = upstream_response.json() + except requests.exceptions.HTTPError as e: + if upstream_response.status_code = 404 and include_downstream_requests: + # on a 404, give downstream a chance at the request + resulting_json = None + else: + raise e + + if current_app.config.get('EXTERNAL_FHIR_API'): + resulting_json = downstream_request( + upstream_response=resulting_json, + relative_path=relative_path, + **upstream_request_args) + return resulting_json