Skip to content

Commit

Permalink
Merge pull request #349 from weni-ai/feature/eda-projects-consumers
Browse files Browse the repository at this point in the history
Create TemplateType and Project consumers and handle queues
  • Loading branch information
Sandro-Meireles authored Aug 24, 2023
2 parents 1dd46cd + 4b6f3bf commit c88b32b
Show file tree
Hide file tree
Showing 20 changed files with 511 additions and 3 deletions.
1 change: 1 addition & 0 deletions marketplace/applications/usecases/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .app_configuration import AppConfigurationUseCase
19 changes: 19 additions & 0 deletions marketplace/applications/usecases/app_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import TYPE_CHECKING


if TYPE_CHECKING: # pragma: no cover
from django.contrib.auth import get_user_model

from marketplace.core.types import AppType
from ..models import App

User = get_user_model()


class AppConfigurationUseCase: # pragma: no cover
def __init__(self, channel_client, channel_token_client):
self.__channel_client = channel_client
self.__channel_token_client = channel_token_client

def configure_app(self, app: "App", apptype: "AppType", user: "User") -> None:
apptype.configure_app(app, user, self.__channel_client, self.__channel_token_client)
3 changes: 1 addition & 2 deletions marketplace/event_driven/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
from .signals import message_started, message_finished


class EDAConsumer(ABC):

class EDAConsumer(ABC): # pragma: no cover
def handle(self, message: amqp.Message):
message_started.send(sender=self)
try:
Expand Down
4 changes: 3 additions & 1 deletion marketplace/event_driven/handle.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from amqp.channel import Channel

from marketplace.projects.handle import handle_consumers as project_handle_consumers


def handle_consumers(channel: Channel) -> None:
pass
project_handle_consumers(channel)
2 changes: 2 additions & 0 deletions marketplace/projects/consumers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .project_consumer import ProjectConsumer
from .template_type_consumer import TemplateTypeConsumer
47 changes: 47 additions & 0 deletions marketplace/projects/consumers/project_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import amqp
from sentry_sdk import capture_exception

from ..usecases import (
TemplateTypeIntegrationUseCase,
ProjectCreationUseCase,
AppSetupHandlerUseCase,
ProjectCreationDTO,
)
from marketplace.applications.usecases import AppConfigurationUseCase
from marketplace.connect.client import ConnectProjectClient, WPPRouterChannelClient
from marketplace.event_driven.parsers import JSONParser
from marketplace.event_driven.consumers import EDAConsumer


class ProjectConsumer(EDAConsumer): # pragma: no cover
def consume(self, message: amqp.Message):
print(f"[ProjectConsumer] - Consuming a message. Body: {message.body}")

try:
body = JSONParser.parse(message.body)

project_dto = ProjectCreationDTO(
uuid=body.get("uuid"),
name=body.get("name"),
is_template=body.get("is_template"),
date_format=body.get("date_format"),
template_type_uuid=body.get("template_type_uuid"),
timezone=body.get("timezone"),
)

connect_client = ConnectProjectClient()
wpp_router_client = WPPRouterChannelClient()
app_configuration = AppConfigurationUseCase(connect_client, wpp_router_client)

app_setup_handler = AppSetupHandlerUseCase(app_configuration)
template_type_integration = TemplateTypeIntegrationUseCase(app_setup_handler)

project_creation = ProjectCreationUseCase(template_type_integration)
project_creation.create_project(project_dto, body.get("user_email"))

message.channel.basic_ack(message.delivery_tag)

except Exception as exception:
capture_exception(exception)
message.channel.basic_reject(message.delivery_tag, requeue=False)
print(f"[ProjectConsumer] - Message rejected by: {exception}")
16 changes: 16 additions & 0 deletions marketplace/projects/consumers/template_type_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import amqp

from marketplace.event_driven.parsers import JSONParser
from marketplace.event_driven.consumers import EDAConsumer
from ..usecases import create_template_type


class TemplateTypeConsumer(EDAConsumer): # pragma: no cover
def consume(self, message: amqp.Message):
body = JSONParser.parse(message.body)

print(f"[TemplateTypeConsumer] - Consuming a message. Body: {body}")

