diff --git a/app/aidbox/operations.py b/app/aidbox/operations.py index cbc3420..61589e1 100644 --- a/app/aidbox/operations.py +++ b/app/aidbox/operations.py @@ -8,14 +8,17 @@ assemble, constraint_check, extract, - extract_questionnaire_instance, get_questionnaire_context, populate, resolve_expression, ) -from ..sdc.utils import parameter_to_env +from ..sdc.utils import parameter_to_env, validate_context from ..utils import get_extract_services from .utils import AidboxSdcRequest, aidbox_operation, get_user_sdk_client, prepare_args +from ..sdc.exception import ( + ConstraintCheckOperationOutcome, + MissingParamOperationOutcome, +) @aidbox_operation(["GET"], ["Questionnaire", {"name": "id"}, "$assemble"]) @@ -28,7 +31,9 @@ async def assemble_op(request: AidboxSdcRequest): ) assembled_questionnaire_lazy = await assemble(request.fhir_client, questionnaire) - assembled_questionnaire = json.loads(json.dumps(assembled_questionnaire_lazy, default=list)) + assembled_questionnaire = json.loads( + json.dumps(assembled_questionnaire_lazy, default=list) + ) if request.is_fhir: assembled_questionnaire = await from_first_class_extension( assembled_questionnaire, request.aidbox_client @@ -41,15 +46,15 @@ async def assemble_op(request: AidboxSdcRequest): async def constraint_check_operation(request: AidboxSdcRequest): env = parameter_to_env(request.resource) - questionnaire = ( + fce_questionnaire = ( await to_first_class_extension(env["Questionnaire"], request.aidbox_client) if request.is_fhir else env["Questionnaire"] ) - as_root = questionnaire.get("runOnBehalfOfRoot") + as_root = fce_questionnaire.get("runOnBehalfOfRoot") client = client if as_root else get_user_sdk_client(request.request, request.client) - return web.json_response(await constraint_check(client, env)) + return web.json_response(await constraint_check(client, fce_questionnaire, env)) @aidbox_operation(["POST"], ["Questionnaire", "$context"]) @@ -57,14 +62,14 @@ async def constraint_check_operation(request: AidboxSdcRequest): async def get_questionnaire_context_operation(request: AidboxSdcRequest): env = parameter_to_env(request.resource) - questionnaire = ( + fce_questionnaire = ( await to_first_class_extension(env["Questionnaire"], request.aidbox_client) if request.is_fhir else env["Questionnaire"] ) - as_root = questionnaire.get("runOnBehalfOfRoot") + as_root = fce_questionnaire.get("runOnBehalfOfRoot") client = client if as_root else get_user_sdk_client(request.request, request.client) - result = await get_questionnaire_context(client, env) + result = await get_questionnaire_context(client, fce_questionnaire, env) return web.json_response(result) @@ -74,39 +79,50 @@ async def get_questionnaire_context_operation(request: AidboxSdcRequest): async def extract_questionnaire_operation(request: AidboxSdcRequest): resource = request.resource - as_root = False if resource["resourceType"] == "QuestionnaireResponse": env = {} - questionnaire_response = resource - questionnaire = ( + env_questionnaire_response = resource + fce_questionnaire = ( await request.aidbox_client.resources("Questionnaire") .search(_id=resource["questionnaire"]) .get() ) - as_root = questionnaire.get("runOnBehalfOfRoot") + env_questionnaire = ( + await from_first_class_extension(fce_questionnaire, request.aidbox_client) + if request.is_fhir + else fce_questionnaire + ) elif resource["resourceType"] == "Parameters": env = parameter_to_env(request.resource) - questionnaire = ( + if "Questionnaire" not in env: + raise MissingParamOperationOutcome("`Questionnaire` parameter is required") + if "QuestionnaireResponse" not in env: + raise MissingParamOperationOutcome( + "`QuestionnaireResponse` parameter is required" + ) + + fce_questionnaire = ( await to_first_class_extension(env["Questionnaire"], request.aidbox_client) if request.is_fhir else env["Questionnaire"] ) - questionnaire_response = env.get("QuestionnaireResponse") - as_root = questionnaire.get("runOnBehalfOfRoot") + env_questionnaire = env["Questionnaire"] + env_questionnaire_response = env["QuestionnaireResponse"] mappings = [ await request.aidbox_client.resources("Mapping").search(_id=m["id"]).get() - for m in questionnaire.get("mapping", []) + for m in fce_questionnaire.get("mapping", []) ] context = { - "Questionnaire": questionnaire, - "QuestionnaireResponse": questionnaire_response, + "Questionnaire": env_questionnaire, + "QuestionnaireResponse": env_questionnaire_response, **env, } + as_root = fce_questionnaire.get("runOnBehalfOfRoot") client = client if as_root else get_user_sdk_client(request.request, request.client) - await constraint_check(client, context) + await constraint_check(client, fce_questionnaire, context) extraction_result = await extract( client, mappings, context, get_extract_services(request.request["app"]) ) @@ -117,75 +133,127 @@ async def extract_questionnaire_operation(request: AidboxSdcRequest): @prepare_args async def extract_questionnaire_instance_operation(request: AidboxSdcRequest): resource = request.resource - questionnaire = ( + fce_questionnaire = ( await request.aidbox_client.resources("Questionnaire") .search(_id=request.route_params["id"]) .get() ) - as_root = questionnaire.get("runOnBehalfOfRoot") + env_questionnaire = ( + await from_first_class_extension(fce_questionnaire, request.aidbox_client) + if request.is_fhir + else fce_questionnaire + ) + as_root = fce_questionnaire.get("runOnBehalfOfRoot") extract_client = ( - request.client if as_root else get_user_sdk_client(request.request, request.client) + request.client + if as_root + else get_user_sdk_client(request.request, request.client) ) return web.json_response( await extract_questionnaire_instance( request.aidbox_client, extract_client, - questionnaire, + fce_questionnaire, + env_questionnaire, resource, get_extract_services(request.request["app"]), ) ) +async def extract_questionnaire_instance( + aidbox_client, + extract_client, + fce_questionnaire, + env_questionnaire, + resource, + extract_services, +): + if resource["resourceType"] == "QuestionnaireResponse": + env = {} + env_questionnaire_response = extract_client.resource( + "QuestionnaireResponse", **resource + ) + elif resource["resourceType"] == "Parameters": + env = parameter_to_env(resource) + if "QuestionnaireResponse" not in env: + raise MissingParamOperationOutcome( + "`QuestionnaireResponse` parameter is required" + ) + + env_questionnaire_response = env["QuestionnaireResponse"] + else: + raise MissingParamOperationOutcome( + "Either `QuestionnaireResponse` resource or Parameters containing QuestionnaireResponse are required", + ) + + if "launchContext" in fce_questionnaire: + validate_context(fce_questionnaire["launchContext"], env) + context = { + "QuestionnaireResponse": env_questionnaire_response, + "Questionnaire": env_questionnaire, + **env, + } + mappings = [ + await aidbox_client.resources("Mapping").search(_id=m["id"]).get() + for m in fce_questionnaire.get("mapping", []) + ] + await constraint_check(extract_client, fce_questionnaire, context) + + return await extract(extract_client, mappings, context, extract_services) + + @aidbox_operation(["POST"], ["Questionnaire", "$populate"]) @prepare_args async def populate_questionnaire(request: AidboxSdcRequest): env = parameter_to_env(request.resource) if "Questionnaire" not in env: - # TODO: return OperationOutcome - return web.json_response( - { - "error": "bad_request", - "error_description": "`Questionnaire` parameter is required", - }, - status=422, - ) + raise MissingParamOperationOutcome("`Questionnaire` parameter is required") - questionnaire = ( + fce_questionnaire = ( await to_first_class_extension(env["Questionnaire"], request.aidbox_client) if request.is_fhir else env["Questionnaire"] ) - as_root = questionnaire.get("runOnBehalfOfRoot") - client = request.client if as_root else get_user_sdk_client(request.request, request.client) + as_root = fce_questionnaire.get("runOnBehalfOfRoot") + client = ( + request.client + if as_root + else get_user_sdk_client(request.request, request.client) + ) - populated_resource = await populate(client, questionnaire, env) + fce_populated_qr = await populate(client, fce_questionnaire, env) if request.is_fhir: - populated_resource = await from_first_class_extension( - populated_resource, request.aidbox_client + fce_populated_qr = await from_first_class_extension( + fce_populated_qr, request.aidbox_client ) - return web.json_response(populated_resource) + return web.json_response(fce_populated_qr) @aidbox_operation(["POST"], ["Questionnaire", {"name": "id"}, "$populate"]) @prepare_args async def populate_questionnaire_instance(request: AidboxSdcRequest): - questionnaire = ( + fce_questionnaire = ( await request.aidbox_client.resources("Questionnaire") .search(_id=request.route_params["id"]) .get() ) env = parameter_to_env(request.resource) - as_root = questionnaire.get("runOnBehalfOfRoot") + env["Questionnaire"] = ( + await from_first_class_extension(fce_questionnaire, request.aidbox_client) + if request.is_fhir + else fce_questionnaire + ) + as_root = fce_questionnaire.get("runOnBehalfOfRoot") client = client if as_root else get_user_sdk_client(request.request, request.client) - populated_resource = await populate(client, questionnaire, env) + fce_populated_qr = await populate(client, fce_questionnaire, env) if request.is_fhir: - populated_resource = await from_first_class_extension( - populated_resource, request.aidbox_client + fce_populated_qr = await from_first_class_extension( + fce_populated_qr, request.aidbox_client ) - return web.json_response(populated_resource) + return web.json_response(fce_populated_qr) @aidbox_operation(["POST"], ["Questionnaire", "$resolve-expression"], public=True) diff --git a/app/fhir_server/operations.py b/app/fhir_server/operations.py index 1df8025..2adb7ab 100644 --- a/app/fhir_server/operations.py +++ b/app/fhir_server/operations.py @@ -37,11 +37,12 @@ async def assemble_handler(request: web.BaseRequest): @routes.post("/QuestionnaireResponse/$constraint-check") async def constraint_check_handler(request: web.BaseRequest): env = parameter_to_env(await request.json()) + # TODO: I believe there's a bug, it should be in FHIR format env["Questionnaire"] = to_first_class_extension(env["Questionnaire"]) env["QuestionnaireResponse"] = to_first_class_extension(env["QuestionnaireResponse"]) client = request.app["client"] - return web.json_response(await constraint_check(client, env)) + return web.json_response(await constraint_check(client, env["Questionnaire"], env)) @routes.post("/Questionnaire/$context") @@ -50,7 +51,13 @@ async def get_questionnaire_context_handler(request: web.BaseRequest): env = parameter_to_env(await request.json()) client = request.app["client"] - return web.json_response(await get_questionnaire_context(client, env)) + return web.json_response( + await get_questionnaire_context( + client, + to_first_class_extension(env["Questionnaire"]), + env + ) + ) @routes.post("/Questionnaire/$extract") @@ -93,14 +100,14 @@ async def extract_questionnaire_handler(request: web.BaseRequest): ] ) jute_templates.append(json.loads(template_string)) - + # TODO: I believe there's a bug, it should be in FHIR context = { "Questionnaire": to_first_class_extension(questionnaire), "QuestionnaireResponse": to_first_class_extension(questionnaire_response), **env, } - await constraint_check(client, context) + await constraint_check(client, to_first_class_extension(questionnaire), context) extraction_result = await extract( client, jute_templates, context, get_extract_services(request.app) ) @@ -142,11 +149,12 @@ async def extract_questionnaire_instance_operation(request: web.BaseRequest): if resource["resourceType"] == "QuestionnaireResponse": questionnaire_response = client.resource("QuestionnaireResponse", **resource) + # TODO: I believe there's a bug, it should be in FHIR context = { "Questionnaire": to_first_class_extension(questionnaire), "QuestionnaireResponse": questionnaire_response, } - await constraint_check(client, context) + await constraint_check(client, questionnaire, context) return web.json_response( await extract(client, jute_templates, context, get_extract_services(request.app)) ) @@ -176,7 +184,7 @@ async def extract_questionnaire_instance_operation(request: web.BaseRequest): "Questionnaire": questionnaire, **env, } - await constraint_check(client, context) + await constraint_check(client, questionnaire, context) return web.json_response( await extract(client, jute_templates, context, get_extract_services(request.app)) ) diff --git a/app/sdc/assemble.py b/app/sdc/assemble.py index d0cd04d..aacabe8 100644 --- a/app/sdc/assemble.py +++ b/app/sdc/assemble.py @@ -19,59 +19,59 @@ PROPAGATE_ELEMENTS = ["itemContext", "itemPopulationContext"] -async def assemble(client, questionnaire): - root_elements = project(dict(questionnaire), WHITELISTED_ROOT_ELEMENTS.keys()) - questionnaire["item"] = await assemble_questionnaire( - client, questionnaire, questionnaire["item"], root_elements +async def assemble(client, fce_questionnaire): + root_elements = project(dict(fce_questionnaire), WHITELISTED_ROOT_ELEMENTS.keys()) + fce_questionnaire["item"] = await _assemble_questionnaire( + client, fce_questionnaire, fce_questionnaire["item"], root_elements ) - dict.update(questionnaire, root_elements) - questionnaire["assembledFrom"] = questionnaire["id"] - del questionnaire["id"] - return questionnaire + dict.update(fce_questionnaire, root_elements) + fce_questionnaire["assembledFrom"] = fce_questionnaire["id"] + del fce_questionnaire["id"] + return fce_questionnaire -async def load_sub_questionnaire(client, root_elements, parent_item, item): +async def _load_sub_questionnaire(client, root_elements, parent_item, item): if "subQuestionnaire" in item: - sub_fhir = ( + fhir_subq = ( await client.resources("Questionnaire").search(_id=item["subQuestionnaire"]).get() ) - sub = to_first_class_extension(sub_fhir) + fce_subq = to_first_class_extension(fhir_subq) variables = prepare_variables(item) - if validate_assemble_context(sub, variables): - sub = prepare_link_ids(sub, variables) + if _validate_assemble_context(fce_subq, variables): + fce_subq = prepare_link_ids(fce_subq, variables) - propagate = project(dict(sub), PROPAGATE_ELEMENTS) + propagate = project(dict(fce_subq), PROPAGATE_ELEMENTS) dict.update(parent_item, propagate) - root = project(dict(sub), WHITELISTED_ROOT_ELEMENTS.keys()) + root = project(dict(fce_subq), WHITELISTED_ROOT_ELEMENTS.keys()) for key, value in root.items(): uniqueness = WHITELISTED_ROOT_ELEMENTS[key] current = root_elements.get(key, []) new = concat(current, value) root_elements[key] = distinct(new, uniqueness) - return sub["item"] + return fce_subq["item"] return item -async def assemble_questionnaire(client, parent, questionnaire_items, root_elements): +async def _assemble_questionnaire(client, parent, questionnaire_items, root_elements): with_sub_items = questionnaire_items while len([i for i in with_sub_items if "subQuestionnaire" in i]) > 0: with_sub_items_futures = ( - load_sub_questionnaire(client, root_elements, parent, i) for i in with_sub_items + _load_sub_questionnaire(client, root_elements, parent, i) for i in with_sub_items ) with_sub_items = list(flatten(await asyncio.gather(*with_sub_items_futures))) resp = [] for i in with_sub_items: if "item" in i: - i["item"] = await assemble_questionnaire(client, i, i["item"], root_elements) + i["item"] = await _assemble_questionnaire(client, i, i["item"], root_elements) resp.append(i) return resp -def validate_assemble_context(questionnaire, variables: dict): +def _validate_assemble_context(questionnaire, variables: dict): if "assembleContext" not in questionnaire: return False diff --git a/app/sdc/constraint_check.py b/app/sdc/constraint_check.py index 32cfbd7..06407e0 100644 --- a/app/sdc/constraint_check.py +++ b/app/sdc/constraint_check.py @@ -4,20 +4,18 @@ from .utils import load_source_queries, validate_context -async def constraint_check(client, env): - questionnaire = env["Questionnaire"] - questionnaire_response = env["QuestionnaireResponse"] - if "launchContext" in questionnaire: - validate_context(questionnaire["launchContext"], env) - await load_source_queries(client, questionnaire, env) +async def constraint_check(client, fce_questionnaire, env): + if "launchContext" in fce_questionnaire: + validate_context(fce_questionnaire["launchContext"], env) + await load_source_queries(client, fce_questionnaire, env) errors = [] - constraint_check_for_item(errors, questionnaire, env) + _constraint_check_for_item(errors, fce_questionnaire, env) if len(errors) > 0: raise ConstraintCheckOperationOutcome(errors) - return questionnaire_response + return env["QuestionnaireResponse"] -def constraint_check_for_item(errors, questionnaire_item, env): +def _constraint_check_for_item(errors, questionnaire_item, env): for constraint in questionnaire_item.get("itemConstraint", []): expression = constraint["expression"] result = fhirpath({}, expression, env) @@ -27,4 +25,4 @@ def constraint_check_for_item(errors, questionnaire_item, env): errors.append(constraint) for item in questionnaire_item.get("item", []): - constraint_check_for_item(errors, item, env) + _constraint_check_for_item(errors, item, env) diff --git a/app/sdc/context.py b/app/sdc/context.py index fc52f77..720b7a9 100644 --- a/app/sdc/context.py +++ b/app/sdc/context.py @@ -1,7 +1,6 @@ from .utils import load_source_queries -async def get_questionnaire_context(client, env): - questionnaire = env["Questionnaire"] - await load_source_queries(client, questionnaire, env) +async def get_questionnaire_context(client, fce_questionnaire, env): + await load_source_queries(client, fce_questionnaire, env) return env diff --git a/app/sdc/exception.py b/app/sdc/exception.py index d0a8453..36c3c09 100644 --- a/app/sdc/exception.py +++ b/app/sdc/exception.py @@ -18,3 +18,17 @@ def __init__(self, validation_errors): ], }, ) + + +class MissingParamOperationOutcome(ConstraintCheckOperationOutcome): + def __init__(self, reason): + ConstraintCheckOperationOutcome.__init__( + self, + validation_errors=[ + { + "severity": "error", + "key": "missing-parameter", + "human": reason, + } + ], + ) diff --git a/app/sdc/extract.py b/app/sdc/extract.py index 1a67e7e..8047fb2 100644 --- a/app/sdc/extract.py +++ b/app/sdc/extract.py @@ -1,67 +1,5 @@ from aiohttp import ClientSession, web -from .constraint_check import constraint_check -from .exception import ConstraintCheckOperationOutcome -from .utils import parameter_to_env, validate_context - - -async def extract_questionnaire_instance( - aidbox_client, extract_client, questionnaire, resource, extract_services -): - # TODO move to Aidbox - if resource["resourceType"] == "QuestionnaireResponse": - questionnaire_response = extract_client.resource("QuestionnaireResponse", **resource) - context = {"Questionnaire": questionnaire, "QuestionnaireResponse": questionnaire_response} - mappings = [ - await aidbox_client.resources("Mapping").search(_id=m["id"]).get() - for m in questionnaire.get("mapping", []) - ] - await constraint_check(extract_client, context) - return await extract(extract_client, mappings, context, extract_services) - - if resource["resourceType"] == "Parameters": - env = parameter_to_env(resource) - - questionnaire_response_data = env.get("QuestionnaireResponse") - if not questionnaire_response_data: - raise ConstraintCheckOperationOutcome( - [ - { - "severity": "error", - "key": "missing-parameter", - "human": "`QuestionnaireResponse` parameter is required", - } - ] - ) - - questionnaire_response = extract_client.resource( - "QuestionnaireResponse", **questionnaire_response_data - ) - if "launchContext" in questionnaire: - validate_context(questionnaire["launchContext"], env) - context = { - "QuestionnaireResponse": questionnaire_response, - "Questionnaire": questionnaire, - **env, - } - mappings = [ - await aidbox_client.resources("Mapping").search(_id=m["id"]).get() - for m in questionnaire.get("mapping", []) - ] - await constraint_check(extract_client, context) - return await extract(extract_client, mappings, context, extract_services) - - raise ConstraintCheckOperationOutcome( - [ - { - "severity": "error", - "key": "missing-parameter", - "human": "Either `QuestionnaireResponse` resource or Parameters containing " - "QuestionnaireResponse are required", - } - ] - ) - async def external_service_extraction(client, service, template, context): async with ClientSession() as session: diff --git a/app/sdc/populate.py b/app/sdc/populate.py index 495da20..2e98618 100644 --- a/app/sdc/populate.py +++ b/app/sdc/populate.py @@ -5,24 +5,24 @@ from .utils import get_type, load_source_queries, validate_context -async def populate(client, questionnaire, env): - if "launchContext" in questionnaire: - validate_context(questionnaire["launchContext"], env) +async def populate(client, fce_questionnaire, env): + if "launchContext" in fce_questionnaire: + validate_context(fce_questionnaire["launchContext"], env) - await load_source_queries(client, questionnaire, env) + await load_source_queries(client, fce_questionnaire, env) root = { "resourceType": "QuestionnaireResponse", - "questionnaire": questionnaire.get("id"), + "questionnaire": fce_questionnaire.get("id"), "item": [], } - for item in questionnaire["item"]: - root["item"].extend(handle_item(item, env, {})) + for item in fce_questionnaire["item"]: + root["item"].extend(_handle_item(item, env, {})) return root -def handle_item(item, env, context): +def _handle_item(item, env, context): def init_item(): new_item = {"linkId": item["linkId"]} if "text" in item: @@ -41,7 +41,7 @@ def init_item(): for c in context: populated_items = [] for i in item["item"]: - populated_items.extend(handle_item(i, env, c)) + populated_items.extend(_handle_item(i, env, c)) root_item = init_item() root_item["item"] = populated_items @@ -83,7 +83,7 @@ def init_item(): if "item" in item: populated_items = [] for i in item["item"]: - populated_items.extend(handle_item(i, env, context)) + populated_items.extend(_handle_item(i, env, context)) root_item["item"] = populated_items diff --git a/app/sdc/utils.py b/app/sdc/utils.py index 0bbecf2..aefc875 100644 --- a/app/sdc/utils.py +++ b/app/sdc/utils.py @@ -42,7 +42,10 @@ def get_type(item, data): def walk_dict(d, transform): for k, v in d.items(): if is_list(v): - d[k] = [walk_dict(vi, transform) if is_mapping(vi) else transform(vi, k) for vi in v] + d[k] = [ + walk_dict(vi, transform) if is_mapping(vi) else transform(vi, k) + for vi in v + ] elif is_mapping(v): d[k] = walk_dict(v, transform) else: @@ -65,7 +68,9 @@ def prepare_link_ids(questionnaire, variables): def prepare_bundle(raw_bundle, env): - return walk_dict(raw_bundle, lambda v, _k: resolve_string_template(v, env, encode_result=True)) + return walk_dict( + raw_bundle, lambda v, _k: resolve_string_template(v, env, encode_result=True) + ) def resolve_string_template(i, env, encode_result=False): @@ -117,12 +122,13 @@ def parameter_to_env(resource): return env -async def load_source_queries(client, questionnaire, env): +async def load_source_queries(client, fce_questionnaire, env): contained = { - f"{item['resourceType']}#{item['id']}": item for item in questionnaire.get("contained", []) + f"{item['resourceType']}#{item['id']}": item + for item in fce_questionnaire.get("contained", []) } - source_queries = questionnaire.get("sourceQueries", {}) + source_queries = fce_questionnaire.get("sourceQueries", {}) if isinstance(source_queries, dict): source_queries = [source_queries]