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

Create endpoint for checking LORIS integration - retrieve server configuration (M2-7429) #1548

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a64b123
feat: implemented retrieval of integrations
david-montano-metalab Aug 19, 2024
a03a350
fix: ruff issues
david-montano-metalab Sep 13, 2024
cc514a7
fix: fixing integrations access validation
david-montano-metalab Sep 13, 2024
14bb17e
fix: ruff format
david-montano-metalab Sep 13, 2024
f10c8fa
fix: specifying https in loris client
david-montano-metalab Sep 13, 2024
3f1008e
fix: creating public and private versions of integrations domain object
david-montano-metalab Sep 13, 2024
540c96f
fix: fixing ruff
david-montano-metalab Sep 13, 2024
5e105e4
fix: ruff reformat some files
david-montano-metalab Sep 13, 2024
af69828
fix: fixing the retrieval of loris projects
david-montano-metalab Sep 14, 2024
dd759f2
fix: added new test for retrieval of configurations
david-montano-metalab Sep 14, 2024
ed97c5b
fix: adding integration retrieval test
david-montano-metalab Sep 14, 2024
4d2e9d1
fix: fix ruff formatting
david-montano-metalab Sep 14, 2024
f96e7a9
fix: removing type in check access
david-montano-metalab Sep 16, 2024
8ea76da
fix: removing unneded errors
david-montano-metalab Sep 16, 2024
d95b7a9
fix: remove import
david-montano-metalab Sep 16, 2024
ffb658f
fix: fixing test
david-montano-metalab Sep 16, 2024
1454e95
fix: fixes wrong reference to AvailableIntegrations type when retriev…
david-montano-metalab Sep 17, 2024
c79d14b
fix: running make cq to fix files with formatting issues
david-montano-metalab Sep 17, 2024
763819a
fix: removing enable and disable integration endpoints
david-montano-metalab Sep 17, 2024
d062779
fix: removing uneeded change on ReviewFlow
david-montano-metalab Sep 17, 2024
f76c63b
fix: removing files that have nothing to do with the PR contents and …
david-montano-metalab Sep 18, 2024
b171730
fix: updating name of integrations service retriever given that it re…
david-montano-metalab Sep 20, 2024
a5a0021
fix: renaming integration service method retrieve_integration back
david-montano-metalab Sep 20, 2024
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
24 changes: 17 additions & 7 deletions src/apps/integrations/api/integrations.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import uuid

from fastapi import Body, Depends

from apps.authentication.deps import get_current_user
from apps.integrations.domain import Integration, IntegrationFilter
from apps.integrations.loris.domain.loris_integrations import IntegrationsCreate
from apps.integrations.loris.domain.loris_projects import LorisProjects
from apps.integrations.service import IntegrationService
from apps.shared.domain import ResponseMulti
from apps.shared.query_params import QueryParams, parse_query_params
Expand All @@ -12,7 +12,7 @@
from infrastructure.database import atomic
from infrastructure.database.deps import get_session

__all__ = ["enable_integration", "disable_integration", "create_integration"]
__all__ = ["enable_integration", "disable_integration", "create_integration", "retrieve_integration"]


async def enable_integration(
Expand All @@ -35,11 +35,21 @@ async def disable_integration(


async def create_integration(
session=Depends(get_session),
user: User = Depends(get_current_user),
integrationsCreate: Integration = Body(...),
) -> Integration:
await CheckAccessService(session, user.id).check_integrations_access(integrationsCreate.applet_id)
async with atomic(session):
return await IntegrationService(session, user).create_integration(integrationsCreate)


async def retrieve_integration(
type: str,
applet_id: uuid.UUID,
session=Depends(get_session),
user: User = Depends(get_current_user),
params: IntegrationsCreate = Body(...),
) -> LorisProjects:
await CheckAccessService(session, user.id).check_integrations_create_access(params.applet_id, type)
) -> Integration:
await CheckAccessService(session, user.id).check_integrations_access(applet_id)
async with atomic(session):
return await IntegrationService(session, user).create_integration(type, params)
return await IntegrationService(session, user).retrieve_integration(applet_id, type)
12 changes: 12 additions & 0 deletions src/apps/integrations/crud/integrations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import uuid

from sqlalchemy import select
from sqlalchemy.engine import Result
from sqlalchemy.exc import IntegrityError

from apps.integrations.db.schemas import IntegrationsSchema
Expand All @@ -20,3 +24,11 @@ async def create(self, schema: IntegrationsSchema) -> IntegrationsSchema:
type=schema.type, applet_id=schema.applet_id
)
return new_integrations

