Skip to content

Commit

Permalink
feat: implemented retrieval of integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
david-montano-metalab committed Sep 11, 2024
1 parent 95f315c commit a14dca3
Show file tree
Hide file tree
Showing 19 changed files with 344 additions and 91 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ run:
run_local:
${DOCKER_COMPOSE_CMD} up -d redis postgres mailhog rabbitmq

.PHONY: run_local_mac
run_local_mac:
docker compose up -d redis postgres mailhog rabbitmq

.PHONY: test
test:
${TEST_COMMAND} ./
Expand Down
2 changes: 0 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.4"

services:

postgres:
Expand Down
28 changes: 20 additions & 8 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.domain import Integration, IntegrationFilter, NewIntegration
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,23 @@ async def disable_integration(


async def create_integration(
session=Depends(get_session),
user: User = Depends(get_current_user),
integrationsCreate: NewIntegration = Body(...),
) -> NewIntegration:
await CheckAccessService(session, user.id).check_integrations_create_access(
integrationsCreate.applet_id, integrationsCreate.integration_type
)
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)
) -> NewIntegration:
await CheckAccessService(session, user.id).check_integrations_retrieve_access(applet_id, type)
async with atomic(session):
return await IntegrationService(session, user).create_integration(type, params)
return await IntegrationService(session, user).retrieve_integration(applet_id, type)
10 changes: 10 additions & 0 deletions src/apps/integrations/crud/integrations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
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 +22,11 @@ async def create(self, schema: IntegrationsSchema) -> IntegrationsSchema:
type=schema.type, applet_id=schema.applet_id
)
return new_integrations

async def retrieve(self, schema: IntegrationsSchema) -> IntegrationsSchema:
query = select(IntegrationsSchema)
query = query.where(IntegrationsSchema.applet_id == schema.applet_id)
query = query.where(IntegrationsSchema.type == schema.type)
query = query.limit(1)
result: Result = await self._execute(query)
return result.scalars().first()
35 changes: 35 additions & 0 deletions src/apps/integrations/domain.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
import json
import uuid
from enum import Enum
from typing import Union

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


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_dict["api_key"] = "*****"

new_future_integration = cls(
endpoint=new_future_integration_dict["endpoint"],
api_key=new_future_integration_dict["api_key"],
)
return new_future_integration


class Integration(InternalModel):
integration_type: AvailableIntegrations


class NewIntegration(InternalModel):
integration_type: AvailableIntegrations
applet_id: uuid.UUID
configuration: Union[LorisIntegration, FutureIntegration]

@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):
integration_types: list[AvailableIntegrations] | None
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}`")
19 changes: 14 additions & 5 deletions src/apps/integrations/loris/api/applets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@
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
from infrastructure.database.deps import get_session

__all__ = [
"start_transmit_process",
"visits_list",
"users_info_with_visits",
]
__all__ = ["start_transmit_process", "visits_list", "users_info_with_visits", "get_loris_projects"]


async def integration(applet_id: uuid.UUID, session, user, users_and_visits):
Expand Down Expand Up @@ -59,3 +56,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)
31 changes: 13 additions & 18 deletions src/apps/integrations/loris/domain/loris_integrations.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import uuid
import json

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


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
project: str

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


class IntegrationsCreate(InternalModel):
applet_id: uuid.UUID
hostname: str
username: str
password: str
new_loris_integration_dict = json.loads(schema.configuration.replace("'", '"'))
new_loris_integration_dict["password"] = "*****"

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
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)
27 changes: 27 additions & 0 deletions 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
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 @@ -853,3 +859,24 @@ 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, password, project) -> LorisIntegration:
integration_schema = await IntegrationsCRUD(self.session).create(
IntegrationsSchema(
applet_id=self.applet_id,
type=self.type,
configuration={
"hostname": hostname,
"username": username,
"password": password,
"project": project,
},
)
)
return LorisIntegration.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)
11 changes: 7 additions & 4 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 @@ -22,11 +22,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",
f"{hostname}/api/v0.0.3/login",
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 @@ -47,7 +50,7 @@ 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"
url = f"{hostname}/api/v0.0.3/projects"
async with session.get(
url=url,
headers=headers,
Expand Down
Loading

0 comments on commit a14dca3

Please sign in to comment.