create_template_type(uuid=body.get("uuid"), project_uuid=body.get("project_uuid"), name=body.get("name"))

message.channel.basic_ack(message.delivery_tag)
8 changes: 8 additions & 0 deletions marketplace/projects/handle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from amqp.channel import Channel

from .consumers import TemplateTypeConsumer, ProjectConsumer


def handle_consumers(channel: Channel) -> None:
channel.basic_consume("integrations.template-types", callback=TemplateTypeConsumer().handle)
channel.basic_consume("integrations.projects", callback=ProjectConsumer().handle)
5 changes: 5 additions & 0 deletions marketplace/projects/usecases/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .exceptions import InvalidProjectData
from .template_type_creation import create_template_type
from .project_creation import ProjectCreationUseCase, ProjectCreationDTO
from .app_setup_handler import AppSetupHandlerUseCase
from .template_type_integration import TemplateTypeIntegrationUseCase
33 changes: 33 additions & 0 deletions marketplace/projects/usecases/app_setup_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.contrib.auth import get_user_model

from ..models import Project, TemplateType
from .exceptions import InvalidTemplateTypeData
from marketplace.core.types import APPTYPES


User = get_user_model()


class AppSetupHandlerUseCase:
def __init__(self, app_configuration):
self.__app_configuration = app_configuration

def setup_apps_in_project(self, project: Project, template_type: TemplateType, user: User):
setup = template_type.setup

if setup == {}:
raise InvalidTemplateTypeData(f"The `setup` of TemplateType {template_type.uuid} is empty!")

for setup_app in setup.get("apps"):
code = setup_app.get("code")

if not code:
raise InvalidTemplateTypeData(f"The TemplateType {template_type.uuid} has an invalid setup!")

try:
apptype = APPTYPES.get(code)
except KeyError:
raise InvalidTemplateTypeData(f"TemplateType {template_type.uuid} has invalid app code!")

app = apptype.create_app(project_uuid=str(project.uuid), created_by=user)
self.__app_configuration.configure_app(app, apptype, user)
6 changes: 6 additions & 0 deletions marketplace/projects/usecases/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class InvalidProjectData(Exception):
pass


class InvalidTemplateTypeData(Exception):
pass
15 changes: 15 additions & 0 deletions marketplace/projects/usecases/interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING: # pragma: no cover
from django.contrib.auth import get_user_model

from ..models import Project

User = get_user_model()


class TemplateTypeIntegrationInterface(ABC): # pragma: no cover
@abstractmethod
def integrate_template_type_in_project(self, project: "Project", template_type_uuid: str, user: "User"):
pass
47 changes: 47 additions & 0 deletions marketplace/projects/usecases/project_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from dataclasses import dataclass
from django.contrib.auth import get_user_model

from ..models import Project
from .interfaces import TemplateTypeIntegrationInterface


User = get_user_model()


@dataclass
class ProjectCreationDTO:
uuid: str
name: str
is_template: bool
date_format: str
timezone: str
template_type_uuid: str


class ProjectCreationUseCase:
def __init__(self, template_type_integration: TemplateTypeIntegrationInterface):
self.__template_type_integration = template_type_integration

def get_or_create_user_by_email(self, email: str) -> tuple:
return User.objects.get_or_create(email=email)

def get_or_create_project(self, project_dto: ProjectCreationDTO, user: User) -> tuple:
return Project.objects.get_or_create(
uuid=project_dto.uuid,
defaults=dict(
name=project_dto.name,
date_format=project_dto.date_format,
timezone=project_dto.timezone,
created_by=user,
is_template=project_dto.is_template,
),
)

def create_project(self, project_dto: ProjectCreationDTO, user_email: str) -> None:
user, _ = self.get_or_create_user_by_email(user_email)
project, _ = self.get_or_create_project(project_dto, user)

if project_dto.is_template:
self.__template_type_integration.integrate_template_type_in_project(
project, project_dto.template_type_uuid, user
)
22 changes: 22 additions & 0 deletions marketplace/projects/usecases/template_type_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from marketplace.applications.models import App
from ..models import TemplateType, Project


def create_template_type(uuid: str, project_uuid: Project, name: str) -> TemplateType:
setup = {"apps": []}

