flagsmith-common 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. common/__init__.py +0 -0
  2. common/core/__init__.py +0 -0
  3. common/core/app.py +6 -0
  4. common/core/logging.py +24 -0
  5. common/core/main.py +40 -0
  6. common/core/management/__init__.py +0 -0
  7. common/core/management/commands/__init__.py +0 -0
  8. common/core/management/commands/start.py +41 -0
  9. common/core/metrics.py +23 -0
  10. common/core/urls.py +17 -0
  11. common/core/utils.py +90 -0
  12. common/core/views.py +23 -0
  13. common/environments/permissions.py +15 -0
  14. common/features/__init__.py +0 -0
  15. common/features/multivariate/__init__.py +0 -0
  16. common/features/multivariate/serializers.py +19 -0
  17. common/features/serializers.py +68 -0
  18. common/features/versioning/__init__.py +0 -0
  19. common/features/versioning/serializers.py +13 -0
  20. common/gunicorn/__init__.py +0 -0
  21. common/gunicorn/conf.py +18 -0
  22. common/gunicorn/constants.py +1 -0
  23. common/gunicorn/logging.py +94 -0
  24. common/gunicorn/metrics.py +14 -0
  25. common/gunicorn/middleware.py +28 -0
  26. common/gunicorn/utils.py +61 -0
  27. common/metadata/serializers.py +100 -0
  28. common/organisations/permissions.py +10 -0
  29. common/projects/permissions.py +40 -0
  30. common/prometheus/__init__.py +3 -0
  31. common/prometheus/utils.py +18 -0
  32. common/py.typed +0 -0
  33. common/segments/serializers.py +338 -0
  34. common/test_tools/__init__.py +3 -0
  35. common/test_tools/plugin.py +34 -0
  36. common/test_tools/types.py +11 -0
  37. common/types.py +45 -0
  38. flagsmith_common-1.5.0.dist-info/LICENSE +28 -0
  39. flagsmith_common-1.5.0.dist-info/METADATA +128 -0
  40. flagsmith_common-1.5.0.dist-info/RECORD +42 -0
  41. flagsmith_common-1.5.0.dist-info/WHEEL +4 -0
  42. flagsmith_common-1.5.0.dist-info/entry_points.txt +6 -0
