Skip to content

Commit

Permalink
Merge pull request #57 from beda-software/develop
Browse files Browse the repository at this point in the history
Multitenancy support
  • Loading branch information
ir4y authored Jan 24, 2024
2 parents 25bc43b + 5492d75 commit 64ca087
Show file tree
Hide file tree
Showing 25 changed files with 416 additions and 199 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ jobs:
docker build --cache-from ${{ env.CACHE_IMAGE }}
--tag ${{ env.BUILD_IMAGE }} .
- name: Run tests
run: ./run_test.sh
run: >-
mkdir ./tests_zen_project/zen-packages/ &&
ls -la ./tests_zen_project &&
id &&
chmod -R 0777 ./tests_zen_project &&
./run_test.sh
- name: Show logs
if: ${{ failure() }}
run: docker-compose -f docker-compose.tests.yaml logs
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
Expand Down
185 changes: 87 additions & 98 deletions app/aidbox/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

from aiohttp import web

from app.converter.fce_to_fhir import from_first_class_extension
from app.converter.fhir_to_fce import to_first_class_extension
from app.converter import from_first_class_extension, to_first_class_extension

from ..sdc import (
assemble,
Expand All @@ -16,82 +15,81 @@
)
from ..sdc.utils import parameter_to_env
from ..utils import get_extract_services
from .sdk import sdk
from .utils import get_aidbox_fhir_client, get_user_sdk_client
from .utils import AidboxSdcRequest, aidbox_operation, get_user_sdk_client, prepare_args


@sdk.operation(["GET"], ["Questionnaire", {"name": "id"}, "$assemble"])
@sdk.operation(["GET"], ["fhir", "Questionnaire", {"name": "id"}, "$assemble"])
async def assemble_op(operation, request):
is_fhir = operation["request"][1] == "fhir"

client = get_user_sdk_client(request)
@aidbox_operation(["GET"], ["Questionnaire", {"name": "id"}, "$assemble"])
@prepare_args
async def assemble_op(request: AidboxSdcRequest):
questionnaire = (
await client.resources("Questionnaire").search(_id=request["route-params"]["id"]).get()
await request.aidbox_client.resources("Questionnaire")
.search(_id=request.route_params["id"])
.get()
)

assembled_questionnaire_lazy = await assemble(client, questionnaire)
assembled_questionnaire_lazy = await assemble(request.fhir_client, questionnaire)
assembled_questionnaire = json.loads(json.dumps(assembled_questionnaire_lazy, default=list))
if is_fhir:
if request.is_fhir:
assembled_questionnaire = from_first_class_extension(assembled_questionnaire)
return web.json_response(assembled_questionnaire)


@sdk.operation(["POST"], ["QuestionnaireResponse", "$constraint-check"])
@sdk.operation(["POST"], ["fhir", "QuestionnaireResponse", "$constraint-check"])
async def constraint_check_operation(_operation, request):
env = parameter_to_env(request["resource"])
questionnaire = env["Questionnaire"]
client = (
request["app"]["client"]
if questionnaire.get("runOnBehalfOfRoot")
else get_user_sdk_client(request)
@aidbox_operation(["POST"], ["QuestionnaireResponse", "$constraint-check"])
@prepare_args
async def constraint_check_operation(request: AidboxSdcRequest):
env = parameter_to_env(request.resource)

questionnaire = (
to_first_class_extension(env["Questionnaire"]) if request.is_fhir else env["Questionnaire"]
)
as_root = 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))