async def retrieve_by_applet_and_type(self, applet_id: uuid.UUID, type: str) -> IntegrationsSchema:
query = select(IntegrationsSchema)
query = query.where(IntegrationsSchema.applet_id == applet_id)
query = query.where(IntegrationsSchema.type == type)
query = query.limit(1)
result: Result = await self._execute(query)
return result.scalars().first()
48 changes: 47 additions & 1 deletion src/apps/integrations/domain.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@
import json
import uuid
from enum import Enum

from apps.shared.domain import InternalModel
from apps.integrations.db.schemas import IntegrationsSchema
from apps.integrations.loris.domain.loris_integrations import LorisIntegration, LorisIntegrationPublic
from apps.shared.domain import InternalModel, PublicModel


class AvailableIntegrations(str, Enum):
LORIS = "LORIS"
FUTURE = "FUTURE"


class FutureIntegration(InternalModel):
endpoint: str
api_key: str

@classmethod
def from_schema(cls, schema: IntegrationsSchema):
new_future_integration_dict = json.loads(schema.configuration.replace("'", '"'))
new_future_integration = cls(
endpoint=new_future_integration_dict["endpoint"],
api_key=new_future_integration_dict["api_key"],
)
return new_future_integration

def __repr__(self):
return "FutureIntegration()"


class FutureIntegrationPublic(PublicModel):
endpoint: str

@classmethod
def from_schema(cls, schema: IntegrationsSchema):
new_future_integration_dict = json.loads(schema.configuration.replace("'", '"'))
new_future_integration = cls(
endpoint=new_future_integration_dict["endpoint"],
)
return new_future_integration

def __repr__(self):
return "FutureIntegrationPublic()"


class Integration(InternalModel):
integration_type: AvailableIntegrations
applet_id: uuid.UUID
configuration: FutureIntegrationPublic | LorisIntegrationPublic | FutureIntegration | LorisIntegration

@classmethod
def from_schema(cls, schema: IntegrationsSchema):
new_integration = cls(
applet_id=schema.applet_id, integration_type=schema.type, configuration=schema.configuration
)
return new_integration


class IntegrationFilter(InternalModel):
Expand Down
11 changes: 11 additions & 0 deletions src/apps/integrations/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,16 @@ class IntegrationsConfigurationsTypeAlreadyAssignedToAppletError(ValidationError
message = _("Provided Integration Type `{type}` has previously been tied to applet `{applet_id}`.")


class UnexpectedPropertiesForIntegration(ValidationError):
message = _(
"""Provided configurations `{provided_keys}` for Integration Type `{type}` were not expected.
Expected keys are: `{expected_keys}`"""
)


class UnsupportedIntegrationError(ValidationError):
message = _("The specified integration type `{type}` is not supported")


class UnavailableIntegrationError(ValidationError):
message = _("The specified integration type `{type}` does not exist for applet `{applet_id}`")
14 changes: 14 additions & 0 deletions src/apps/integrations/loris/api/applets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from apps.answers.deps.preprocess_arbitrary import get_answer_session
from apps.authentication.deps import get_current_user
from apps.integrations.loris.domain.domain import PublicListOfVisits, UploadableAnswersResponse, VisitsForUsers
from apps.integrations.loris.domain.loris_projects import LorisProjects
from apps.integrations.loris.service.loris import LorisIntegrationService
from apps.users.domain import User
from apps.workspaces.service.check_access import CheckAccessService
Expand All @@ -16,6 +17,7 @@
"start_transmit_process",
"visits_list",
"users_info_with_visits",
"get_loris_projects"
]


