Skip to content

Commit

Permalink
wip(env/viewset): Add support for uuid as lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
gagantrivedi committed Nov 29, 2024
1 parent 7ffa794 commit 43d1114
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 42 deletions.
11 changes: 11 additions & 0 deletions api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import typing
from unittest.mock import MagicMock
from uuid import UUID

import boto3
import pytest
Expand Down Expand Up @@ -394,6 +395,16 @@ def environment(project):
return Environment.objects.create(name="Test Environment", project=project)


@pytest.fixture()
def environment_uuid(environment: Environment) -> UUID:
return environment.uuid


@pytest.fixture()
def environment_client_api_key(environment: Environment) -> str:
return environment.api_key


@pytest.fixture()
def with_environment_permissions(
environment: Environment, staff_user: FFAdminUser
Expand Down
15 changes: 9 additions & 6 deletions api/edge_api/identities/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from edge_api.identities.models import EdgeIdentity
from environments.identities.traits.models import Trait
from environments.models import Environment
from features.models import Feature, FeatureState
from features.multivariate.models import MultivariateFeatureOption

Expand All @@ -27,19 +28,19 @@ def export_edge_identity_and_overrides( # noqa: C901
traits_export = []
identity_override_export = []

environment_uuid = Environment.objects.get(api_key=environment_api_key).uuid
feature_id_to_uuid: dict[int, str] = get_feature_uuid_cache(environment_api_key)
mv_feature_option_id_to_uuid: dict[int, str] = get_mv_feature_option_uuid_cache(
environment_api_key
)

while True:
response = EdgeIdentity.dynamo_wrapper.get_all_items(**kwargs)
for item in response["Items"]:
identifier = item["identifier"]
# export identity
identity_export.append(
export_edge_identity(
identifier, environment_api_key, item["created_date"]
)
export_edge_identity(identifier, environment_uuid, item["created_date"])
)
# export traits
for trait in item["identity_traits"]:
Expand All @@ -60,6 +61,7 @@ def export_edge_identity_and_overrides( # noqa: C901
export_edge_feature_state(
identifier,
environment_api_key,
environment_uuid,
featurestate_uuid,
feature_uuid,
override["enabled"],
Expand Down Expand Up @@ -132,21 +134,22 @@ def export_edge_trait(trait: dict, identifier: str, environment_api_key: str) ->


def export_edge_identity(
identifier: str, environment_api_key: str, created_date: str
identifier: str, environment_uuid: str, created_date: str
) -> dict:
return {
"model": "identities.identity",
"fields": {
"identifier": identifier,
"created_date": created_date,
"environment": [environment_api_key],
"environment": [environment_uuid],
},
}


def export_edge_feature_state(
identifier: str,
environment_api_key: str,
environment_uuid: str,
featurestate_uuid: str,
feature_uuid: str,
enabled: bool,
Expand All @@ -162,7 +165,7 @@ def export_edge_feature_state(
"updated_at": timezone.now().isoformat(),
"live_from": timezone.now().isoformat(),
"feature": [feature_uuid],
"environment": [environment_api_key],
"environment": [environment_uuid],
"identity": [
identifier,
environment_api_key,
Expand Down
9 changes: 7 additions & 2 deletions api/environments/managers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import typing

from django.db.models import Prefetch
from softdelete.models import SoftDeleteManager

from features.models import FeatureSegment, FeatureState
from features.multivariate.models import MultivariateFeatureStateValue
from segments.models import Segment

if typing.TYPE_CHECKING:
from environments.models import Environment


class EnvironmentManager(SoftDeleteManager):
def filter_for_document_builder(
Expand Down Expand Up @@ -55,5 +60,5 @@ def filter_for_document_builder(
def get_queryset(self):
return super().get_queryset().select_related("project", "project__organisation")

def get_by_natural_key(self, api_key):
return self.get(api_key=api_key)
def get_by_natural_key(self, uuid: str) -> "Environment":
return self.get(uuid=uuid)
30 changes: 30 additions & 0 deletions api/environments/migrations/0037_add_uuid_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 3.2.23 on 2024-11-27 08:46

from django.db import migrations, models
import uuid

from core.migration_helpers import AddDefaultUUIDs


class Migration(migrations.Migration):
atomic = False # avoid long running transaction
dependencies = [
("environments", "0036_add_is_creating_field"),
]

operations = [
migrations.AddField(
model_name="environment",
name="uuid",
field=models.UUIDField(editable=False, null=True),
),
migrations.RunPython(
AddDefaultUUIDs("environments", "environment"),
reverse_code=migrations.RunPython.noop,
),
migrations.AlterField(
model_name="environment",
name="uuid",
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
]
9 changes: 7 additions & 2 deletions api/environments/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import typing
import uuid
from copy import deepcopy

from core.models import abstract_base_auditable_model_factory
Expand Down Expand Up @@ -63,13 +64,16 @@
class Environment(
LifecycleModel,
abstract_base_auditable_model_factory(
change_details_excluded_fields=["updated_at"]
historical_records_excluded_fields=["uuid"],
change_details_excluded_fields=["updated_at"],
),
SoftDeleteObject,
):
history_record_class_path = "environments.models.HistoricalEnvironment"
related_object_type = RelatedObjectType.ENVIRONMENT

uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)

name = models.CharField(max_length=2000)
created_date = models.DateTimeField("DateCreated", auto_now_add=True)
description = models.TextField(null=True, blank=True, max_length=20000)
Expand Down Expand Up @@ -166,7 +170,7 @@ def __str__(self):
return "Project %s - Environment %s" % (self.project.name, self.name)

def natural_key(self):
return (self.api_key,)
return (self.uuid,)

def clone(
self, name: str, api_key: str = None, clone_feature_states_async: bool = False
Expand All @@ -179,6 +183,7 @@ def clone(
clone = deepcopy(self)
clone.id = None
clone.name = name
clone.uuid = uuid.uuid4()
clone.api_key = api_key if api_key else create_hash()
clone.is_creating = True
clone.save()
Expand Down
1 change: 1 addition & 0 deletions api/environments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class Meta:
model = Environment
fields = (
"id",
"uuid",
"name",
"api_key",
"description",
Expand Down
27 changes: 24 additions & 3 deletions api/environments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
Expand All @@ -29,6 +30,7 @@
UserObjectPermissionsSerializer,
)
from projects.models import Project
from util.uuid import is_valid_uuid
from webhooks.mixins import TriggerSampleWebhookMixin
from webhooks.webhooks import WebhookType

Expand Down Expand Up @@ -69,7 +71,7 @@
),
)
class EnvironmentViewSet(viewsets.ModelViewSet):
lookup_field = "api_key"
lookup_field = "api_key" # This is deprecated please uuid
permission_classes = [EnvironmentPermissions]

def get_serializer_class(self):
Expand All @@ -91,6 +93,19 @@ def get_serializer_context(self):
context["environment"] = self.get_object()
return context

def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
lookup_value = self.kwargs[self.lookup_field]
if is_valid_uuid(lookup_value):
obj = get_object_or_404(queryset, uuid=lookup_value)
else:
obj = get_object_or_404(queryset, api_key=lookup_value)

# May raise a permission denied
self.check_object_permissions(self.request, obj)

return obj

def get_queryset(self):
if self.action == "list":
project_id = self.request.query_params.get(
Expand All @@ -113,8 +128,14 @@ def get_queryset(self):
# Since we don't have the environment at this stage, we would need to query the database
# regardless, so it seems worthwhile to just query the database for the latest versions
# and use their existence as a proxy to whether v2 feature versioning is enabled.
latest_versions = EnvironmentFeatureVersion.objects.get_latest_versions_by_environment_api_key(
environment_api_key=self.kwargs["api_key"]

lookup_field_value = self.kwargs["api_key"]
latest_version_kwargs = {"environment_api_key": lookup_field_value}
if is_valid_uuid(lookup_field_value):
latest_version_kwargs = {"environment_uuid": lookup_field_value}

latest_versions = EnvironmentFeatureVersion.objects.get_latest_versions(
**latest_version_kwargs
)
if latest_versions:
# if there are latest versions (and hence v2 feature versioning is enabled), then
Expand Down
23 changes: 16 additions & 7 deletions api/features/versioning/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ def get_latest_versions_by_environment_id(self, environment_id: int) -> RawQuery
"""
return self._get_latest_versions(environment_id=environment_id)

def get_latest_versions_by_environment_api_key(
self, environment_api_key: str
def get_latest_versions(
self, environment_api_key: str = None, environment_uuid: str = None
) -> RawQuerySet:
"""
Get the latest EnvironmentFeatureVersion objects for a given environment.
"""
return self._get_latest_versions(environment_api_key=environment_api_key)
return self._get_latest_versions(
environment_api_key=environment_api_key, environment_uuid=environment_uuid
)

def get_latest_versions_as_queryset(
self, environment_id: int
Expand All @@ -46,17 +48,24 @@ def get_latest_versions_as_queryset(
)

def _get_latest_versions(
self, environment_id: int = None, environment_api_key: str = None
self,
environment_id: int = None,
environment_api_key: str = None,
environment_uuid: str = None,
) -> RawQuerySet:
assert (environment_id or environment_api_key) and not (
environment_id and environment_api_key
), "Must provide exactly one of environment_id or environment_api_key"
assert (
sum(
bool(x) for x in [environment_id, environment_api_key, environment_uuid]
)
== 1
), "Must provide exactly one of environment_id, environment_api_key, or environment_uuid"

return self.raw(
get_latest_versions_sql,
params={
"environment_id": environment_id,
"api_key": environment_api_key,
"environment_uuid": environment_uuid,
# TODO:
# It seems as though there is a timezone issue when using postgres's
# built in now() function, so we pass in the current time from python.
Expand Down
3 changes: 2 additions & 1 deletion api/features/versioning/sql/get_latest_versions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ inner join
environments_environment e on e.id = efv1.environment_id
where
(%(environment_id)s is not null and efv1.environment_id = %(environment_id)s)
or (%(api_key)s is not null and e.api_key = %(api_key)s);
or (%(api_key)s is not null and e.api_key = %(api_key)s)
or (%(environment_uuid)s is not null and e.uuid = %(environment_uuid)s)
Loading

0 comments on commit 43d1114

Please sign in to comment.