flagsmith-common 3.0.0__tar.gz → 3.1.0__tar.gz

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 (99) hide show
  1. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/PKG-INFO +3 -1
  2. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/pyproject.toml +6 -4
  3. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/test_tools/plugin.py +12 -10
  4. flagsmith_common-3.1.0/src/flagsmith_schemas/constants.py +4 -0
  5. flagsmith_common-3.1.0/src/flagsmith_schemas/dynamodb.py +346 -0
  6. flagsmith_common-3.1.0/src/flagsmith_schemas/pydantic_types.py +22 -0
  7. flagsmith_common-3.1.0/src/flagsmith_schemas/types.py +91 -0
  8. flagsmith_common-3.1.0/src/flagsmith_schemas/validators.py +55 -0
  9. flagsmith_common-3.1.0/src/task_processor/migrations/sql/__init__.py +0 -0
  10. flagsmith_common-3.1.0/src/task_processor/py.typed +0 -0
  11. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/LICENSE +0 -0
  12. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/README.md +0 -0
  13. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/__init__.py +0 -0
  14. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/__init__.py +0 -0
  15. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/app.py +0 -0
  16. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/cli/__init__.py +0 -0
  17. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/cli/healthcheck.py +0 -0
  18. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/constants.py +0 -0
  19. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/logging.py +0 -0
  20. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/main.py +0 -0
  21. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/management/__init__.py +0 -0
  22. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/management/commands/__init__.py +0 -0
  23. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/management/commands/docgen.py +0 -0
  24. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/management/commands/start.py +0 -0
  25. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/management/commands/waitfordb.py +0 -0
  26. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/metrics.py +0 -0
  27. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/middleware.py +0 -0
  28. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/templates/docgen-metrics.md +0 -0
  29. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/urls.py +0 -0
  30. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/utils.py +0 -0
  31. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/core/views.py +0 -0
  32. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/environments/permissions.py +0 -0
  33. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/features/__init__.py +0 -0
  34. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/features/multivariate/__init__.py +0 -0
  35. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/features/multivariate/serializers.py +0 -0
  36. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/features/serializers.py +0 -0
  37. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/features/versioning/__init__.py +0 -0
  38. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/features/versioning/serializers.py +0 -0
  39. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/gunicorn/__init__.py +0 -0
  40. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/gunicorn/conf.py +0 -0
  41. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/gunicorn/constants.py +0 -0
  42. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/gunicorn/logging.py +0 -0
  43. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/gunicorn/metrics.py +0 -0
  44. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/gunicorn/middleware.py +0 -0
  45. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/gunicorn/utils.py +0 -0
  46. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/migrations/__init__.py +0 -0
  47. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/migrations/helpers/__init__.py +0 -0
  48. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/migrations/helpers/postgres_helpers.py +0 -0
  49. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/organisations/permissions.py +0 -0
  50. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/projects/permissions.py +0 -0
  51. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/prometheus/__init__.py +0 -0
  52. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/prometheus/utils.py +0 -0
  53. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/py.typed +0 -0
  54. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/test_tools/__init__.py +0 -0
  55. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/test_tools/types.py +0 -0
  56. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/test_tools/utils.py +0 -0
  57. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/common/types.py +0 -0
  58. {flagsmith_common-3.0.0/src/task_processor → flagsmith_common-3.1.0/src/flagsmith_schemas}/__init__.py +0 -0
  59. {flagsmith_common-3.0.0/src/task_processor → flagsmith_common-3.1.0/src/flagsmith_schemas}/py.typed +0 -0
  60. {flagsmith_common-3.0.0/src/task_processor/migrations → flagsmith_common-3.1.0/src/task_processor}/__init__.py +0 -0
  61. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/admin.py +0 -0
  62. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/apps.py +0 -0
  63. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/decorators.py +0 -0
  64. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/exceptions.py +0 -0
  65. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/health.py +0 -0
  66. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/managers.py +0 -0
  67. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/metrics.py +0 -0
  68. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0001_initial.py +0 -0
  69. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0002_healthcheckmodel.py +0 -0
  70. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0003_add_completed_to_task.py +0 -0
  71. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0004_recreate_task_indexes.py +0 -0
  72. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0005_update_conditional_index_conditions.py +0 -0
  73. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0006_auto_20230221_0802.py +0 -0
  74. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0007_add_is_locked.py +0 -0
  75. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0008_add_get_task_to_process_function.py +0 -0
  76. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +0 -0
  77. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0010_task_priority.py +0 -0
  78. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +0 -0
  79. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +0 -0
  80. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/0013_add_last_picked_at.py +0 -0
  81. {flagsmith_common-3.0.0/src/task_processor/migrations/sql → flagsmith_common-3.1.0/src/task_processor/migrations}/__init__.py +0 -0
  82. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +0 -0
  83. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +0 -0
  84. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +0 -0
  85. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +0 -0
  86. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +0 -0
  87. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/models.py +0 -0
  88. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/monitoring.py +0 -0
  89. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/processor.py +0 -0
  90. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/routers.py +0 -0
  91. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/serializers.py +0 -0
  92. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/task_registry.py +0 -0
  93. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/task_run_method.py +0 -0
  94. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/tasks.py +0 -0
  95. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/threads.py +0 -0
  96. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/types.py +0 -0
  97. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/urls.py +0 -0
  98. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/utils.py +0 -0
  99. {flagsmith_common-3.0.0 → flagsmith_common-3.1.0}/src/task_processor/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flagsmith-common
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: Flagsmith's common library
5
5
  Author: Matthew Elwell, Gagan Trivedi, Kim Gustyr, Zach Aysan, Francesco Lo Franco, Rodrigo López Dato, Evandro Myller, Wadii Zaim
