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 TemplateType and Project consumers #349

Merged
merged 20 commits into from
Aug 24, 2023
Merged
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
54a249d
feat: Create TemplateType and Project consumers and handle queues
Sandro-Meireles Jul 21, 2023
02bb331
feat: Usecase to create template type by project
Sandro-Meireles Jul 23, 2023
eb62734
feat: Usecase to setup template type apps in project
Sandro-Meireles Jul 23, 2023
d30502a
feat: Usecase to create project
Sandro-Meireles Jul 23, 2023
e004d37
feat: Add a consumer handler to the app project and add template type…
Sandro-Meireles Jul 23, 2023
56ccb19
feat: Add a project consumer
Sandro-Meireles Jul 23, 2023
e822012
fix: Handle exception on create template type
Sandro-Meireles Jul 23, 2023
8e27fb1
feat: Create a usecase to integrate a template type into a project
Sandro-Meireles Aug 17, 2023
29763b1
refactor: refactor the project creation usecase
Sandro-Meireles Aug 18, 2023
e961fef
feat: Adjust the project consumer to the new project creation usecase…
Sandro-Meireles Aug 18, 2023
ddc8308
feat: Handle project creation usecase exceptions to reject messages a…
Sandro-Meireles Aug 18, 2023
f470392
fix: Remove the verbose annotation from the ProjectCreationUseCase.ge…
Sandro-Meireles Aug 18, 2023
b19b28e
feat: Adjust consumers to inherit from EDAConsumer
Sandro-Meireles Aug 22, 2023
27699b1
refactor: Adjust ProjectConsumer ack logic
Aug 23, 2023
0dfc011
fix: Run black and adjust flake8 requirements
Sandro-Meireles Aug 23, 2023
ba72ec5
feat: Adds use case that allows configuring an app
Sandro-Meireles Aug 24, 2023
9bbd2d8
feat: Add unit tests to app setup handler usecase
Sandro-Meireles Aug 24, 2023
06ea17d
test: Covers the use case of creating template types with unit tests
Sandro-Meireles Aug 24, 2023
5822172
fix: Add # pragma: no cover in EDAConsumer
Sandro-Meireles Aug 24, 2023
4b6f3bf
fix: Adjusts the method that get user by email to create it if it doe…
Sandro-Meireles Aug 24, 2023
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
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
@@ -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:
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
51 changes: 51 additions & 0 deletions marketplace/projects/usecases/project_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from dataclasses import dataclass
from django.contrib.auth import get_user_model

from ..models import Project
from .interfaces import TemplateTypeIntegrationInterface
from .exceptions import InvalidProjectData


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_user_by_email(self, email: str) -> User:
try:
return User.objects.get(email=email)
except User.DoesNotExist:
raise InvalidProjectData(f"User with email `{email}` does not exist!")

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_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