Skip to content

Commit

Permalink
Get globals data from integrations and flows (#50)
Browse files Browse the repository at this point in the history
* Get globals data from integrations and flows

* Populate globals from services on integrate feature

* Return all infos from IntegratedFeatureSerializer after integrate feature
  • Loading branch information
elitonzky authored Oct 4, 2024
1 parent 6cd93cb commit 67b0bd8
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 39 deletions.
41 changes: 41 additions & 0 deletions retail/api/base_service_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# base_service_view.py
from rest_framework import views

from rest_framework.permissions import IsAuthenticated

from retail.clients.flows.client import FlowsClient
from retail.clients.integrations.client import IntegrationsClient
from retail.services.flows.service import FlowsService
from retail.services.integrations.service import IntegrationsService


class BaseServiceView(views.APIView):
"""
BaseServiceView is a base class that provides common service and client
injection logic for views. Other views should inherit from this class to
reuse the integration and flows service logic.
"""

permission_classes = [IsAuthenticated]

integrations_service_class = IntegrationsService
integrations_client_class = IntegrationsClient
flows_service_class = FlowsService
flows_client_class = FlowsClient

_integrations_service = None
_flows_service = None

@property
def integrations_service(self):
if not self._integrations_service:
self._integrations_service = self.integrations_service_class(
self.integrations_client_class()
)
return self._integrations_service

@property
def flows_service(self):
if not self._flows_service:
self._flows_service = self.flows_service_class(self.flows_client_class())
return self._flows_service
24 changes: 15 additions & 9 deletions retail/api/features/views.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
from rest_framework import views, status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated


from retail.api.base_service_view import BaseServiceView
from retail.api.features.serializers import FeaturesSerializer
from retail.api.usecases.remove_globals_keys import RemoveGlobalsKeysUsecase
from retail.features.models import Feature, IntegratedFeature


class FeaturesView(views.APIView):

permission_classes = [IsAuthenticated]

def get(self, request, project_uuid):
class FeaturesView(BaseServiceView):
def get(self, request, project_uuid: str):
try:

category = request.query_params.get("category", None)

integrated_features = IntegratedFeature.objects.filter(
project__uuid=project_uuid
).values_list("feature__uuid", flat=True)
Expand All @@ -26,7 +23,16 @@ def get(self, request, project_uuid):

serializer = FeaturesSerializer(features, many=True)

return Response({"results": serializer.data}, status=status.HTTP_200_OK)
usecase = RemoveGlobalsKeysUsecase(
integrations_service=self.integrations_service,
flows_service=self.flows_service,
)

# Execute usecase to modify globals
user_email = request.user.email
features_data = usecase.execute(serializer.data, user_email, project_uuid)

return Response({"results": features_data}, status=status.HTTP_200_OK)

except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
66 changes: 38 additions & 28 deletions retail/api/integrated_feature/views.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
from django.contrib.auth.models import User

from rest_framework import views, viewsets, status
from rest_framework import status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated


from retail.api.base_service_view import BaseServiceView
from retail.api.integrated_feature.serializers import IntegratedFeatureSerializer
from retail.api.usecases.populate_globals_values import PopulateGlobalsValuesUsecase

from retail.features.models import Feature, IntegratedFeature
from retail.features.integrated_feature_eda import IntegratedFeatureEDA
from retail.projects.models import Project


class IntegratedFeatureView(views.APIView):

permission_classes = [IsAuthenticated]

class IntegratedFeatureView(BaseServiceView):
def post(self, request, *args, **kwargs):
feature = Feature.objects.get(uuid=kwargs["feature_uuid"])
try:
Expand All @@ -23,9 +21,10 @@ def post(self, request, *args, **kwargs):
return Response(
status=status.HTTP_404_NOT_FOUND,
data={
"error": f"Project with uuid equals {request.data['project_uuid' ]} does not exists!"
"error": f"Project with uuid equals {request.data['project_uuid']} does not exist!"
},
)

user, _ = User.objects.get_or_create(email=request.user.email)
feature_version = feature.last_version

Expand All @@ -35,7 +34,7 @@ def post(self, request, *args, **kwargs):

sectors_data = []
integrated_feature.sectors = []
if feature_version.sectors != None:
if feature_version.sectors is not None:
for sector in feature_version.sectors:
for r_sector in request.data.get("sectors", []):
if r_sector.get("name") == sector.get("name"):
Expand All @@ -46,13 +45,19 @@ def post(self, request, *args, **kwargs):
}
integrated_feature.sectors.append(new_sector)
break
for globals_key, globals_value in request.data.get(
"globals_values", {}
).items():
integrated_feature.globals_values[globals_key] = globals_value
integrated_feature.save(
update_fields=["sectors", "globals_values"]
)