@@ -0,0 +1,100 @@
1
+ import typing
2
+
3
+ from django.apps import apps
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.db import models
6
+ from rest_framework import serializers
7
+
8
+ if typing.TYPE_CHECKING:
9
+ from common.types import (
10
+ Metadata, # noqa: F401
11
+ MetadataModelFieldRequirement,
12
+ Organisation,
13
+ Project,
14
+ )
15
+
16
+
17
+ class MetadataSerializer(serializers.ModelSerializer["Metadata"]):
18
+ class Meta:
19
+ model = apps.get_model("metadata", "Metadata")
20
+ fields = ("id", "model_field", "field_value")
21
+
22
+ def validate(self, data: dict[str, typing.Any]) -> dict[str, typing.Any]:
23
+ data = super().validate(data)
24
+ if not data["model_field"].field.is_field_value_valid(data["field_value"]):
25
+ raise serializers.ValidationError(
26
+ f"Invalid value for field {data['model_field'].field.name}"
27
+ )
28
+
29
+ return data
30
+
31
+
32
+ class SerializerWithMetadata(serializers.Serializer[models.Model]):
33
+ def get_organisation(
34
+ self,
35
+ validated_data: dict[str, typing.Any] | None = None,
36
+ ) -> "Organisation":
37
+ return self.get_project(validated_data).organisation
38
+
39
+ def get_project(
40
+ self,
41
+ validated_data: dict[str, typing.Any] | None = None,
42
+ ) -> "Project":
43
+ raise NotImplementedError()
44
+
45
+ def get_required_for_object(
46
+ self,
47
+ requirement: "MetadataModelFieldRequirement",
48
+ data: dict[str, typing.Any],
49
+ ) -> models.Model:
50
+ model_name = requirement.content_type.model
51
+ try:
52
+ instance: models.Model = getattr(self, f"get_{model_name}")(data)
53
+ except AttributeError:
54
+ raise ValueError(
55
+ f"`get_{model_name}_from_validated_data` method does not exist"
56
+ )
57
+ return instance
58
+
59
+ def validate_required_metadata(
60
+ self,
61
+ data: dict[str, typing.Any],
62
+ ) -> None:
63
+ metadata = data.get("metadata", [])
64
+
65
+ content_type = ContentType.objects.get_for_model(self.Meta.model)
66
+
67
+ organisation = self.get_organisation(data)
68
+
69
+ requirements: models.QuerySet["MetadataModelFieldRequirement"] = apps.get_model(
70
+ "metadata", "MetadataModelFieldRequirement"
71
+ ).objects.filter(
72
+ model_field__content_type=content_type,
73
+ model_field__field__organisation=organisation,
74
+ )
75
+
76
+ for requirement in requirements:
77
+ required_for = self.get_required_for_object(requirement, data)
78
+ if required_for.pk == requirement.object_id:
79
+ if not any(
80
+ [
81
+ field["model_field"] == requirement.model_field
82
+ for field in metadata
83
+ ]
84
+ ):
85
+ raise serializers.ValidationError(
86
+ {
87
+ "metadata": f"Missing required metadata field: {requirement.model_field.field.name}"
88
+ }
89
+ )
90
+
91
+ def validate(
92
+ self,
93
+ data: dict[str, typing.Any],
94
+ ) -> dict[str, typing.Any]:
95
+ data = super().validate(data)
96
+ self.validate_required_metadata(data)
97
+ return data
98
+
99
+ class Meta:
100
+ model: typing.Type[models.Model] = models.Model
@@ -0,0 +1,10 @@
1
+ CREATE_PROJECT = "CREATE_PROJECT"
2
+ MANAGE_USER_GROUPS = "MANAGE_USER_GROUPS"
3
+
4
+ ORGANISATION_PERMISSIONS = (
5
+ (CREATE_PROJECT, "Allows the user to create projects in this organisation."),
6
+ (
7
+ MANAGE_USER_GROUPS,
8
+ "Allows the user to manage the groups in the organisation and their members.",
9
+ ),
10
+ )
@@ -0,0 +1,40 @@
1
+ VIEW_AUDIT_LOG = "VIEW_AUDIT_LOG"
2
+
3
+ # Maintain a list of permissions here
4
+ VIEW_PROJECT = "VIEW_PROJECT"
5
+ CREATE_ENVIRONMENT = "CREATE_ENVIRONMENT"
6
+ DELETE_FEATURE = "DELETE_FEATURE"
7
+ CREATE_FEATURE = "CREATE_FEATURE"
8
+ EDIT_FEATURE = "EDIT_FEATURE"
9
+ MANAGE_SEGMENTS = "MANAGE_SEGMENTS"
10
+ MANAGE_TAGS = "MANAGE_TAGS"
11
+
12
+ # Note that this does not impact change requests in an environment
13
+ MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS = "MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS"
14
+ APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS = "APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS"
15
+ CREATE_PROJECT_LEVEL_CHANGE_REQUESTS = "CREATE_PROJECT_LEVEL_CHANGE_REQUESTS"
16
+
17
+ TAG_SUPPORTED_PERMISSIONS = [DELETE_FEATURE]
18
+
19
+ PROJECT_PERMISSIONS = [
20
+ (VIEW_PROJECT, "View permission for the given project."),
21
+ (CREATE_ENVIRONMENT, "Ability to create an environment in the given project."),
22
+ (DELETE_FEATURE, "Ability to delete features in the given project."),
23
+ (CREATE_FEATURE, "Ability to create features in the given project."),
24
+ (EDIT_FEATURE, "Ability to edit features in the given project."),
25
+ (MANAGE_SEGMENTS, "Ability to manage segments in the given project."),
26
+ (VIEW_AUDIT_LOG, "Allows the user to view the audit logs for this organisation."),
27
+ (
28
+ MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS,
29
+ "Ability to create, delete, and publish change requests associated with a project.",
30
+ ),
31
+ (
32
+ APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS,
33
+ "Ability to approve project level change requests.",
34
+ ),
35
+ (
36
+ CREATE_PROJECT_LEVEL_CHANGE_REQUESTS,
37
+ "Ability to create project level change requests.",
38
+ ),
39
+ (MANAGE_TAGS, "Allows the user to manage tags in the given project."),
40
+ ]
@@ -0,0 +1,3 @@
1
+ from common.prometheus.utils import Histogram
2
+
3
+ __all__ = ("Histogram",)
@@ -0,0 +1,18 @@
1
+ import typing
2
+
3
+ import prometheus_client
4
+ from django.conf import settings
5
+ from prometheus_client.metrics import MetricWrapperBase
6
+ from prometheus_client.multiprocess import MultiProcessCollector
7
+
8
+ T = typing.TypeVar("T", bound=MetricWrapperBase)
9
+
10
+
11
+ class Histogram(prometheus_client.Histogram):
12
+ DEFAULT_BUCKETS = settings.PROMETHEUS_HISTOGRAM_BUCKETS
13
+
14
+
15
+ def get_registry() -> prometheus_client.CollectorRegistry:
16
+ registry = prometheus_client.CollectorRegistry()
17
+ MultiProcessCollector(registry) # type: ignore[no-untyped-call]
18
+ return registry
common/py.typed ADDED
File without changes
@@ -0,0 +1,338 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ from django.apps import apps
5
+ from django.conf import settings
6
+ from django.contrib.contenttypes.models import ContentType
7
+ from django.db import models
8
+ from flag_engine.segments.constants import PERCENTAGE_SPLIT
9
+ from rest_framework import serializers
10
+ from rest_framework.exceptions import ValidationError
11
+ from rest_framework.serializers import ListSerializer
12
+ from rest_framework_recursive.fields import ( # type: ignore[import-untyped]
13
+ RecursiveField,
14
+ )
15
+
16
+ from common.metadata.serializers import (
17
+ MetadataSerializer,
18
+ SerializerWithMetadata,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from common.types import ( # noqa: F401
23
+ Condition,
24
+ Project,
25
+ Rule,
26
+ Segment,
27
+ )
28
+ from common.types import (
29
+ SegmentRule as SegmentRule_,
30
+ )
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class ConditionSerializer(serializers.ModelSerializer["Condition"]):
36
+ delete = serializers.BooleanField(write_only=True, required=False)
37
+ version_of = RecursiveField(required=False, allow_null=True)
38
+
39
+ class Meta:
40
+ model = apps.get_model("segments", "Condition")
41
+ fields = (
42
+ "id",
43
+ "operator",
44
+ "property",
45
+ "value",
46
+ "description",
47
+ "delete",
48
+ "version_of",
49
+ )
50
+
51
+ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
52
+ super(ConditionSerializer, self).validate(attrs)
53
+ if attrs.get("operator") != PERCENTAGE_SPLIT and not attrs.get("property"):
54
+ raise ValidationError({"property": ["This field may not be blank."]})
55
+ return attrs
56
+
57
+ def to_internal_value(self, data: dict[str, Any]) -> Any:
58
+ # convert value to a string - conversion to correct value type is handled elsewhere
59
+ data["value"] = str(data["value"]) if "value" in data else None
60
+ return super(ConditionSerializer, self).to_internal_value(data)
61
+
62
+
63
+ class RuleSerializer(serializers.ModelSerializer["Rule"]):
64
+ delete = serializers.BooleanField(write_only=True, required=False)
65
+ conditions = ConditionSerializer(many=True, required=False)
66
+ rules: ListSerializer["Rule"] = ListSerializer(
67
+ child=RecursiveField(), required=False
68
+ )
69
+ version_of = RecursiveField(required=False, allow_null=True)
70
+
71
+ class Meta:
72
+ model = apps.get_model("segments", "SegmentRule")
73
+ fields = ("id", "type", "rules", "conditions", "delete", "version_of")
74
+
75
+
76
+ class SegmentSerializer(serializers.ModelSerializer["Segment"], SerializerWithMetadata):
77
+ rules = RuleSerializer(many=True)
78
+ metadata = MetadataSerializer(required=False, many=True)
79
+
80
+ class Meta:
81
+ model = apps.get_model("segments", "Segment")
82
+ fields = "__all__"
83
+
84
+ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
85
+ attrs = super().validate(attrs)
86
+ self.validate_required_metadata(attrs)
87
+ if not attrs.get("rules"):
88
+ raise ValidationError(
89
+ {"rules": "Segment cannot be created without any rules."}
90
+ )
91
+ return attrs
92
+
93
+ def get_project(
94
+ self,
95
+ validated_data: dict[str, Any] | None = None,
96
+ ) -> "Project":
97
+ project: "Project"
98
+ if validated_data and "project" in validated_data:
99
+ project = validated_data["project"]
100
+ return project
101
+ project = apps.get_model("projects", "Project").objects.get(
102
+ id=self.context["view"].kwargs["project_pk"]
103
+ )
104
+ return project
105
+
106
+ def create(self, validated_data: dict[str, Any]) -> "Segment":
107
+ project = validated_data["project"]
108
+ self.validate_project_segment_limit(project)
109
+
110
+ rules_data = validated_data.pop("rules", [])
111
+ metadata_data = validated_data.pop("metadata", [])
112
+ self.validate_segment_rules_conditions_limit(rules_data)
113
+
114
+ # create segment with nested rules and conditions
115
+ segment: "Segment" = apps.get_model("segments", "Segment").objects.create(
116
+ **validated_data
117
+ )
118
+ self._update_or_create_segment_rules(
119
+ rules_data, segment=segment, is_create=True
120
+ )
121
+ self._update_or_create_metadata(metadata_data, segment=segment)
122
+ return segment
123
+
124
+ def update(
125
+ self,
126
+ instance: "Segment",
127
+ validated_data: dict[str, Any],
128
+ ) -> "Segment":
129
+ # use the initial data since we need the ids included to determine which to update & which to create
130
+ rules_data = self.initial_data.pop("rules", [])
131
+ metadata_data = validated_data.pop("metadata", [])
132
+ self.validate_segment_rules_conditions_limit(rules_data)
133
+
134
+ # Create a version of the segment now that we're updating.
135
+ cloned_segment = instance.deep_clone()
136
+ logger.info(
137
+ f"Updating cloned segment {cloned_segment.id} for original segment {instance.id}"
138
+ )
139
+
140
+ try:
141
+ self._update_segment_rules(rules_data, segment=instance)
142
+ self._update_or_create_metadata(metadata_data, segment=instance)
143
+
144
+ # remove rules from validated data to prevent error trying to create segment with nested rules
145
+ del validated_data["rules"]
146
+ response = super().update(instance, validated_data)
147
+ except Exception:
148
+ # Since there was a problem during the update we now delete the cloned segment,
149
+ # since we no longer need a versioned segment.
150
+ instance.refresh_from_db()
151
+ instance.version = cloned_segment.version
152
+ instance.save()
153
+ cloned_segment.hard_delete()
154
+ raise
155
+
156
+ return response
157
+
158
+ def validate_project_segment_limit(self, project: "Project") -> None:
159
+ if (
160
+ apps.get_model("segments", "Segment")
161
+ .live_objects.filter(project=project)
162
+ .count()
163
+ >= project.max_segments_allowed
164
+ ):
165
+ raise ValidationError(
166
+ {
167
+ "project": "The project has reached the maximum allowed segments limit."
168
+ }
169
+ )
170
+
171
+ def validate_segment_rules_conditions_limit(
172
+ self, rules_data: list[dict[str, Any]]
173
+ ) -> None:
174
+ if self.instance and getattr(self.instance, "whitelisted_segment", None):
175
+ return
176
+
177
+ count = self._calculate_condition_count(rules_data)
178
+
179
+ if count > settings.SEGMENT_RULES_CONDITIONS_LIMIT:
180
+ raise ValidationError(
181
+ {
182
+ "segment": f"The segment has {count} conditions, which exceeds the maximum "
183
+ f"condition count of {settings.SEGMENT_RULES_CONDITIONS_LIMIT}."
184
+ }
185
+ )
186
+
187
+ def _calculate_condition_count(
188
+ self,
189
+ rules_data: list[dict[str, Any]],
190
+ ) -> int:
191
+ count: int = 0
192
+
193
+ for rule_data in rules_data:
194
+ child_rules: list[dict[str, Any]] = rule_data.get("rules", [])
195
+ if child_rules:
196
+ count += self._calculate_condition_count(child_rules)
197
+ conditions: list[dict[str, Any]] = rule_data.get("conditions", [])
198
+ for condition in conditions:
199
+ if condition.get("delete", False) is True:
200
+ continue
201
+ count += 1
202
+ return count
203
+
204
+ def _update_segment_rules(
205
+ self,
206
+ rules_data: list[dict[str, Any]],
207
+ segment: "Segment | None" = None,
208
+ ) -> None:
209
+ """
210
+ Since we don't have a unique identifier for the rules / conditions for the update, we assume that the client
211
+ passes up the new configuration for the rules of the segment and simply wipe the old ones and create new ones
212
+ """
213
+ Segment = apps.get_model("segments", "Segment")
214
+
215
+ # traverse the rules / conditions tree - if no ids are provided, then maintain the previous behaviour (clear
216
+ # existing rules and create the ones that were sent)
217
+ # note: we do this to preserve backwards compatibility after adding logic to include the id in requests
218
+ if not Segment.id_exists_in_rules_data(rules_data):
219
+ assert segment
220
+ segment.rules.set([])
221
+
222
+ self._update_or_create_segment_rules(rules_data, segment=segment)
223
+
224
+ def _update_or_create_segment_rules(
225
+ self,
226
+ rules_data: list[dict[str, Any]],
227
+ segment: "Segment | None" = None,
228
+ rule: "Rule | None" = None,
229
+ is_create: bool = False,
230
+ ) -> None:
231
+ if all(x is None for x in {segment, rule}):
232
+ raise RuntimeError("Can't create rule without parent segment or rule")
233
+
234
+ for rule_data in rules_data:
235
+ child_rules = rule_data.pop("rules", [])
236
+ conditions = rule_data.pop("conditions", [])
237
+
238
+ child_rule = self._update_or_create_segment_rule(
239
+ rule_data, segment=segment, rule=rule
240
+ )
241
+ if not child_rule:
242
+ # child rule was deleted
243
+ continue
244
+
245
+ self._update_or_create_conditions(
246
+ conditions, child_rule, is_create=is_create, segment=segment
247
+ )
248
+
249
+ self._update_or_create_segment_rules(
250
+ child_rules, rule=child_rule, is_create=is_create
251
+ )
252
+
253
+ def _update_or_create_metadata(
254
+ self,
255
+ metadata_data: list[dict[str, Any]],
256
+ segment: "Segment | None" = None,
257
+ ) -> None:
258
+ Metadata = apps.get_model("metadata", "Metadata")
259
+ Segment = apps.get_model("segments", "Segment")
260
+ assert segment
261
+ if len(metadata_data) == 0:
262
+ Metadata.objects.filter(object_id=segment.id).delete()
263
+ return
264
+ if metadata_data is not None:
265
+ for metadata_item in metadata_data:
266
+ metadata_model_field = metadata_item.pop("model_field", None)
267
+ if metadata_item.get("delete"):
268
+ Metadata.objects.filter(model_field=metadata_model_field).delete()
269
+ continue
270
+
271
+ Metadata.objects.update_or_create(
272
+ model_field=metadata_model_field,
273
+ defaults={
274
+ **metadata_item,
275
+ "content_type": ContentType.objects.get_for_model(Segment),
276
+ "object_id": segment.id,
277
+ },
278
+ )
279
+
280
+ @staticmethod
281
+ def _update_or_create_segment_rule(
282
+ rule_data: dict[str, Any],
283
+ segment: "Segment | None" = None,
284
+ rule: "Rule | None" = None,
285
+ ) -> "SegmentRule_ | None":
286
+ SegmentRule = apps.get_model("segments", "SegmentRule")
287
+ rule_id = rule_data.pop("id", None)
288
+ if rule_id is not None:
289
+ segment_rule: "SegmentRule_" = SegmentRule.objects.get(id=rule_id)
290
+ assert rule
291
+ matching_segment = segment or rule.get_segment()
292
+
293
+ if segment_rule.get_segment() != matching_segment:
294
+ raise ValidationError({"segment": "Mismatched segment is not allowed"})
295
+
296
+ if rule_data.get("delete"):
297
+ SegmentRule.objects.filter(id=rule_id).delete()
298
+ return None
299
+
300
+ segment_rule, _ = SegmentRule.objects.update_or_create(
301
+ id=rule_id, defaults={"segment": segment, "rule": rule, **rule_data}
302
+ )
303
+ return segment_rule
304
+
305
+ @staticmethod
306
+ def _update_or_create_conditions(
307
+ conditions_data: list[dict[str, Any]],
308
+ rule: "Rule",
309
+ segment: models.Model | None = None,
310
+ is_create: bool = False,
311
+ ) -> None:
312
+ Condition = apps.get_model("segments", "Condition")
313
+ for condition_data in conditions_data:
314
+ condition_id = condition_data.pop("id", None)
315
+ if condition_id is not None:
316
+ condition = Condition.objects.filter(id=condition_id).first()
317
+ if condition is None:
318
+ raise ValidationError(
319
+ {"condition": "Condition can't be found and is likely deleted"}
320
+ )
321
+ matching_segment = segment or rule.get_segment()
322
+ if condition._get_segment() != matching_segment:
323
+ raise ValidationError(
324
+ {"segment": "Mismatched segment is not allowed"}
325
+ )
326
+
327
+ if condition_data.get("delete"):
328
+ Condition.objects.filter(id=condition_id).delete()
329
+ continue
330
+
331
+ Condition.objects.update_or_create(
332
+ id=condition_id,
333
+ defaults={
334
+ **condition_data,
335
+ "created_with_segment": is_create,
336
+ "rule": rule,
337
+ },
338
+ )
@@ -0,0 +1,3 @@
1
+ from common.test_tools.types import AssertMetricFixture
2
+
3
+ __all__ = ("AssertMetricFixture",)
@@ -0,0 +1,34 @@
1
+ from typing import Generator
2
+
3
+ import prometheus_client
4
+ import pytest
5
+ from prometheus_client.metrics import MetricWrapperBase
6
+
7
+ from common.test_tools.types import AssertMetricFixture
8
+
9
+
10
+ def assert_metric_impl() -> Generator[AssertMetricFixture, None, None]:
11
+ registry = prometheus_client.REGISTRY
12
+ collectors = [*registry._collector_to_names]
13
+
14
+ def _assert_metric(
15
+ *,
16
+ name: str,
17
+ labels: dict[str, str],
18
+ value: float | int,
19
+ ) -> None:
20
+ metric_value = registry.get_sample_value(name, labels)
21
+ assert metric_value == value, (
22
+ f"Metric {name} not found in registry:\n"
23
+ f"{prometheus_client.generate_latest(registry).decode()}"
24
+ )
25
+
26
+ yield _assert_metric
27
+
28
+ # Reset registry state
29
+ for collector in collectors:
30
+ if isinstance(collector, MetricWrapperBase):
31
+ collector.clear()
32
+
33
+
34
+ assert_metric = pytest.fixture(assert_metric_impl)
@@ -0,0 +1,11 @@
1
+ from typing import Protocol
2
+
3
+
4
+ class AssertMetricFixture(Protocol):
5
+ def __call__(
6
+ self,
7
+ *,
8
+ name: str,
9
+ labels: dict[str, str],
10
+ value: float | int,
11
+ ) -> None: ...
common/types.py ADDED
@@ -0,0 +1,45 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.db import models
6
+
7
+ FeatureStateValue: typing.TypeAlias = models.Model
8
+ FeatureSegment: typing.TypeAlias = models.Model
9
+ Condition: typing.TypeAlias = models.Model
10
+ MultivariateFeatureStateValue: typing.TypeAlias = models.Model
11
+ Metadata: typing.TypeAlias = models.Model
12
+ Organisation: typing.TypeAlias = models.Model
13
+
14
+ class SoftDeleteExportableModel(models.Model):
15
+ def hard_delete(self) -> None: ...
16
+
17
+ class Segment(SoftDeleteExportableModel):
18
+ id: int
19
+ version: int | None
20
+ rules = models.ForeignKey("Rule", on_delete=models.CASCADE)
21
+
22
+ def deep_clone(self) -> "Segment": ...
23
+
24
+ class Rule(models.Model):
25
+ def get_segment(self) -> Segment: ...
26
+
27
+ class SegmentRule(Rule):
28
+ pass
29
+
30
+ class Project(models.Model):
31
+ organisation: "Organisation"
32
+ max_segments_allowed: int
33
+
34
+ class MetadataField(models.Model):
35
+ name: str
36
+ organisation: "Organisation"
37
+
38
+ class MetadataModelField(models.Model):
39
+ field: "MetadataField"
40
+ content_type: ContentType
41
+
42
+ class MetadataModelFieldRequirement(models.Model):
43
+ model_field: "MetadataModelField"
44
+ object_id: int
45
+ content_type: ContentType
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Flagsmith
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.