6
6
  License-Expression: BSD-3-Clause
@@ -21,6 +21,7 @@ Requires-Dist: prometheus-client>=0.0.16 ; extra == 'common-core'
21
21
  Requires-Dist: psycopg2-binary>=2.9,<3 ; extra == 'common-core'
22
22
  Requires-Dist: requests ; extra == 'common-core'
23
23
  Requires-Dist: simplejson>=3,<4 ; extra == 'common-core'
24
+ Requires-Dist: typing-extensions ; extra == 'flagsmith-schemas'
24
25
  Requires-Dist: backoff>=2.2.1,<3.0.0 ; extra == 'task-processor'
25
26
  Requires-Dist: django>4,<6 ; extra == 'task-processor'
26
27
  Requires-Dist: django-health-check ; extra == 'task-processor'
@@ -36,6 +37,7 @@ Project-URL: Homepage, https://flagsmith.com
36
37
  Project-URL: Issues, https://github.com/flagsmith/flagsmith-common/issues
37
38
  Project-URL: Repository, https://github.com/flagsmith/flagsmith-common
38
39
  Provides-Extra: common-core
40
+ Provides-Extra: flagsmith-schemas
39
41
  Provides-Extra: task-processor
40
42
  Provides-Extra: test-tools
41
43
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flagsmith-common"
3
- version = "3.0.0"
3
+ version = "3.1.0"
4
4
  description = "Flagsmith's common library"
5
5
  requires-python = ">=3.11,<4.0"
6
6
  dependencies = []
@@ -20,12 +20,13 @@ optional-dependencies = { test-tools = [
20
20
  "psycopg2-binary (>=2.9,<3)",
21
21
  "requests",
22
22
  "simplejson (>=3,<4)",
23
-
24
23
  ], task-processor = [
25
24
  "backoff (>=2.2.1,<3.0.0)",
26
25
  "django (>4,<6)",
27
26
  "django-health-check",
28
27
  "prometheus-client (>=0.0.16)",
28
+ ], flagsmith-schemas = [
29
+ "typing_extensions",
29
30
  ] }