# Treat and fill specific globals
fill_globals_usecase = PopulateGlobalsValuesUsecase(
self.integrations_service, self.flows_service
)
treated_globals_values = fill_globals_usecase.execute(
request.data.get("globals_values", {}),
request.user.email,
request.data["project_uuid"],
)
# Add all globals from the request, including treated ones
for globals_key, globals_value in treated_globals_values.items():
integrated_feature.globals_values[globals_key] = globals_value

for sector in integrated_feature.sectors:
sectors_data.append(
Expand All @@ -68,24 +73,26 @@ def post(self, request, *args, **kwargs):
actions = []
for function in feature.functions.all():
function_last_version = function.last_version
if function_last_version.action_base_flow_uuid != None:
if function_last_version.action_base_flow_uuid is not None:
actions.append(
{
"name": function_last_version.action_name,
"prompt": function_last_version.action_prompt,
"root_flow_uuid": str(function_last_version.action_base_flow_uuid),
"type": ""
"root_flow_uuid": str(
function_last_version.action_base_flow_uuid
),
"type": "",
}
)
if feature_version.action_base_flow_uuid:
actions.append(
{
"name": feature_version.action_name,
"prompt": feature_version.action_prompt,
"root_flow_uuid": str(feature_version.action_base_flow_uuid),
"type": ""
}
)
{
"name": feature_version.action_name,
"prompt": feature_version.action_prompt,
"root_flow_uuid": str(feature_version.action_base_flow_uuid),
"type": "",
}
)

body = {
"definition": integrated_feature.feature_version.definition,
Expand All @@ -95,11 +102,13 @@ def post(self, request, *args, **kwargs):
"feature_version": str(integrated_feature.feature_version.uuid),
"feature_uuid": str(integrated_feature.feature.uuid),
"sectors": sectors_data,
"action": actions
"action": actions,
}

IntegratedFeatureEDA().publisher(body=body, exchange="integrated-feature.topic")
print(f"message send `integrated feature` - body: {body}")
print(f"message sent `integrated feature` - body: {body}")

serializer = IntegratedFeatureSerializer(integrated_feature.feature)

response = {
"status": 200,
Expand All @@ -109,6 +118,7 @@ def post(self, request, *args, **kwargs):
"project": integrated_feature.project.uuid,
"user": integrated_feature.user.email,
"integrated_on": integrated_feature.integrated_on,
**serializer.data,
},
}
return Response(response)
Expand Down
53 changes: 53 additions & 0 deletions retail/api/usecases/populate_globals_values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
PopulateGlobalsValuesUsecase is responsible for filling in specific global keys
in the globals_values dictionary using data fetched from external services.
Attributes:
integrations_service: Service used to fetch data related to VTEX integrations.
flows_service: Service used to fetch user API tokens from the flows system.
"""


class PopulateGlobalsValuesUsecase:
def __init__(self, integrations_service, flows_service):
self.integrations_service = integrations_service
self.flows_service = flows_service

def execute(self, globals_values: dict, user_email: str, project_uuid: str) -> dict:
"""
Fill in the keys of globals_values using the appropriate services.
Only the following keys are manipulated:
- x_vtex_api_appkey
- x_vtex_api_apptoken
- url_api_vtex
- api_token (from flows, mapped to 'token')
"""
filled_globals_values = globals_values.copy()

# Handle the keys related to the integrations service
integration_data = self.integrations_service.get_vtex_integration_detail(
project_uuid
)
if integration_data:
if "url_api_vtex" in globals_values:
filled_globals_values["url_api_vtex"] = integration_data.get(
"domain", globals_values["url_api_vtex"]
)
if "x_vtex_api_appkey" in globals_values:
filled_globals_values["x_vtex_api_appkey"] = integration_data.get(
"app_key", globals_values["x_vtex_api_appkey"]
)
if "x_vtex_api_apptoken" in globals_values:
filled_globals_values["x_vtex_api_apptoken"] = integration_data.get(
"app_token", globals_values["x_vtex_api_apptoken"]
)

# Handle the key related to the flows service (api_token)
flow_data = self.flows_service.get_user_api_token(user_email, project_uuid)
if flow_data:
if "api_token" in globals_values:
filled_globals_values["api_token"] = flow_data.get(
"token", globals_values["api_token"]
)

return filled_globals_values
55 changes: 55 additions & 0 deletions retail/api/usecases/remove_globals_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
RemoveGlobalsKeysUsecase is responsible for removing specific global keys
from the feature's globals list based on data fetched from external services.
Attributes:
integrations_service: Service used to fetch data related to VTEX integrations.
flows_service: Service used to fetch user API tokens from the flows system.
"""


