From 1792aeb20b98d51404b6bc7c0e8e70de88193dec Mon Sep 17 00:00:00 2001 From: Natasha Singh Date: Thu, 28 Mar 2024 14:43:03 -0400 Subject: [PATCH 1/4] :sparkles: Add patient us-core-race and us-core-ethnicity search params --- .../SearchParameter-us-core-ethnicity.json | 60 +++++++++++++++++++ .../SearchParameter-us-core-race.json | 60 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 smilecdr/search_parameters/SearchParameter-us-core-ethnicity.json create mode 100644 smilecdr/search_parameters/SearchParameter-us-core-race.json diff --git a/smilecdr/search_parameters/SearchParameter-us-core-ethnicity.json b/smilecdr/search_parameters/SearchParameter-us-core-ethnicity.json new file mode 100644 index 0000000..ac0bebb --- /dev/null +++ b/smilecdr/search_parameters/SearchParameter-us-core-ethnicity.json @@ -0,0 +1,60 @@ +{ + "resourceType": "SearchParameter", + "id": "us-core-ethnicity", + "url": "http://hl7.org/fhir/us/core/SearchParameter/us-core-ethnicity", + "version": "7.0.0", + "name": "USCoreEthnicity", + "status": "active", + "date": "2022-04-14", + "publisher": "HL7 International / Cross-Group Projects", + "contact": [ + { + "name": "HL7 International / Cross-Group Projects", + "telecom": [ + { + "system": "url", + "value": "http://www.hl7.org/Special/committees/cgp" + }, + { + "system": "email", + "value": "cgp@lists.HL7.org" + } + ] + } + ], + "description": "Returns patients with an ethnicity extension matching the specified code.", + "jurisdiction": [ + { + "coding": [ + { + "system": "urn:iso:std:iso:3166", + "code": "US" + } + ] + } + ], + "code": "ethnicity", + "base": ["Patient"], + "type": "token", + "expression": "Patient.extension.where(url = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity').extension.value.code", + "xpathUsage": "normal", + "multipleOr": true, + "_multipleOr": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation", + "valueCode": "MAY" + } + ] + }, + "multipleAnd": true, + "_multipleAnd": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation", + "valueCode": "MAY" + } + ] + } +} + diff --git a/smilecdr/search_parameters/SearchParameter-us-core-race.json b/smilecdr/search_parameters/SearchParameter-us-core-race.json new file mode 100644 index 0000000..c56ff7b --- /dev/null +++ b/smilecdr/search_parameters/SearchParameter-us-core-race.json @@ -0,0 +1,60 @@ +{ + "resourceType": "SearchParameter", + "id": "us-core-race", + "url": "http://hl7.org/fhir/us/core/SearchParameter/us-core-race", + "version": "7.0.0", + "name": "USCoreRace", + "status": "active", + "date": "2022-04-14", + "publisher": "HL7 International / Cross-Group Projects", + "contact": [ + { + "name": "HL7 International / Cross-Group Projects", + "telecom": [ + { + "system": "url", + "value": "http://www.hl7.org/Special/committees/cgp" + }, + { + "system": "email", + "value": "cgp@lists.HL7.org" + } + ] + } + ], + "description": "Returns patients with a race extension matching the specified code.", + "jurisdiction": [ + { + "coding": [ + { + "system": "urn:iso:std:iso:3166", + "code": "US" + } + ] + } + ], + "code": "race", + "base": ["Patient"], + "type": "token", + "expression": "Patient.extension.where(url = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race').extension.value.code", + "xpathUsage": "normal", + "multipleOr": true, + "_multipleOr": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation", + "valueCode": "MAY" + } + ] + }, + "multipleAnd": true, + "_multipleAnd": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation", + "valueCode": "MAY" + } + ] + } +} + From 87e762fc1badecee9494eb8f5196b5b6609f10c9 Mon Sep 17 00:00:00 2001 From: Natasha Singh Date: Thu, 28 Mar 2024 14:46:15 -0400 Subject: [PATCH 2/4] :wrench: Configure search param endpoint and reindexing --- src/config/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/config/__init__.py b/src/config/__init__.py index 702fb06..70362da 100644 --- a/src/config/__init__.py +++ b/src/config/__init__.py @@ -5,6 +5,7 @@ ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname((__file__)))) DATA_DIR = os.path.join(ROOT_DIR, "data", "resources") SEED_USERS_FILEPATH = os.path.join(ROOT_DIR, "smilecdr/settings", "users.json") +SEARCH_PARAMETER_DIR = os.path.join(ROOT_DIR, "smilecdr/search_parameters") DOTENV_PATH = find_dotenv() if DOTENV_PATH: @@ -17,6 +18,7 @@ FHIR_APP_ADMIN_PW = os.environ.get("FHIR_APP_ADMIN_PW") FHIR_TEST_USER_PW = os.environ.get("FHIR_TEST_USER_PW") USER_MGMNT_ENDPOINT = os.environ.get("USER_MGMNT_ENDPOINT") +REINDEX_ENDPOINT = os.environ.get("REINDEX_ENDPOINT") KEYCLOAK_PROXY_URL = os.environ.get( "KEYCLOAK_PROXY_URL", "http://localhost:8081/keycloak-proxy" ) @@ -25,3 +27,11 @@ KEYCLOAK_READ_CLIENT_ID = os.environ.get("KEYCLOAK_READ_CLIENT_ID") KEYCLOAK_READ_CLIENT_SECRET = os.environ.get("KEYCLOAK_READ_CLIENT_SECRET") KEYCLOAK_ISSUER = os.environ.get("KEYCLOAK_ISSUER") + +REINDEX_PAYLOAD= { + "resourceType": "Parameters", + "parameter": [ { + "name": "url", + "valueString": "Patient?" + }] +} From e25116098883b58d785574845bde885a5500d1d6 Mon Sep 17 00:00:00 2001 From: Natasha Singh Date: Thu, 28 Mar 2024 14:47:36 -0400 Subject: [PATCH 3/4] :sparkles: Add send_request and read_json funcs to utils --- src/misc.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/misc.py b/src/misc.py index d98b622..aacdc31 100644 --- a/src/misc.py +++ b/src/misc.py @@ -1,11 +1,12 @@ +""" +Miscellaneous utility functions +""" import time +import json +from pprint import pformat import requests -from src.config import ( - BASE_URL, -) - def elapsed_time_hms(start_time): """ @@ -15,3 +16,72 @@ def elapsed_time_hms(start_time): return time.strftime('%H:%M:%S', time.gmtime(elapsed)) +def send_request(method, *args, ignore_status_codes=None, **kwargs): + """Send http request. Raise exception on status_code >= 300 + + :param method: name of the requests method to call + :type method: str + :raises: requests.Exception.HTTPError + :returns: requests Response object + :rtype: requests.Response + """ + if isinstance(ignore_status_codes, str): + ignore_status_codes = [ignore_status_codes] + + # NOTE: Set timeout so requests don't hang + # See https://requests.readthedocs.io/en/latest/user/advanced/#timeouts + if not kwargs.get("timeout"): + # connect timeout, read timeout + kwargs["timeout"] = (3, 60) + else: + print( + f"⌚️ Applying user timeout: {kwargs['timeout']} (connect, read)" + " seconds to request" + ) + + requests_op = getattr(requests, method.lower()) + status_code = 0 + try: + resp = requests_op(*args, **kwargs) + status_code = resp.status_code + resp.raise_for_status() + except requests.exceptions.HTTPError as e: + if ignore_status_codes and (status_code in ignore_status_codes): + pass + else: + body = "" + try: + body = pformat(resp.json()) + except: + body = resp.text + + msg = ( + f"❌ Problem sending {method} request to server\n" + f"{str(e)}\n" + f"args: {args}\n" + f"kwargs: {pformat(kwargs)}\n" + f"{body}\n" + ) + print(msg) + raise e + + return resp + + +def read_json(filepath, default=None): + """ + Read JSON file into Python dict. If default is not None and the file + does not exist, then return default. + + :param filepath: path to JSON file + :type filepath: str + :param default: default return value if file not found, defaults to None + :type default: any, optional + :returns: your data + :rtype: dict + """ + if (default is not None) and (not os.path.isfile(filepath)): + return default + + with open(filepath, "r") as json_file: + return json.load(json_file) From c55b1a9d5350b4d45c100466a21c0d94b26da6c2 Mon Sep 17 00:00:00 2001 From: Natasha Singh Date: Thu, 28 Mar 2024 14:47:55 -0400 Subject: [PATCH 4/4] :sparkles: Load search parameter script --- src/bin/load_search_parameters.py | 96 +++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100755 src/bin/load_search_parameters.py diff --git a/src/bin/load_search_parameters.py b/src/bin/load_search_parameters.py new file mode 100755 index 0000000..08204f9 --- /dev/null +++ b/src/bin/load_search_parameters.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +import os +import argparse +from pprint import pprint + +from requests.auth import HTTPBasicAuth + +from src.config import ( + SEARCH_PARAMETER_DIR, + REINDEX_PAYLOAD, + BASE_URL, + REINDEX_ENDPOINT, + FHIR_APP_ADMIN, + FHIR_APP_ADMIN_PW +) +from src.misc import ( + read_json, + send_request +) + + +def upsert_search_parameters( + client_id, client_secret, search_parameter_dir=SEARCH_PARAMETER_DIR +): + """ + Read search parameters from file, then upsert them in server + Last, reindex the resources so that SearchParameters take effect + """ + print("Loading search parameters") + + for fn in os.listdir(search_parameter_dir): + if not fn.endswith(".json"): + continue + filepath = os.path.join(search_parameter_dir, fn) + search_param = read_json(filepath) + + # Load search parameter + id_ = search_param["id"] + endpoint = "/".join( + part.strip("/") for part in [BASE_URL, "SearchParameter", id_] + ) + resp = send_request( + "put", + endpoint, + json=search_param, + auth=HTTPBasicAuth(client_id, client_secret), + ) + print( + f"PUT {endpoint}" + ) + + # Start reindexing operation + endpoint = f"{BASE_URL}/$reindex" + resp = send_request( + "post", + endpoint, + json=REINDEX_PAYLOAD, + auth=HTTPBasicAuth(client_id, client_secret), + ) + pprint(resp.json()) + + +def cli(): + """ + CLI for running this script + """ + parser = argparse.ArgumentParser( + description='Load SearchParameters in FHIR server' + ) + parser.add_argument( + "--client_id", + default=FHIR_APP_ADMIN, + help="Admin ID to authenticate with FHIR server", + ) + parser.add_argument( + "--client_secret", + default=FHIR_APP_ADMIN_PW, + help="Admin secret to authenticate with FHIR server", + ) + parser.add_argument( + "--search_parameter_dir", + default=SEARCH_PARAMETER_DIR, + help="Path to dir with SearchParameters", + ) + args = parser.parse_args() + + upsert_search_parameters( + args.client_id, args.client_secret, args.search_parameter_dir + ) + + print("✅ Load SearchParameters complete") + + +if __name__ == "__main__": + cli()