@sdk.operation(["POST"], ["Questionnaire", "$context"])
@sdk.operation(["POST"], ["fhir", "Questionnaire", "$context"])
async def get_questionnaire_context_operation(_operation, request):
client = request["app"]["client"]
env = parameter_to_env(request["resource"])
questionnaire = env["Questionnaire"]
client = (
request["app"]["client"]
if questionnaire.get("runOnBehalfOfRoot")
else get_user_sdk_client(request)
@aidbox_operation(["POST"], ["Questionnaire", "$context"])
@prepare_args
async def get_questionnaire_context_operation(request: AidboxSdcRequest):
env = parameter_to_env(request.resource)

questionnaire = (
to_first_class_extension(env["Questionnaire"]) if request.is_fhir else env["Questionnaire"]
)
as_root = questionnaire.get("runOnBehalfOfRoot")
client = client if as_root else get_user_sdk_client(request.request, request.client)
result = await get_questionnaire_context(client, env)

return web.json_response(await get_questionnaire_context(client, env))
return web.json_response(result)


@sdk.operation(["POST"], ["Questionnaire", "$extract"])
@sdk.operation(["POST"], ["fhir", "Questionnaire", "$extract"])
async def extract_questionnaire_operation(operation, request):
is_fhir = operation["request"][1] == "fhir"
resource = request["resource"]
client = request["app"]["client"]
@aidbox_operation(["POST"], ["Questionnaire", "$extract"])
@prepare_args
async def extract_questionnaire_operation(request: AidboxSdcRequest):
resource = request.resource

run_on_behalf_of_root = False
as_root = False
if resource["resourceType"] == "QuestionnaireResponse":
env = {}
questionnaire_response = resource
questionnaire = (
await client.resources("Questionnaire").search(_id=resource["questionnaire"]).get()
await request.aidbox_client.resources("Questionnaire")
.search(_id=resource["questionnaire"])
.get()
)
run_on_behalf_of_root = questionnaire.get("runOnBehalfOfRoot")
as_root = questionnaire.get("runOnBehalfOfRoot")
elif resource["resourceType"] == "Parameters":
env = parameter_to_env(request["resource"])
questionnaire_data = env.get("Questionnaire")
env = parameter_to_env(request.resource)
questionnaire = (
to_first_class_extension(questionnaire_data) if is_fhir else questionnaire_data
to_first_class_extension(env["Questionnaire"])
if request.is_fhir
else env["Questionnaire"]
)
questionnaire_response = env.get("QuestionnaireResponse")
run_on_behalf_of_root = questionnaire.get("runOnBehalfOfRoot")
as_root = questionnaire.get("runOnBehalfOfRoot")

mappings = [
await client.resources("Mapping").search(_id=m["id"]).get()
await request.aidbox_client.resources("Mapping").search(_id=m["id"]).get()
for m in questionnaire.get("mapping", [])
]

Expand All @@ -101,46 +99,44 @@ async def extract_questionnaire_operation(operation, request):
**env,
}

client = request["app"]["client"] if run_on_behalf_of_root else get_user_sdk_client(request)
client = get_aidbox_fhir_client(client) if is_fhir else client

client = client if as_root else get_user_sdk_client(request.request, request.client)
await constraint_check(client, context)
extraction_result = await extract(
client, mappings, context, get_extract_services(request["app"])
client, mappings, context, get_extract_services(request.request["app"])
)
return web.json_response(extraction_result)