Expand Down Expand Up @@ -59,3 +61,15 @@ async def users_info_with_visits(
# TODO move to worker
info, count = await loris_service.get_uploadable_answers()
return UploadableAnswersResponse(result=info, count=count)


async def get_loris_projects(
hostname: str,
username: str,
password: str,
user: User = Depends(get_current_user),
session=Depends(get_session),
) -> LorisProjects:
return await LorisIntegrationService(
uuid.UUID("00000000-0000-0000-0000-000000000000"), session=session, user=user
).get_loris_projects(hostname, username, password)
47 changes: 31 additions & 16 deletions src/apps/integrations/loris/domain/loris_integrations.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
import uuid
import json

from apps.integrations.db.schemas import IntegrationsSchema
from apps.integrations.domain import AvailableIntegrations
from apps.shared.domain import InternalModel
from apps.shared.domain import InternalModel, PublicModel


class IntegrationMeta(InternalModel):
class LorisIntegration(InternalModel):
hostname: str
username: str
password: str
project: str | None


class Integrations(InternalModel):
applet_id: uuid.UUID
type: AvailableIntegrations
configuration: IntegrationMeta
password: str | None
project: str

@classmethod
def from_schema(cls, schema: IntegrationsSchema):
return cls(applet_id=schema.applet_id, type=schema.type, configuration=schema.configuration)
new_loris_integration_dict = json.loads(schema.configuration.replace("'", '"'))
new_loris_integration = cls(
hostname=new_loris_integration_dict["hostname"],
username=new_loris_integration_dict["username"],
password=new_loris_integration_dict["password"],
project=new_loris_integration_dict["project"],
)
return new_loris_integration

def __repr__(self):
return "LorisIntegration()"


class IntegrationsCreate(InternalModel):
applet_id: uuid.UUID
class LorisIntegrationPublic(PublicModel):
hostname: str
username: str
password: str
project: str

@classmethod
def from_schema(cls, schema: IntegrationsSchema):
new_loris_integration_dict = json.loads(schema.configuration.replace("'", '"'))
new_loris_integration = cls(
hostname=new_loris_integration_dict["hostname"],
username=new_loris_integration_dict["username"],
project=new_loris_integration_dict["project"],
)
return new_loris_integration

def __repr__(self):
return "LorisIntegrationPublic()"
14 changes: 13 additions & 1 deletion src/apps/integrations/loris/router.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from fastapi.routing import APIRouter
from starlette import status

from apps.integrations.loris.api import start_transmit_process, users_info_with_visits, visits_list
from apps.integrations.loris.api import get_loris_projects, start_transmit_process, users_info_with_visits, visits_list
from apps.integrations.loris.api.consent import (
consent_create,
consent_get_by_id,
consent_get_by_user_id,
consent_update,
)
from apps.integrations.loris.domain.domain import PublicConsent, PublicListOfVisits, UploadableAnswersResponse
from apps.integrations.loris.domain.loris_projects import LorisProjects
from apps.shared.domain import Response
from apps.shared.domain.response import (
AUTHENTICATION_ERROR_RESPONSES,
Expand Down Expand Up @@ -105,3 +106,14 @@
**AUTHENTICATION_ERROR_RESPONSES,
},
)(users_info_with_visits)


