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

Get globals data from integrations and flows #50

Merged
merged 4 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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