30
31
  authors = [
31
32
  { name = "Matthew Elwell" },
@@ -68,6 +69,7 @@ dev = [
68
69
  "djangorestframework-stubs (>=3.15.3, <4.0.0)",
69
70
  "mypy (>=1.15.0, <2.0.0)",
70
71
  "pre-commit",
72
+ "pydantic>=2.12.5",
71
73
  "pyfakefs (>=5.7.4, <6.0.0)",
72
74
  "pytest (>=8.3.4, <9.0.0)",
73
75
  "pytest-asyncio (>=0.25.3, <1.0.0)",
@@ -77,8 +79,8 @@ dev = [
77
79
  "pytest-httpserver (>=1.1.3, <2.0.0)",
78
80
  "pytest-mock (>=3.14.0, <4.0.0)",
79
81
  "setuptools (>=78.1.1, <79.0.0)",
80
- "types-simplejson (>=3.20.0.20250326, <4.0.0)",
81
82
  "types-python-dateutil (>=2.9.0.20250516, <3.0.0)",
83
+ "types-simplejson (>=3.20.0.20250326, <4.0.0)",
82
84
  ]
83
85
 
84
86
  [build-system]
@@ -86,7 +88,7 @@ requires = ["uv_build>=0.9.17,<0.10.0"]
86
88
  build-backend = "uv_build"
87
89
 
88
90
  [tool.uv.build-backend]
89
- module-name = ["common", "task_processor"]
91
+ module-name = ["common", "task_processor", "flagsmith_schemas"]
90
92
 
91
93
  [tool.coverage.report]
92
94
  # Regexes for lines to exclude from consideration
@@ -1,11 +1,7 @@
1
1
  from functools import partial
2
- from typing import Generator
2
+ from typing import TYPE_CHECKING, Generator
3
3
 
4
- import prometheus_client
5
4
  import pytest
6
- from prometheus_client.metrics import MetricWrapperBase
7
- from pyfakefs.fake_filesystem import FakeFilesystem
8
- from pytest_django.fixtures import SettingsWrapper
9
5
 
10
6
  from common.test_tools.types import (
11
7
  AssertMetricFixture,
@@ -15,6 +11,10 @@ from common.test_tools.types import (
15
11
  )
16
12
  from task_processor.task_run_method import TaskRunMethod
17
13
 
14
+ if TYPE_CHECKING:
15
+ from pyfakefs.fake_filesystem import FakeFilesystem
16
+ from pytest_django.fixtures import SettingsWrapper
17
+
18
18
 
19
19
  def pytest_addoption(parser: pytest.Parser) -> None:
20
20
  group = parser.getgroup("snapshot")
@@ -26,12 +26,14 @@ def pytest_addoption(parser: pytest.Parser) -> None:
26
26
 
27
27
 
28
28
  def assert_metric_impl() -> Generator[AssertMetricFixture, None, None]:
29
+ import prometheus_client
30
+
29
31
  registry = prometheus_client.REGISTRY
30
32
  collectors = [*registry._collector_to_names]
31
33
 
32
34
  # Reset registry state
33
35
  for collector in collectors:
34
- if isinstance(collector, MetricWrapperBase):
36
+ if isinstance(collector, prometheus_client.metrics.MetricWrapperBase):
35
37
  collector.clear()
36
38
 
37
39
  def _assert_metric(
@@ -53,7 +55,7 @@ assert_metric = pytest.fixture(assert_metric_impl)
53
55
 
54
56
 
55
57
  @pytest.fixture()
56
- def saas_mode(fs: FakeFilesystem) -> Generator[None, None, None]:
58
+ def saas_mode(fs: "FakeFilesystem") -> Generator[None, None, None]:
57
59
  from common.core.utils import is_saas
58
60
 
59
61
  is_saas.cache_clear()
@@ -65,7 +67,7 @@ def saas_mode(fs: FakeFilesystem) -> Generator[None, None, None]:
65
67
 
66
68
 
67
69
  @pytest.fixture()
68
- def enterprise_mode(fs: FakeFilesystem) -> Generator[None, None, None]:
70
+ def enterprise_mode(fs: "FakeFilesystem") -> Generator[None, None, None]:
69
71
  from common.core.utils import is_enterprise
70
72
 
71
73
  is_enterprise.cache_clear()
@@ -77,7 +79,7 @@ def enterprise_mode(fs: FakeFilesystem) -> Generator[None, None, None]:
77
79
 
78
80
 
79
81
  @pytest.fixture()
80
- def task_processor_mode(settings: SettingsWrapper) -> None:
82
+ def task_processor_mode(settings: "SettingsWrapper") -> None:
81
83
  settings.TASK_PROCESSOR_MODE = True
82
84
  # The setting is supposed to be set before the metrics module is imported,
83
85
  # so reload it
@@ -101,7 +103,7 @@ def flagsmith_markers_marked(
101
103
 
102
104
  @pytest.fixture(name="run_tasks")
103
105
  def run_tasks_impl(
104
- settings: SettingsWrapper,
106
+ settings: "SettingsWrapper",
105
107
  transactional_db: None,
106
108
  task_processor_mode: None,
107
109
  ) -> RunTasksFixture:
@@ -0,0 +1,4 @@
1
+ from importlib.util import find_spec
2
+
3
+ PYDANTIC_INSTALLED = find_spec("pydantic") is not None
4
+ MAX_STRING_FEATURE_STATE_VALUE_LENGTH = 20_000
@@ -0,0 +1,346 @@
1
+ """
2
+ The types in this module describe the Edge API's data model.
3
+ They are used to type DynamoDB documents representing Flagsmith entities.
4
+
5
+ These types can be used with Pydantic for validation and serialization
6
+ when `pydantic` is installed.
7
+ Otherwise, they serve as documentation for the structure of the data stored in DynamoDB.
8
+ """
9
+
10
+ from typing import Annotated, Literal
11
+
12
+ from typing_extensions import NotRequired, TypedDict
13
+
14
+ from flagsmith_schemas.constants import PYDANTIC_INSTALLED
15
+ from flagsmith_schemas.types import (
16
+ ConditionOperator,
17
+ DateTimeStr,
18
+ DynamoContextValue,
19
+ DynamoFeatureValue,
20
+ DynamoFloat,
21
+ DynamoInt,
22
+ FeatureType,
23
+ RuleType,
24
+ UUIDStr,
25
+ )
26
+
27
+ if PYDANTIC_INSTALLED:
28
+ from flagsmith_schemas.pydantic_types import (
29
+ ValidateIdentityFeatureStatesList,
30
+ ValidateMultivariateFeatureValuesList,
31
+ )
32
+
33
+
34
+ class Feature(TypedDict):
35
+ """Represents a Flagsmith feature, defined at project level."""
36
+
37
+ id: DynamoInt
38
+ """Unique identifier for the feature in Core."""
39
+ name: str
40
+ """Name of the feature. Must be unique within a project."""
41
+ type: FeatureType
42
+
43
+
44
+ class MultivariateFeatureOption(TypedDict):
45
+ """Represents a single multivariate feature option in the Flagsmith UI."""
46
+
47
+ id: NotRequired[DynamoInt | None]
48
+ """Unique identifier for the multivariate feature option in Core. This is used by Core UI to display the selected option for an identity override for a multivariate feature."""
49
+ value: DynamoFeatureValue
50
+ """The feature state value that should be served when this option's parent multivariate feature state is selected by the engine."""
51
+
52
+
53
+ class MultivariateFeatureStateValue(TypedDict):
54
+ """Represents a multivariate feature state value.
55
+
56
+ Identity overrides are meant to hold only one of these, solely to inform the UI which option is selected for the given identity.
57
+ """
58
+
59
+ id: NotRequired[DynamoInt | None]
60
+ """Unique identifier for the multivariate feature state value in Core. Used for multivariate bucketing. If feature state created via `edge-identities` APIs in Core, this can be missing or `None`."""
61
+ mv_fs_value_uuid: NotRequired[UUIDStr]
62
+ """The UUID for this multivariate feature state value. Should be used for multivariate bucketing if `id` is `None`."""
63
+ percentage_allocation: DynamoFloat
64
+ """The percentage allocation for this multivariate feature state value. Should be between or equal to 0 and 100."""
65
+ multivariate_feature_option: MultivariateFeatureOption
66
+ """The multivariate feature option that this value corresponds to."""
67
+
68
+
69
+ class FeatureSegment(TypedDict):
70
+ """Represents data specific to a segment feature override."""
71
+
72
+ priority: NotRequired[DynamoInt | None]
73
+ """The priority of this segment feature override. Lower numbers indicate stronger priority. If `None` or not set, the weakest priority is assumed."""
74
+
75
+
76
+ class FeatureState(TypedDict):
77
+ """Used to define the state of a feature for an environment, segment overrides, and identity overrides."""
78
+
79
+ feature: Feature
80
+ """The feature that this feature state is for."""
81
+ enabled: bool
82
+ """Whether the feature is enabled or disabled."""
83
+ feature_state_value: DynamoFeatureValue
84
+ """The value for this feature state."""
85
+ django_id: NotRequired[DynamoInt | None]
86
+ """Unique identifier for the feature state in Core. If feature state created via Core's `edge-identities` APIs in Core, this can be missing or `None`."""
87
+ featurestate_uuid: NotRequired[UUIDStr]
88
+ """The UUID for this feature state. Should be used if `django_id` is `None`. If not set, should be generated."""
89
+ feature_segment: NotRequired[FeatureSegment | None]
90
+ """Segment override data, if this feature state is for a segment override."""
91
+ multivariate_feature_state_values: "NotRequired[Annotated[list[MultivariateFeatureStateValue], ValidateMultivariateFeatureValuesList]]"
92
+ """List of multivariate feature state values, if this feature state is for a multivariate feature.
93
+
94
+ Total `percentage_allocation` sum of the child multivariate feature state values must be less or equal to 100.
95
+ """
96
+
97
+
98
+ class Trait(TypedDict):
99
+ """Represents a key-value pair associated with an identity."""
100
+
101
+ trait_key: str
102
+ """Key of the trait."""
103
+ trait_value: DynamoContextValue
104
+ """Value of the trait."""
105
+
106
+
107
+ class SegmentCondition(TypedDict):
108
+ """Represents a condition within a segment rule used by Flagsmith engine."""
109
+
110
+ operator: ConditionOperator
111
+ """Operator to be applied for this condition."""
112
+ value: NotRequired[str | None]
113
+ """Value to be compared against in this condition. May be `None` for `IS_SET` and `IS_NOT_SET` operators."""
114
+ property_: NotRequired[str | None]
115
+ """The property (context key) this condition applies to. May be `None` for the `PERCENTAGE_SPLIT` operator.
116
+
117
+ Named `property_` to avoid conflict with Python's `property` built-in.
118
+ """
119
+
120
+
121
+ class SegmentRule(TypedDict):
122
+ """Represents a rule within a segment used by Flagsmith engine."""
123
+
124
+ type: RuleType
125
+ """Type of the rule, defining how conditions are evaluated."""
126
+ rules: "list[SegmentRule]"
127
+ """Nested rules within this rule."""
128
+ conditions: list[SegmentCondition]
129
+ """Conditions that must be met for this rule, evaluated based on the rule type."""
130
+
131
+
132
+ class Segment(TypedDict):
133
+ """Represents a Flagsmith segment. Carries rules, feature overrides, and segment rules."""
134
+
135
+ id: DynamoInt
136
+ """Unique identifier for the segment in Core."""
137
+ name: str
138
+ """Name of the segment."""
139
+ rules: list[SegmentRule]
140
+ """List of rules within the segment."""
141
+ feature_states: list[FeatureState]
142
+ """List of segment overrides."""
143
+
144
+
145
+ class Organisation(TypedDict):
146
+ """Represents data about a Flagsmith organisation. Carries settings necessary for an SDK API operation."""
147
+
148
+ id: DynamoInt
149
+ """Unique identifier for the organisation in Core."""
150
+ name: str
151
+ """Organisation name."""
152
+ feature_analytics: NotRequired[bool]
153
+ """Whether the SDK API should log feature analytics events for this organisation. Defaults to `False`."""
154
+ stop_serving_flags: NotRequired[bool]
155
+ """Whether flag serving is disabled for this organisation. Defaults to `False`."""
156
+ persist_trait_data: NotRequired[bool]
157
+ """If set to `False`, trait data will never be persisted for this organisation. Defaults to `True`."""
158
+
159
+
160
+ class Project(TypedDict):
161
+ """Represents data about a Flagsmith project. Carries settings necessary for an SDK API operation."""
162
+
163
+ id: DynamoInt
164
+ """Unique identifier for the project in Core."""
165
+ name: str
166
+ """Project name."""
167
+ organisation: Organisation
168
+ """The organisation that this project belongs to."""
169
+ segments: list[Segment]
170
+ """List of segments."""
171
+ server_key_only_feature_ids: NotRequired[list[DynamoInt]]
172
+ """List of feature IDs that are skipped when the SDK API serves flags for a public client-side key."""
173
+ enable_realtime_updates: NotRequired[bool]
174
+ """Whether the SDK API should use real-time updates. Defaults to `False`. Not currently used neither by SDK APIs nor by SDKs themselves."""
175
+ hide_disabled_flags: NotRequired[bool | None]
176
+ """Whether the SDK API should hide disabled flags for this project. Defaults to `False`."""
177
+
178
+
179
+ class Integration(TypedDict):
180
+ """Represents evaluation integration data."""
181
+
182
+ api_key: NotRequired[str | None]
183
+ """API key for the integration."""
184
+ base_url: NotRequired[str | None]
185
+ """Base URL for the integration."""
186
+
187
+
188
+ class DynatraceIntegration(Integration):
189
+ """Represents Dynatrace evaluation integration data."""
190
+
191
+ entity_selector: str
192
+ """A Dynatrace entity selector string."""
193
+
194
+
195
+ class Webhook(TypedDict):
196
+ """Represents a webhook configuration."""
197
+
198
+ url: str
199
+ """Webhook target URL."""
200
+ secret: str
201
+ """Secret used to sign webhook payloads."""
202
+
203
+
204
+ class _EnvironmentFields(TypedDict):
205
+ """Common fields for Environment documents."""
206
+
207
+ name: NotRequired[str]
208
+ """Environment name. Defaults to an empty string if not set."""
209
+ updated_at: NotRequired[DateTimeStr | None]
210
+ """Last updated timestamp. If not set, current timestamp should be assumed."""
211
+
212
+ project: Project
213
+ """Project-specific data for this environment."""
214
+ feature_states: list[FeatureState]
215
+ """List of feature states representing the environment defaults."""
216
+
217
+ allow_client_traits: NotRequired[bool]
218
+ """Whether the SDK API should allow clients to set traits for this environment. Identical to project-level's `persist_trait_data` setting. Defaults to `True`."""
219
+ hide_sensitive_data: NotRequired[bool]
220
+ """Whether the SDK API should hide sensitive data for this environment. Defaults to `False`."""
221
+ hide_disabled_flags: NotRequired[bool | None]
222
+ """Whether the SDK API should hide disabled flags for this environment. If `None`, the SDK API should fall back to project-level setting."""
223
+ use_identity_composite_key_for_hashing: NotRequired[bool]
224
+ """Whether the SDK API should set `$.identity.key` in engine evaluation context to identity's composite key. Defaults to `False`."""
225
+ use_identity_overrides_in_local_eval: NotRequired[bool]
226
+ """Whether the SDK API should return identity overrides as part of the environment document. Defaults to `False`."""
227
+
228
+ amplitude_config: NotRequired[Integration | None]
229
+ """Amplitude integration configuration."""
230
+ dynatrace_config: NotRequired[DynatraceIntegration | None]
231
+ """Dynatrace integration configuration."""
232
+ heap_config: NotRequired[Integration | None]
233
+ """Heap integration configuration."""
234
+ mixpanel_config: NotRequired[Integration | None]
235
+ """Mixpanel integration configuration."""
236
+ rudderstack_config: NotRequired[Integration | None]
237
+ """RudderStack integration configuration."""
238
+ segment_config: NotRequired[Integration | None]
239
+ """Segment integration configuration."""
240
+ webhook_config: NotRequired[Webhook | None]
241
+ """Webhook configuration."""
242
+
243
+
244
+ ### Root document schemas below. Indexed fields are marked as **INDEXED** in the docstrings. ###
245
+
246
+
247
+ class EnvironmentAPIKey(TypedDict):
248
+ """Represents a server-side API key for a Flagsmith environment.
249
+
250
+ **DynamoDB table**: `flagsmith_environment_api_key`
251
+ """
252
+
253
+ id: DynamoInt
254
+ """Unique identifier for the environment API key in Core. **INDEXED**."""
255
+ key: str
256
+ """The server-side API key string, e.g. `"ser.xxxxxxxxxxxxx"`. **INDEXED**."""
257
+ created_at: DateTimeStr
258
+ """Creation timestamp."""
259
+ name: str
260
+ """Name of the API key."""
261
+ client_api_key: str
262
+ """The corresponding public client-side API key."""
263
+ expires_at: NotRequired[DateTimeStr | None]
264
+ """Expiration timestamp. If `None`, the key does not expire."""
265
+ active: bool
266
+ """Whether the key is active. Defaults to `True`."""
267
+
268
+
269
+ class Identity(TypedDict):
270
+ """Represents a Flagsmith identity within an environment. Carries traits and feature overrides.
271
+ Used for per-identity flag evaluations in remote evaluation mode.
272
+
273
+ **DynamoDB table**: `flagsmith_identities`
274
+ """
275
+
276
+ identifier: str
277
+ """Unique identifier for the identity. **INDEXED**."""
278
+ environment_api_key: str
279
+ """API key of the environment this identity belongs to. Used to scope the identity within a specific environment. **INDEXED**."""
280
+ identity_uuid: UUIDStr
281
+ """The UUID for this identity. **INDEXED**."""
282
+ composite_key: str
283
+ """A composite key combining the environment and identifier. **INDEXED**.
284
+
285
+ Generated as: `{environment_api_key}_{identifier}`.
286
+ """
287
+ created_date: DateTimeStr
288
+ """Creation timestamp."""
289
+ identity_features: (
290
+ "NotRequired[Annotated[list[FeatureState], ValidateIdentityFeatureStatesList]]"
291
+ )
292
+ """List of identity overrides for this identity."""
293
+ identity_traits: list[Trait]
294
+ """List of traits associated with this identity."""
295
+ django_id: NotRequired[DynamoInt | None]
296
+ """Unique identifier for the identity in Core. If identity created via Core's `edge-identities` API, this can be missing or `None`."""
297
+
298
+
299
+ class Environment(_EnvironmentFields):
300
+ """Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment.
301
+
302
+ **DynamoDB table**: `flagsmith_environments`
303
+ """
304
+
305
+ api_key: str
306
+ """Public client-side API key for the environment. **INDEXED**."""
307
+ id: DynamoInt
308
+ """Unique identifier for the environment in Core."""
309
+
310
+
311
+ class EnvironmentV2Meta(_EnvironmentFields):
312
+ """Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment.
313
+
314
+ **DynamoDB table**: `flagsmith_environments_v2`
315
+ """
316
+
317
+ environment_id: str
318
+ """Unique identifier for the environment in Core. Same as `Environment.id`, but string-typed to reduce coupling with Core's type definitions **INDEXED**."""
319
+ environment_api_key: str
320
+ """Public client-side API key for the environment. **INDEXED**."""
321
+ document_key: Literal["_META"]
322
+ """The fixed document key for the environment v2 document. Always `"_META"`. **INDEXED**."""
323
+
324
+ id: DynamoInt
325
+ """Unique identifier for the environment in Core. Exists for compatibility with the API environment document schema."""
326
+
327
+
328
+ class EnvironmentV2IdentityOverride(TypedDict):
329
+ """Represents an identity override.
330
+ Used for per-identity flag evaluations in local evaluation mode. Presented as part of the API environment document.
331
+
332
+ **DynamoDB table**: `flagsmith_environments_v2`
333
+ """
334
+
335
+ environment_id: str
336
+ """Unique identifier for the environment in Core. **INDEXED**."""
337
+ document_key: str
338
+ """The document key for this identity override, formatted as `identity_override:{feature Core ID}:{identity UUID}`. **INDEXED**."""
339
+ environment_api_key: str
340
+ """Public client-side API key for the environment. **INDEXED**."""
341
+ identifier: str
342
+ """Unique identifier for the identity. **INDEXED**."""
343
+ identity_uuid: str
344
+ """The UUID for this identity, used by `edge-identities` APIs in Core. **INDEXED**."""
345
+ feature_state: FeatureState
346
+ """The feature state override for this identity."""
@@ -0,0 +1,22 @@
1
+ from datetime import datetime
2
+ from decimal import Decimal
3
+ from uuid import UUID
4
+
5
+ from pydantic import AfterValidator, BeforeValidator, ValidateAs
6
+
7
+ from flagsmith_schemas.validators import (
8
+ validate_dynamo_feature_state_value,
9
+ validate_identity_feature_states,
10
+ validate_multivariate_feature_state_values,
11
+ )
12
+
13
+ ValidateDecimalAsFloat = ValidateAs(float, lambda v: Decimal(str(v)))
14
+ ValidateDecimalAsInt = ValidateAs(int, lambda v: Decimal(v))
15
+ ValidateStrAsISODateTime = ValidateAs(datetime, lambda dt: dt.isoformat())
16
+ ValidateStrAsUUID = ValidateAs(UUID, str)
17
+
18
+ ValidateDynamoFeatureStateValue = BeforeValidator(validate_dynamo_feature_state_value)
19
+ ValidateIdentityFeatureStatesList = AfterValidator(validate_identity_feature_states)
20
+ ValidateMultivariateFeatureValuesList = AfterValidator(
21
+ validate_multivariate_feature_state_values
22
+ )
@@ -0,0 +1,91 @@
1
+ from decimal import Decimal
2
+ from typing import TYPE_CHECKING, Annotated, Literal, TypeAlias
3
+
4
+ from flagsmith_schemas.constants import PYDANTIC_INSTALLED
5
+
6
+ if PYDANTIC_INSTALLED:
7
+ from flagsmith_schemas.pydantic_types import (
8
+ ValidateDecimalAsFloat,
9
+ ValidateDecimalAsInt,
10
+ ValidateDynamoFeatureStateValue,
11
+ ValidateStrAsISODateTime,
12
+ ValidateStrAsUUID,
13
+ )
14
+ elif not TYPE_CHECKING:
15
+ # This code runs at runtime when Pydantic is not installed.
16
+ # We could use PEP 649 strings with `Annotated`, but Pydantic is inconsistent in how it parses them.
17
+ # Define dummy types instead.
18
+ ValidateDecimalAsFloat = ...
19
+ ValidateDecimalAsInt = ...
20
+ ValidateDynamoFeatureStateValue = ...
21
+ ValidateStrAsISODateTime = ...
22
+ ValidateStrAsUUID = ...
23
+
24
+
25
+ DynamoInt: TypeAlias = Annotated[Decimal, ValidateDecimalAsInt]
26
+ """An integer value stored in DynamoDB.
27
+
28
+ DynamoDB represents all numbers as `Decimal`.
29
+ `DynamoInt` indicates that the value should be treated as an integer.
30
+ """
31
+
32
+ DynamoFloat: TypeAlias = Annotated[Decimal, ValidateDecimalAsFloat]
33
+ """A float value stored in DynamoDB.
34
+
35
+ DynamoDB represents all numbers as `Decimal`.
36
+ `DynamoFloat` indicates that the value should be treated as a float.
37
+ """
38
+
39
+ UUIDStr: TypeAlias = Annotated[str, ValidateStrAsUUID]
40
+ """A string representing a UUID."""
41
+
42
+ DateTimeStr: TypeAlias = Annotated[str, ValidateStrAsISODateTime]
43
+ """A string representing a date and time in ISO 8601 format."""
44
+
45
+ FeatureType = Literal["STANDARD", "MULTIVARIATE"]
46
+ """Represents the type of a Flagsmith feature. Multivariate features include multiple weighted values."""
47
+
48
+ DynamoFeatureValue: TypeAlias = Annotated[
49
+ DynamoInt | bool | str | None,
50
+ ValidateDynamoFeatureStateValue,
51
+ ]
52
+ """Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string.
53
+
54
+ The default (SaaS) maximum length for strings is 20000 characters.
55
+ """
56
+
57
+ DynamoContextValue: TypeAlias = DynamoInt | DynamoFloat | bool | str
58
+ """Represents a scalar value in the Flagsmith context, e.g., of an identity trait.
59
+ Here's how we store different types:
60
+ - Numeric string values (int, float) are stored as numbers.
61
+ - Boolean values are stored as booleans.
62
+ - All other values are stored as strings.
63
+ - Maximum length for strings is 2000 characters.
64
+
65
+ This type does not include complex structures like lists or dictionaries.
66
+ """
67
+
68
+ ConditionOperator = Literal[
69
+ "EQUAL",
70
+ "GREATER_THAN",
71
+ "LESS_THAN",
72
+ "LESS_THAN_INCLUSIVE",
73
+ "CONTAINS",
74
+ "GREATER_THAN_INCLUSIVE",
75
+ "NOT_CONTAINS",
76
+ "NOT_EQUAL",
77
+ "REGEX",
78
+ "PERCENTAGE_SPLIT",
79
+ "MODULO",
80
+ "IS_SET",
81
+ "IS_NOT_SET",
82
+ "IN",
83
+ ]
84
+ """Represents segment condition operators used by Flagsmith engine."""
85
+
86
+ RuleType = Literal[
87
+ "ALL",
88
+ "ANY",
89
+ "NONE",
90
+ ]
91
+ """Represents segment rule types used by Flagsmith engine."""
@@ -0,0 +1,55 @@
1
+ import typing
2
+ from decimal import Decimal
3
+
4
+ from flagsmith_schemas.constants import MAX_STRING_FEATURE_STATE_VALUE_LENGTH
5
+
6
+ if typing.TYPE_CHECKING:
7
+ from flagsmith_schemas.dynamodb import FeatureState, MultivariateFeatureStateValue
8
+ from flagsmith_schemas.types import DynamoFeatureValue
9
+
10
+
11
+ def validate_dynamo_feature_state_value(
12
+ value: typing.Any,
13
+ ) -> "DynamoFeatureValue":
14
+ if isinstance(value, bool | None):
15
+ return value
16
+ if isinstance(value, str):
17
+ if len(value) > MAX_STRING_FEATURE_STATE_VALUE_LENGTH:
18
+ raise ValueError(
19
+ "Dynamo feature state value string length cannot exceed "
20
+ f"{MAX_STRING_FEATURE_STATE_VALUE_LENGTH} characters "
21
+ f"(got {len(value)} characters)."
22
+ )
23
+ return value
24
+ if isinstance(value, int):
25
+ return Decimal(value)
26
+ return str(value)
27
+
28
+
29
+ def validate_multivariate_feature_state_values(
30
+ values: "list[MultivariateFeatureStateValue]",
31
+ ) -> "list[MultivariateFeatureStateValue]":
32
+ total_percentage = sum(value["percentage_allocation"] for value in values)
33
+ if total_percentage > 100:
34
+ raise ValueError(
35
+ "Total `percentage_allocation` of multivariate feature state values "
36
+ "cannot exceed 100."
37
+ )
38
+ return values
39
+
40
+
41
+ def validate_identity_feature_states(
42
+ values: "list[FeatureState]",
43
+ ) -> "list[FeatureState]":
44
+ seen: set[Decimal] = set()
45
+
46
+ for feature_state in values:
47
+ feature_id = feature_state["feature"]["id"]
48
+ if feature_id in seen:
49
+ raise ValueError(
50
+ f"Feature id={feature_id} cannot have multiple "
51
+ "feature states for a single identity."
52
+ )
53
+ seen.add(feature_id)
54
+
55
+ return values
File without changes