router.get(
"/projects",
status_code=status.HTTP_200_OK,
responses={
status.HTTP_200_OK: {"model": LorisProjects},
**DEFAULT_OPENAPI_RESPONSE,
**AUTHENTICATION_ERROR_RESPONSES,
},
)(get_loris_projects)
28 changes: 27 additions & 1 deletion src/apps/integrations/loris/service/loris.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from apps.answers.errors import ReportServerError
from apps.answers.service import ReportServerService
from apps.applets.crud.applets_history import AppletHistoriesCRUD
from apps.integrations.crud.integrations import IntegrationsCRUD, IntegrationsSchema
from apps.integrations.domain import AvailableIntegrations
from apps.integrations.loris.crud.user_relationship import MlLorisUserRelationshipCRUD
from apps.integrations.loris.db.schemas import MlLorisUserRelationshipSchema
from apps.integrations.loris.domain.domain import (
Expand All @@ -29,7 +31,10 @@
UploadableAnswersData,
UserVisits,
)
from apps.integrations.loris.domain.loris_integrations import LorisIntegration, LorisIntegrationPublic
from apps.integrations.loris.domain.loris_projects import LorisProjects
from apps.integrations.loris.errors import LorisServerError
from apps.integrations.loris.service.loris_client import LorisClient
from apps.subjects.crud import SubjectsCrud
from apps.users.domain import User
from apps.workspaces.crud.user_applet_access import UserAppletAccessCRUD
Expand All @@ -55,6 +60,7 @@ def __init__(self, applet_id: uuid.UUID, session, user: User, answer_session=Non
self.applet_id = applet_id
self.session = session
self.user = user
self.type = AvailableIntegrations.LORIS
self._answer_session = answer_session

@property
Expand Down Expand Up @@ -271,7 +277,7 @@ async def _prepare_answers(self, users_answers: dict, activities: dict):
answers_for_loris_by_respondent[user].update(answers_for_loris)
return answers_for_loris_by_respondent

async def _ml_answer_to_loris(
async def _ml_answer_to_loris( # noqa: C901
self, answer_id: str, activity_id: str, version: str, items: list, data: list
) -> dict:
loris_answers: dict = {}
Expand Down Expand Up @@ -853,3 +859,23 @@ async def _create_integration_alerts(self, applet_id: uuid.UUID, message: str):
except Exception as e:
sentry_sdk.capture_exception(e)
break

async def create_loris_integration(self, hostname, username, project) -> LorisIntegration:
integration_schema = await IntegrationsCRUD(self.session).create(
IntegrationsSchema(
applet_id=self.applet_id,
type=self.type,
configuration={
"hostname": hostname,
"username": username,
"project": project,
},
)
)
return LorisIntegrationPublic.from_schema(integration_schema)

async def get_loris_projects(self, hostname, username, password) -> LorisProjects:
token = await LorisClient.login_to_loris(hostname, username, password)
projects_raw = await LorisClient.list_projects(hostname, token)
projects = list(projects_raw["Projects"].keys())
return LorisProjects(projects=projects)
16 changes: 10 additions & 6 deletions src/apps/integrations/loris/service/loris_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json

import aiohttp
from aiohttp.client_exceptions import ClientConnectorError
from aiohttp.client_exceptions import ClientConnectorError, ContentTypeError

from apps.integrations.loris.errors import LorisBadCredentialsError, LorisInvalidHostname, LorisInvalidTokenError
from apps.shared.domain.custom_validations import InvalidUrlError, validate_url
Expand All @@ -10,8 +10,9 @@
class LorisClient:
@classmethod
async def login_to_loris(self, hostname: str, username: str, password: str) -> str:
url = f"https://{hostname}/api/v0.0.3/login"
try:
hostname = validate_url(hostname)
hostname = validate_url(url)
except InvalidUrlError as iue:
raise LorisInvalidHostname(hostname=hostname) from iue
timeout = aiohttp.ClientTimeout(total=60)
Expand All @@ -22,11 +23,14 @@ async def login_to_loris(self, hostname: str, username: str, password: str) -> s
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.post(
f"{hostname}/login",
url,
data=json.dumps(loris_login_data),
) as resp:
if resp.status == 200:
response_data = await resp.json()
try:
response_data = await resp.json()
except ContentTypeError as cce:
raise LorisBadCredentialsError(message=cce.message)
return response_data["token"]
else:
error_message = await resp.text()
Expand All @@ -36,8 +40,9 @@ async def login_to_loris(self, hostname: str, username: str, password: str) -> s

@classmethod
async def list_projects(self, hostname: str, token: str):
url = f"https://{hostname}/api/v0.0.3/projects"
try:
hostname = validate_url(hostname)
hostname = validate_url(url)
except InvalidUrlError as iue:
raise LorisInvalidHostname(hostname=hostname) from iue
headers = {
Expand All @@ -47,7 +52,6 @@ async def list_projects(self, hostname: str, token: str):
}
timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession(timeout=timeout) as session:
url = f"{hostname}/projects"
async with session.get(
url=url,
headers=headers,
Expand Down
Loading
Loading