for app in App.objects.filter(project_uuid=project_uuid):
try:
setup["apps"].append(app.apptype.template_type_setup())
except NotImplementedError as error:
print(error)
pass

template_type, created = TemplateType.objects.get_or_create(uuid=uuid, defaults=dict(name=name, setup=setup))

if not created:
template_type.name = name
template_type.setup = setup
template_type.save()

return template_type
33 changes: 33 additions & 0 deletions marketplace/projects/usecases/template_type_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import TYPE_CHECKING

from .interfaces import TemplateTypeIntegrationInterface
from .exceptions import InvalidTemplateTypeData
from ..models import TemplateType


if TYPE_CHECKING: # pragma: no cover
from ..models import Project
from django.contrib.auth import get_user_model

User = get_user_model()


class TemplateTypeIntegrationUseCase(TemplateTypeIntegrationInterface):
def __init__(self, app_setup_handler):
self.__app_setup_handler = app_setup_handler

def integrate_template_type_in_project(self, project: "Project", template_type_uuid: str, user: "User") -> None:
if project.template_type is not None:
raise InvalidTemplateTypeData(f"The project `{project.uuid}` already has an integrated template!")

if template_type_uuid is None:
raise InvalidTemplateTypeData("'template_type_uuid' cannot be empty when 'is_template' is True!")

try:
template_type = TemplateType.objects.get(uuid=template_type_uuid)
except TemplateType.DoesNotExist:
raise InvalidTemplateTypeData(f"Template Type with uuid `{template_type_uuid}` does not exists!")

self.__app_setup_handler.setup_apps_in_project(project, template_type, user)
project.template_type = template_type
project.save()
Empty file.
52 changes: 52 additions & 0 deletions marketplace/projects/usecases/tests/test_app_setup_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import uuid
from unittest.mock import MagicMock

from django.test import TestCase
from django.contrib.auth import get_user_model

from ..app_setup_handler import AppSetupHandlerUseCase
from ...models import Project, TemplateType
from ..exceptions import InvalidTemplateTypeData


User = get_user_model()


class AppSetupHandlerTestCase(TestCase):
def setUp(self):
self.app_configuration = MagicMock()
self.uescase = AppSetupHandlerUseCase(self.app_configuration)

self.user = User.objects.create_superuser(email="user@marketplace.ai")
self.project = Project.objects.create(uuid=uuid.uuid4(), name="Test Project", created_by=self.user)

def test_empty_setup_raises_invalid_template_type_data(self):
template_type = TemplateType.objects.create(uuid=uuid.uuid4(), name="Fake TT", setup={})

error_message = f"The `setup` of TemplateType {template_type.uuid} is empty!"
with self.assertRaisesMessage(InvalidTemplateTypeData, error_message):
self.uescase.setup_apps_in_project(self.project, template_type, self.user)

def test_setup_without_code_raises_invalid_template_type_data(self):
template_type = TemplateType.objects.create(uuid=uuid.uuid4(), name="Fake TT", setup={"apps": [{}]})

error_message = f"The TemplateType {template_type.uuid} has an invalid setup!"
with self.assertRaisesMessage(InvalidTemplateTypeData, error_message):
self.uescase.setup_apps_in_project(self.project, template_type, self.user)

def test_setup_without_invalid_code_raises_invalid_template_type_data(self):
template_type = TemplateType.objects.create(
uuid=uuid.uuid4(), name="Fake TT", setup={"apps": [{"code": "fake-code"}]}
)

error_message = f"TemplateType {template_type.uuid} has invalid app code!"
with self.assertRaisesMessage(InvalidTemplateTypeData, error_message):
self.uescase.setup_apps_in_project(self.project, template_type, self.user)

def test_configure_app_called_once_with_valid_template_type(self):
template_type = TemplateType.objects.create(
uuid=uuid.uuid4(), name="Fake TT", setup={"apps": [{"code": "wpp-demo"}]}
)

self.uescase.setup_apps_in_project(self.project, template_type, self.user)
self.app_configuration.configure_app.assert_called_once()
Loading

0 comments on commit c88b32b

Please sign in to comment.