Skip to content
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

✨ Add patient us-core-race and us-core-ethnicity search params #190

Merged
merged 4 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions smilecdr/search_parameters/SearchParameter-us-core-ethnicity.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]"
}
]
}
],
"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"
}
]
}
}

60 changes: 60 additions & 0 deletions smilecdr/search_parameters/SearchParameter-us-core-race.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]"
}
]
}
],
"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"
}
]
}
}

96 changes: 96 additions & 0 deletions src/bin/load_search_parameters.py
Original file line number Diff line number Diff line change
@@ -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()
10 changes: 10 additions & 0 deletions src/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
)
Expand All @@ -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?"
}]
}
78 changes: 74 additions & 4 deletions src/misc.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand All @@ -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)
Loading