Skip to content

Commit b10a2c3

Browse files
feat: experimentation flagsmith warehouse setup api scaffolding (#7542)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 03f05ee commit b10a2c3

20 files changed

Lines changed: 816 additions & 1 deletion

File tree

api/app/settings/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
"softdelete",
168168
"metadata",
169169
"app_analytics",
170+
"experimentation",
170171
"oauth2_metadata",
171172
]
172173

api/audit/related_object_type.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ class RelatedObjectType(enum.Enum):
1212
EF_VERSION = "Environment feature version"
1313
FEATURE_HEALTH = "Feature health status"
1414
RELEASE_PIPELINE = "Release pipeline"
15+
WAREHOUSE_CONNECTION = "Warehouse connection"

api/environments/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,8 @@
173173
get_experiment_results,
174174
name="experiment-results",
175175
),
176+
path(
177+
"<str:environment_api_key>/warehouse-connections/",
178+
include("experimentation.urls"),
179+
),
176180
]

api/experimentation/__init__.py

Whitespace-only changes.

api/experimentation/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ExperimentationConfig(AppConfig):
5+
name = "experimentation"

api/experimentation/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
WAREHOUSE_CONNECTION_FLAG = "experimentation_warehouse_connection"
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Generated by Django 5.2.14 on 2026-05-19 11:05
2+
3+
import django.db.models.deletion
4+
import django_lifecycle.mixins # type: ignore[import-untyped]
5+
import uuid
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
("environments", "0037_add_uuid_field"),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name="WarehouseConnection",
20+
fields=[
21+
(
22+
"id",
23+
models.AutoField(
24+
auto_created=True,
25+
primary_key=True,
26+
serialize=False,
27+
verbose_name="ID",
28+
),
29+
),
30+
(
31+
"deleted_at",
32+
models.DateTimeField(
33+
blank=True,
34+
db_index=True,
35+
default=None,
36+
editable=False,
37+
null=True,
38+
),
39+
),
40+
(
41+
"uuid",
42+
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
43+
),
44+
(
45+
"warehouse_type",
46+
models.CharField(
47+
choices=[
48+
("flagsmith", "Flagsmith"),
49+
("snowflake", "Snowflake"),
50+
("clickhouse", "ClickHouse"),
51+
],
52+
max_length=50,
53+
),
54+
),
55+
(
56+
"status",
57+
models.CharField(
58+
choices=[
59+
("pending_connection", "Pending Connection"),
60+
("connected", "Connected"),
61+
("errored", "Errored"),
62+
],
63+
default="pending_connection",
64+
max_length=50,
65+
),
66+
),
67+
("name", models.CharField(max_length=255)),
68+
("created_at", models.DateTimeField(auto_now_add=True)),
69+
(
70+
"environment",
71+
models.ForeignKey(
72+
on_delete=django.db.models.deletion.CASCADE,
73+
related_name="warehouse_connections",
74+
to="environments.environment",
75+
),
76+
),
77+
],
78+
options={
79+
"constraints": [
80+
models.UniqueConstraint(
81+
condition=models.Q(("deleted_at__isnull", True)),
82+
fields=("warehouse_type", "environment"),
83+
name="unique_active_warehouse_per_type_and_env",
84+
)
85+
],
86+
},
87+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
88+
),
89+
]

api/experimentation/migrations/__init__.py

Whitespace-only changes.

api/experimentation/models.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from django.db import models
2+
from django_lifecycle import LifecycleModelMixin # type: ignore[import-untyped]
3+
4+
from core.models import SoftDeleteExportableModel
5+
from environments.models import Environment
6+
7+
8+
class WarehouseType(models.TextChoices):
9+
FLAGSMITH = "flagsmith", "Flagsmith"
10+
SNOWFLAKE = "snowflake", "Snowflake"
11+
CLICKHOUSE = "clickhouse", "ClickHouse"
12+
13+
14+
class WarehouseConnectionStatus(models.TextChoices):
15+
PENDING_CONNECTION = "pending_connection", "Pending Connection"
16+
CONNECTED = "connected", "Connected"
17+
ERRORED = "errored", "Errored"
18+
19+
20+
class WarehouseConnection(LifecycleModelMixin, SoftDeleteExportableModel): # type: ignore[misc]
21+
environment = models.ForeignKey(
22+
Environment,
23+
on_delete=models.CASCADE,
24+
related_name="warehouse_connections",
25+
)
26+
warehouse_type = models.CharField(
27+
max_length=50,
28+
choices=WarehouseType.choices,
29+
)
30+
status = models.CharField(
31+
max_length=50,
32+
choices=WarehouseConnectionStatus.choices,
33+
default=WarehouseConnectionStatus.PENDING_CONNECTION,
34+
)
35+
name = models.CharField(max_length=255)
36+
created_at = models.DateTimeField(auto_now_add=True)
37+
38+
class Meta:
39+
constraints = [
40+
models.UniqueConstraint(
41+
fields=["warehouse_type", "environment"],
42+
condition=models.Q(deleted_at__isnull=True),
43+
name="unique_active_warehouse_per_type_and_env",
44+
),
45+
]

api/experimentation/permissions.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from rest_framework.permissions import BasePermission
2+
from rest_framework.request import Request
3+
from rest_framework.views import APIView
4+
5+
from environments.models import Environment
6+
from experimentation.services import is_warehouse_feature_enabled
7+
from users.models import FFAdminUser
8+
9+
10+
class WarehouseConnectionPermission(BasePermission):
11+
def has_permission(self, request: Request, view: APIView) -> bool:
12+
try:
13+
environment = Environment.objects.get(
14+
api_key=view.kwargs.get("environment_api_key")
15+
)
16+
except Environment.DoesNotExist:
17+
return False
18+
19+
if not is_warehouse_feature_enabled(environment.project.organisation):
20+
return False
21+
22+
user: FFAdminUser = request.user # type: ignore[assignment]
23+
return user.is_environment_admin(environment)

0 commit comments

Comments
 (0)