class RemoveGlobalsKeysUsecase:
def __init__(self, integrations_service, flows_service):
self.integrations_service = integrations_service
self.flows_service = flows_service

def execute(
self, features: list[dict], user_email: str, project_uuid: str
) -> list[dict]:
# Fetch data from the services, handling cases where data might be None
integrations_data = self.integrations_service.get_vtex_integration_detail(
project_uuid
)
flows_data = self.flows_service.get_user_api_token(user_email, project_uuid)

for feature in features:
globals_to_remove = []

# Check and mark globals for removal based on integrations data if available
if integrations_data:
if "x_vtex_api_appkey" in feature["globals"] and integrations_data.get(
"app_key"
):
globals_to_remove.append("x_vtex_api_appkey")
if "x_vtex_api_apptoken" in feature[
"globals"
] and integrations_data.get("app_token"):
globals_to_remove.append("x_vtex_api_apptoken")
if "url_api_vtex" in feature["globals"] and integrations_data.get(
"domain"
):
globals_to_remove.append("url_api_vtex")

# Check and mark globals for removal based on flows data if available
if flows_data:
if "user_api_token" in feature["globals"] and flows_data.get(
"api_token"
):
globals_to_remove.append("user_api_token")

# Remove the marked globals
feature["globals"] = [
g for g in feature["globals"] if g not in globals_to_remove
]

return features
3 changes: 2 additions & 1 deletion retail/clients/flows/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from django.conf import settings

from retail.clients.base import RequestClient, InternalAuthentication
from retail.interfaces.clients.flows.interface import FlowsClientInterface


class FlowsClient(RequestClient):
class FlowsClient(RequestClient, FlowsClientInterface):
def __init__(self):
self.base_url = settings.FLOWS_REST_ENDPOINT
self.authentication_instance = InternalAuthentication()
Expand Down
3 changes: 2 additions & 1 deletion retail/clients/integrations/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from django.conf import settings

from retail.clients.base import RequestClient, InternalAuthentication
from retail.interfaces.clients.integrations.interface import IntegrationsClientInterface


class IntegrationsClient(RequestClient):
class IntegrationsClient(RequestClient, IntegrationsClientInterface):
def __init__(self):
self.base_url = settings.INTEGRATIONS_REST_ENDPOINT
self.authentication_instance = InternalAuthentication()
Expand Down
7 changes: 7 additions & 0 deletions retail/interfaces/clients/flows/interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from abc import ABC, abstractmethod


class FlowsClientInterface(ABC):
@abstractmethod
def get_user_api_token(self, user_email: str, project_uuid: str):
pass
7 changes: 7 additions & 0 deletions retail/interfaces/clients/integrations/interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from abc import ABC, abstractmethod


class IntegrationsClientInterface(ABC):
@abstractmethod
def get_vtex_integration_detail(self, project_uuid: str):
pass
Empty file added retail/services/__init__.py
Empty file.
Empty file.
18 changes: 18 additions & 0 deletions retail/services/flows/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from retail.clients.exceptions import CustomAPIException
from retail.interfaces.clients.flows.interface import FlowsClientInterface


class FlowsService:
def __init__(self, client: FlowsClientInterface):
self.client = client

def get_user_api_token(self, user_email: str, project_uuid: str) -> dict:
"""
Retrieve the user API token for a given email and project UUID.
Handles communication errors and returns None in case of failure.
"""
try:
return self.client.get_user_api_token(user_email, project_uuid)
except CustomAPIException as e:
print(f"Error {e.status_code} when retrieving user API token for project {project_uuid}.")
return None
Empty file.
Loading

0 comments on commit 67b0bd8

Please sign in to comment.