@sdk.operation(["POST"], ["Questionnaire", {"name": "id"}, "$extract"])
@sdk.operation(["POST"], ["fhir", "Questionnaire", {"name": "id"}, "$extract"])
async def extract_questionnaire_instance_operation(operation, request):
is_fhir = operation["request"][1] == "fhir"
resource = request["resource"]
client = request["app"]["client"]
@aidbox_operation(["POST"], ["Questionnaire", {"name": "id"}, "$extract"])
@prepare_args
async def extract_questionnaire_instance_operation(request: AidboxSdcRequest):
resource = request.resource
questionnaire = (
await client.resources("Questionnaire").search(_id=request["route-params"]["id"]).get()
await request.aidbox_client.resources("Questionnaire")
.search(_id=request.route_params["id"])
.get()
)
client = (
request["app"]["client"]
if questionnaire.get("runOnBehalfOfRoot")
else get_user_sdk_client(request)
as_root = questionnaire.get("runOnBehalfOfRoot")
extract_client = (
request.client if as_root else get_user_sdk_client(request.request, request.client)
)
extract_client = get_aidbox_fhir_client(client) if is_fhir else client
return web.json_response(
await extract_questionnaire_instance(
client, extract_client, questionnaire, resource, get_extract_services(request["app"])
request.aidbox_client,
extract_client,
questionnaire,
resource,
get_extract_services(request.request["app"]),
)
)


@sdk.operation(["POST"], ["Questionnaire", "$populate"])
@sdk.operation(["POST"], ["fhir", "Questionnaire", "$populate"])
async def populate_questionnaire(operation, request):
is_fhir = operation["request"][1] == "fhir"
client = request["app"]["client"]
env = parameter_to_env(request["resource"])
questionnaire_data = env["Questionnaire"]
if not questionnaire_data:
@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(
{
Expand All @@ -150,43 +146,36 @@ async def populate_questionnaire(operation, request):
status=422,
)

if is_fhir:
converted = to_first_class_extension(questionnaire_data)
questionnaire = client.resource("Questionnaire", **converted)
else:
questionnaire = client.resource("Questionnaire", **questionnaire_data)

client = client if questionnaire.get("runOnBehalfOfRoot") else get_user_sdk_client(request)

populated_resource = await populate(
get_aidbox_fhir_client(client) if is_fhir else client, questionnaire, env
questionnaire = (
to_first_class_extension(env["Questionnaire"]) if request.is_fhir else env["Questionnaire"]
)
if is_fhir:
as_root = 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)
if request.is_fhir:
populated_resource = from_first_class_extension(populated_resource)
return web.json_response(populated_resource)


@sdk.operation(["POST"], ["Questionnaire", {"name": "id"}, "$populate"])
@sdk.operation(["POST"], ["fhir", "Questionnaire", {"name": "id"}, "$populate"])
async def populate_questionnaire_instance(operation, request):
is_fhir = operation["request"][1] == "fhir"
client = request["app"]["client"]
@aidbox_operation(["POST"], ["Questionnaire", {"name": "id"}, "$populate"])
@prepare_args
async def populate_questionnaire_instance(request: AidboxSdcRequest):
questionnaire = (
await client.resources("Questionnaire").search(_id=request["route-params"]["id"]).get()
await request.aidbox_client.resources("Questionnaire")
.search(_id=request.route_params["id"])
.get()
)
env = parameter_to_env(request["resource"])
env["Questionnaire"] = questionnaire
client = client if questionnaire.get("runOnBehalfOfRoot") else get_user_sdk_client(request)
env = parameter_to_env(request.resource)
as_root = questionnaire.get("runOnBehalfOfRoot")
client = client if as_root else get_user_sdk_client(request.request, request.client)

populated_resource = await populate(
get_aidbox_fhir_client(client) if is_fhir else client, questionnaire, env
)
if is_fhir:
populated_resource = await populate(client, questionnaire, env)
if request.is_fhir:
populated_resource = from_first_class_extension(populated_resource)
return web.json_response(populated_resource)


@sdk.operation(["POST"], ["Questionnaire", "$resolve-expression"], public=True)
@sdk.operation(["POST"], ["fhir", "Questionnaire", "$resolve-expression"], public=True)
@aidbox_operation(["POST"], ["Questionnaire", "$resolve-expression"], public=True)
def resolve_expression_operation(_operation, request):
return web.json_response(resolve_expression(request["resource"]))
77 changes: 74 additions & 3 deletions app/aidbox/utils.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
from dataclasses import dataclass

from aidbox_python_sdk.aidboxpy import AsyncAidboxClient
from fhirpy import AsyncFHIRClient
from fhirpy.base import AsyncClient

from .sdk import sdk


def get_user_sdk_client(request):
def get_user_sdk_client(request, client=None):
headers = request["headers"].copy()
client = request["app"]["client"]
client = client or request["app"]["client"]

# We removed content-length because populate extract are post operations
# and post queries contains content-length that must not be set as default header
if "content-length" in headers:
headers.pop("content-length")

return AsyncAidboxClient(client.url, extra_headers=headers)
return type(client)(client.url, extra_headers=headers)


def get_aidbox_fhir_client(aidbox_client):
Expand All @@ -20,3 +25,69 @@ def get_aidbox_fhir_client(aidbox_client):
authorization=aidbox_client.authorization,
extra_headers=aidbox_client.extra_headers,
)


def get_organization_client(aidbox_client, organization):
if isinstance(organization, str):
org_id = organization
else:
org_id = organization.id
return AsyncFHIRClient(
f"{aidbox_client.url}/Organization/{org_id}/fhir/",
authorization=aidbox_client.authorization,
extra_headers=aidbox_client.extra_headers,
)


def get_clients(operation, request):
aidbox_client = request["app"]["client"]
if operation["request"][1] == "Organization":
is_fhir = True
fhir_client = get_organization_client(aidbox_client, request["route-params"]["org_id"])
else:
is_fhir = operation["request"][1] == "fhir"
fhir_client = get_aidbox_fhir_client(aidbox_client)
return is_fhir, aidbox_client, fhir_client, fhir_client if is_fhir else aidbox_client


@dataclass
class AidboxSdcRequest:
"""
Representation of SDC specific data
extracted from original aidbox request
"""

is_fhir: bool
aidbox_client: AsyncAidboxClient
fhir_client: AsyncFHIRClient
client: AsyncClient
route_params: dict
resource: dict
request: dict


def prepare_args(fn):
def wrap(operation, request):
is_fhir, aidbox_client, fhir_client, client = get_clients(operation, request)
request = AidboxSdcRequest(
is_fhir,
aidbox_client,
fhir_client,
client,
request["route-params"],
request.get("resource", None),
request,
)
return fn(request)

return wrap


def aidbox_operation(method, path, **kwrgs):
def register(fn):
sdk.operation(method, ["Organization", {"name": "org_id"}, "fhir"] + path, **kwrgs)(fn)
sdk.operation(method, path, **kwrgs)(fn)
sdk.operation(method, ["fhir"] + path, **kwrgs)(fn)
return fn

return register
2 changes: 2 additions & 0 deletions app/converter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .fce_to_fhir import from_first_class_extension
from .fhir_to_fce import to_first_class_extension
Loading

0 comments on commit 64ca087

Please sign in to comment.