Skip to content

Commit 8b18b1a

Browse files
committed
feat: Add DynamoDB document schemas
1 parent 51c529b commit 8b18b1a

File tree

8 files changed

+1030
-3
lines changed

8 files changed

+1030
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
*.pyc
55
.coverage
66
common.sqlite3
7-
dist/
7+
dist/
8+
coverage.xml

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ optional-dependencies = { test-tools = [
2121
"psycopg2-binary (>=2.9,<3)",
2222
"requests",
2323
"simplejson (>=3,<4)",
24-
2524
], task-processor = [
2625
"backoff (>=2.2.1,<3.0.0)",
2726
"django (>4,<5)",
@@ -69,6 +68,7 @@ dev = [
6968
"djangorestframework-stubs (>=3.15.3, <4.0.0)",
7069
"mypy (>=1.15.0, <2.0.0)",
7170
"pre-commit",
71+
"pydantic>=2.12.5",
7272
"pyfakefs (>=5.7.4, <6.0.0)",
7373
"pytest (>=8.3.4, <9.0.0)",
7474
"pytest-asyncio (>=0.25.3, <1.0.0)",
@@ -78,8 +78,8 @@ dev = [
7878
"pytest-httpserver (>=1.1.3, <2.0.0)",
7979
"pytest-mock (>=3.14.0, <4.0.0)",
8080
"setuptools (>=78.1.1, <79.0.0)",
81-
"types-simplejson (>=3.20.0.20250326, <4.0.0)",
8281
"types-python-dateutil (>=2.9.0.20250516, <3.0.0)",
82+
"types-simplejson (>=3.20.0.20250326, <4.0.0)",
8383
]
8484

8585
[build-system]

src/flagsmith_models/__init__.py

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
"""
2+
Types describing Flagsmith Edge API's data model.
3+
The schemas are written to DynamoDB documents by Core, and read by Edge.
4+
"""
5+
6+
from datetime import datetime
7+
from typing import Literal, NotRequired, TypeAlias, TypedDict
8+
from uuid import UUID
9+
10+
FeatureType = Literal["STANDARD", "MULTIVARIATE"]
11+
"""Represents the type of a Flagsmith feature. Multivariate features include multiple weighted values."""
12+
13+
FeatureValue: TypeAlias = object
14+
"""Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string.
15+
16+
The default (SaaS) maximum length for strings is 20000 characters.
17+
"""
18+
19+
ContextValue: TypeAlias = int | float | bool | str
20+
"""Represents a scalar value in the Flagsmith context, e.g., of an identity trait.
21+
Here's how we store different types:
22+
- Numeric string values (int, float) are stored as numbers.
23+
- Boolean values are stored as booleans.
24+
- All other values are stored as strings.
25+
- Maximum length for strings is 2000 characters.
26+
27+
This type does not include complex structures like lists or dictionaries.
28+
"""
29+
30+
ConditionOperator = Literal[
31+
"EQUAL",
32+
"GREATER_THAN",
33+
"LESS_THAN",
34+
"LESS_THAN_INCLUSIVE",
35+
"CONTAINS",
36+
"GREATER_THAN_INCLUSIVE",
37+
"NOT_CONTAINS",
38+
"NOT_EQUAL",
39+
"REGEX",
40+
"PERCENTAGE_SPLIT",
41+
"MODULO",
42+
"IS_SET",
43+
"IS_NOT_SET",
44+
"IN",
45+
]
46+
"""Represents segment condition operators used by Flagsmith engine."""
47+
48+
RuleType = Literal[
49+
"ALL",
50+
"ANY",
51+
"NONE",
52+
]
53+
"""Represents segment rule types used by Flagsmith engine."""
54+
55+
56+
class Feature(TypedDict):
57+
"""Represents a Flagsmith feature defined at project level."""
58+
59+
id: int
60+
"""Unique identifier for the feature in Core."""
61+
name: str
62+
"""Name of the feature. Must be unique within a project."""
63+
type: FeatureType
64+
65+
66+
class MultivariateFeatureOption(TypedDict):
67+
"""A container for a feature state value of a multivariate feature state."""
68+
69+
id: NotRequired[int | None]
70+
"""Unique identifier for the multivariate feature option in Core. **DEPRECATED**: MultivariateFeatureValue.id should be used instead."""
71+
value: FeatureValue
72+
"""The feature state value that should be served when this option's parent multivariate feature state is selected by the engine."""
73+
74+
75+
class MultivariateFeatureStateValue(TypedDict):
76+
"""Represents a multivariate feature state value assigned to an identity or environment."""
77+
78+
id: NotRequired[int | None]
79+
"""Unique identifier for the multivariate feature state value in Core. TODO: document why and when this can be `None`."""
80+
mv_fs_value_uuid: NotRequired[UUID]
81+
"""The UUID for this multivariate feature state value. Should be used if `id` is `None`."""
82+
percentage_allocation: float
83+
"""The percentage allocation for this multivariate feature state value. Should be between or equal to 0 and 100."""
84+
multivariate_feature_option: MultivariateFeatureOption
85+
"""The multivariate feature option that this value corresponds to."""
86+
87+
88+
class FeatureSegment(TypedDict):
89+
"""Represents data specific to a segment feature override."""
90+
91+
priority: NotRequired[int | None]
92+
"""The priority of this segment feature override. Lower numbers indicate stronger priority. If `None` or not set, the weakest priority is assumed."""
93+
94+
95+
class FeatureState(TypedDict):
96+
"""Represents a Flagsmith feature state. Used to define the state of a feature for an environment, segment overrides, and identity overrides."""
97+
98+
feature: Feature
99+
"""The feature that this feature state is for."""
100+
enabled: bool
101+
"""Whether the feature is enabled or disabled."""
102+
feature_state_value: FeatureValue
103+
"""The value for this feature state."""
104+
django_id: NotRequired[int | None]
105+
"""Unique identifier for the feature state in Core. TODO: document why and when this can be `None`."""
106+
featurestate_uuid: NotRequired[UUID]
107+
"""The UUID for this feature state. Should be used if `django_id` is `None`. If not set, should be generated."""
108+
feature_segment: NotRequired[FeatureSegment | None]
109+
"""Segment override data, if this feature state is for a segment override."""
110+
multivariate_feature_state_values: NotRequired[list[MultivariateFeatureStateValue]]
111+
"""List of multivariate feature state values, if this feature state is for a multivariate feature.
112+
113+
Total `percentage_allocation` sum must be less or equal to 100.
114+
"""
115+
116+
117+
class Trait(TypedDict):
118+
"""Represents a key-value pair associated with an identity."""
119+
120+
trait_key: str
121+
"""Key of the trait."""
122+
trait_value: ContextValue
123+
"""Value of the trait."""
124+
125+
126+
class SegmentCondition(TypedDict):
127+
"""Represents a condition within a segment rule used by Flagsmith engine."""
128+
129+
operator: ConditionOperator
130+
"""Operator to be applied for this condition."""
131+
value: NotRequired[str | None]
132+
"""Value to be compared against in this condition. May be `None` for `IS_SET` and `IS_NOT_SET` operators."""
133+
property_: NotRequired[str | None]
134+
"""The property (context key) this condition applies to. May be `None` for the `PERCENTAGE_SPLIT` operator.
135+
136+
Named `property_` to avoid conflict with Python's `property` built-in.
137+
"""
138+
139+
140+
class SegmentRule(TypedDict):
141+
"""Represents a rule within a segment used by Flagsmith engine."""
142+
143+
type: RuleType
144+
"""Type of the rule, defining how conditions are evaluated."""
145+
rules: "list[SegmentRule]"
146+
"""Nested rules within this rule."""
147+
conditions: list[SegmentCondition]
148+
"""Conditions that must be met for this rule, evaluated based on the rule type."""
149+
150+
151+
class Segment(TypedDict):
152+
"""Represents a Flagsmith segment. Carries rules, feature overrides, and segment rules."""
153+
154+
id: int
155+
"""Unique identifier for the segment in Core."""
156+
name: str
157+
"""Name of the segment."""
158+
rules: list[SegmentRule]
159+
"""List of rules within the segment."""
160+
feature_states: list[FeatureState]
161+
"""List of segment overrides."""
162+
163+
164+
class Organisation(TypedDict):
165+
"""Represents data about a Flagsmith organisation. Carries settings necessary for an SDK API operation."""
166+
167+
id: int
168+
"""Unique identifier for the organisation in Core."""
169+
name: str
170+
"""Organisation name as set via Core."""
171+
feature_analytics: NotRequired[bool]
172+
"""Whether the SDK API should log feature analytics events for this organisation. Defaults to `False`."""
173+
stop_serving_flags: NotRequired[bool]
174+
"""Whether flag serving is disabled for this organisation. Defaults to `False`."""
175+
persist_trait_data: NotRequired[bool]
176+
"""If set to `False`, trait data will never be persisted for this organisation. Defaults to `True`."""
177+
178+
179+
class Project(TypedDict):
180+
"""Represents data about a Flagsmith project. Carries settings necessary for an SDK API operation."""
181+
182+
id: int
183+
"""Unique identifier for the project in Core."""
184+
name: str
185+
"""Project name as set via Core."""
186+
organisation: Organisation
187+
"""The organisation that this project belongs to."""
188+
segments: list[Segment]
189+
"""List of segments."""
190+
server_key_only_feature_ids: NotRequired[list[int]]
191+
"""List of feature IDs that are skipped when the SDK API serves flags for a public client-side key."""
192+
enable_realtime_updates: NotRequired[bool]
193+
"""Whether the SDK API should use real-time updates. Defaults to `False`. Not currently used neither by SDK APIs nor by SDKs themselves."""
194+
hide_disabled_flags: NotRequired[bool]
195+
"""Whether the SDK API should hide disabled flags for this project. Defaults to `False`."""
196+
197+
198+
class Integration(TypedDict):
199+
"""Represents evaluation integration data."""
200+
201+
api_key: NotRequired[str | None]
202+
"""API key for the integration."""
203+
base_url: NotRequired[str | None]
204+
"""Base URL for the integration."""
205+
206+
207+
class DynatraceIntegration(Integration):
208+
"""Represents Dynatrace evaluation integration data."""
209+
210+
entity_selector: str
211+
"""A Dynatrace entity selector string."""
212+
213+
214+
class Webhook(TypedDict):
215+
"""Represents a webhook configuration."""
216+
217+
url: str
218+
"""Webhook target URL."""
219+
secret: str
220+
"""Secret used to sign webhook payloads."""
221+
222+
223+
### Root document schemas below. Indexed fields are marked as **INDEXED** in the docstrings. ###
224+
225+
226+
class EnvironmentAPIKey(TypedDict):
227+
"""Represents a server-side API key for a Flagsmith environment."""
228+
229+
id: int
230+
"""Unique identifier for the environment API key in Core. **INDEXED**."""
231+
key: str
232+
"""The server-side API key string, e.g. `"ser.xxxxxxxxxxxxx"`. **INDEXED**."""
233+
created_at: datetime
234+
"""Creation timestamp."""
235+
name: str
236+
"""Name of the API key."""
237+
client_api_key: str
238+
"""The corresponding public client-side API key."""
239+
expires_at: NotRequired[datetime | None]
240+
"""Expiration timestamp. If `None`, the key does not expire."""
241+
active: bool
242+
"""Whether the key is active. Defaults to `True`."""
243+
244+
245+
class Identity(TypedDict):
246+
"""Represents a Flagsmith identity within an environment. Carries traits and feature overrides."""
247+
248+
identifier: str
249+
"""Unique identifier for the identity. **INDEXED**."""
250+
environment_api_key: str
251+
"""API key of the environment this identity belongs to. Used to scope the identity within a specific environment. **INDEXED**."""
252+
identity_uuid: UUID
253+
"""The UUID for this identity. **INDEXED**."""
254+
composite_key: str
255+
"""A composite key combining the environment and identifier. **INDEXED**.
256+
257+
Generated as: `{environment_api_key}_{identifier}`.
258+
"""
259+
created_date: datetime
260+
"""Creation timestamp."""
261+
identity_features: NotRequired[list[FeatureState]]
262+
"""List of identity overrides for this identity."""
263+
identity_traits: list[Trait]
264+
"""List of traits associated with this identity."""
265+
django_id: NotRequired[int | None]
266+
"""Unique identifier for the identity in Core. TODO: document why and when this can be `None`."""
267+
268+
269+
class Environment(TypedDict):
270+
"""Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment."""
271+
272+
id: int
273+
"""Unique identifier for the environment in Core. **INDEXED**."""
274+
api_key: str
275+
"""Public client-side API key for the environment. **INDEXED**."""
276+
name: NotRequired[str]
277+
"""Environment name."""
278+
updated_at: NotRequired[datetime | None]
279+
"""Last updated timestamp. If not set, current timestamp should be assumed."""
280+
281+
project: Project
282+
"""Project-specific data for this environment."""
283+
feature_states: list[FeatureState]
284+
"""List of feature states representing the environment defaults."""
285+
286+
allow_client_traits: NotRequired[bool]
287+
"""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`."""
288+
hide_sensitive_data: NotRequired[bool]
289+
"""Whether the SDK API should hide sensitive data for this environment. Defaults to `False`."""
290+
hide_disabled_flags: NotRequired[bool]
291+
"""Whether the SDK API should hide disabled flags for this environment. If `None`, the SDK API should fall back to project-level setting."""
292+
use_identity_composite_key_for_hashing: NotRequired[bool]
293+
"""Whether the SDK API should set `$.identity.key` in engine evaluation context to identity's composite key. Defaults to `False`."""
294+
use_identity_overrides_in_local_eval: NotRequired[bool]
295+
"""Whether the SDK API should return identity overrides as part of the environment document. Defaults to `False`."""
296+
297+
amplitude_config: NotRequired[Integration]
298+
"""Amplitude integration configuration."""
299+
dynatrace_config: NotRequired[DynatraceIntegration]
300+
"""Dynatrace integration configuration."""
301+
heap_config: NotRequired[Integration]
302+
"""Heap integration configuration."""
303+
mixpanel_config: NotRequired[Integration]
304+
"""Mixpanel integration configuration."""
305+
rudderstack_config: NotRequired[Integration]
306+
"""RudderStack integration configuration."""
307+
segment_config: NotRequired[Integration]
308+
"""Segment integration configuration."""
309+
webhook_config: NotRequired[Webhook]
310+
"""Webhook configuration."""

src/flagsmith_models/py.typed

Whitespace-only changes.

tests/integration/flagsmith_models/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)