Skip to content

Commit 6921024

Browse files
committed
feat(schemas): Add SDK API request and response schemas
1 parent c8dd426 commit 6921024

File tree

4 files changed

+252
-31
lines changed

4 files changed

+252
-31
lines changed

src/flagsmith_schemas/api.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""
2+
The types in this module describe Flagsmith SDK API request and response schemas.
3+
The docstrings here comprise user-facing documentation for these types.
4+
5+
The types are used by:
6+
- SDK API OpenAPI schema generation.
7+
- Flagsmith's API and SDK implementations written in Python.
8+
9+
These types can be used with for validation and serialization
10+
with any library that supports TypedDict, such as Pydantic or typeguard.
11+
12+
When updating this module, ensure that the changes are backwards compatible.
13+
"""
14+
15+
from flag_engine.engine import ContextValue
16+
from flag_engine.segments.types import ConditionOperator, RuleType
17+
from typing_extensions import NotRequired, TypedDict
18+
19+
from flagsmith_schemas.types import FeatureType, FeatureValue, UUIDStr
20+
21+
22+
class Feature(TypedDict):
23+
"""Represents a Flagsmith feature, defined at project level."""
24+
25+
id: int
26+
"""Unique identifier for the feature in Core."""
27+
name: str
28+
"""Name of the feature. Must be unique within a project."""
29+
type: FeatureType
30+
"""Feature type."""
31+
32+
33+
class MultivariateFeatureOption(TypedDict):
34+
"""Represents a single multivariate feature option in the Flagsmith UI."""
35+
36+
value: str
37+
"""The feature state value that should be served when this option's parent multivariate feature state is selected by the engine."""
38+
39+
40+
class MultivariateFeatureStateValue(TypedDict):
41+
"""Represents a multivariate feature state value."""
42+
43+
id: int | None
44+
"""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`."""
45+
mv_fs_value_uuid: UUIDStr | None
46+
"""The UUID for this multivariate feature state value. Should be used for multivariate bucketing if `id` is null."""
47+
percentage_allocation: float
48+
"""The percentage allocation for this multivariate feature state value. Should be between or equal to 0 and 100."""
49+
multivariate_feature_option: MultivariateFeatureOption
50+
"""The multivariate feature option that this value corresponds to."""
51+
52+
53+
class FeatureSegment(TypedDict):
54+
"""Represents data specific to a segment feature override."""
55+
56+
priority: int | None
57+
"""The priority of this segment feature override. Lower numbers indicate stronger priority. If null or not set, the weakest priority is assumed."""
58+
59+
60+
class FeatureState(TypedDict):
61+
"""Used to define the state of a feature for an environment, segment overrides, and identity overrides."""
62+
63+
feature: Feature
64+
"""The feature that this feature state is for."""
65+
enabled: bool
66+
"""Whether the feature is enabled or disabled."""
67+
feature_state_value: object
68+
"""The value for this feature state."""
69+
featurestate_uuid: UUIDStr
70+
"""The UUID for this feature state."""
71+
feature_segment: FeatureSegment | None
72+
"""Segment override data, if this feature state is for a segment override."""
73+
multivariate_feature_state_values: list[MultivariateFeatureStateValue]
74+
"""List of multivariate feature state values, if this feature state is for a multivariate feature."""
75+
76+
77+
class Trait(TypedDict):
78+
"""Represents a key-value pair associated with an identity."""
79+
80+
trait_key: str
81+
"""Key of the trait."""
82+
trait_value: ContextValue
83+
"""Value of the trait."""
84+
85+
86+
class SegmentCondition(TypedDict):
87+
"""Represents a condition within a segment rule used by Flagsmith engine."""
88+
89+
operator: ConditionOperator
90+
"""Operator to be applied for this condition."""
91+
value: str
92+
"""Value to be compared against in this condition. May be `None` for `IS_SET` and `IS_NOT_SET` operators."""
93+
property_: str
94+
"""The property (context key) this condition applies to. May be `None` for the `PERCENTAGE_SPLIT` operator.
95+
96+
Named `property_` for legacy reasons.
97+
"""
98+
99+
100+
class SegmentRule(TypedDict):
101+
"""Represents a rule within a segment used by Flagsmith engine."""
102+
103+
type: RuleType
104+
"""Type of the rule, defining how conditions are evaluated."""
105+
rules: "list[SegmentRule]"
106+
"""Nested rules within this rule."""
107+
conditions: list[SegmentCondition]
108+
"""Conditions that must be met for this rule, evaluated based on the rule type."""
109+
110+
111+
class Segment(TypedDict):
112+
"""Represents a Flagsmith segment. Carries rules, feature overrides, and segment rules."""
113+
114+
id: int
115+
"""Unique identifier for the segment in Core."""
116+
name: str
117+
"""Segment name."""
118+
rules: list[SegmentRule]
119+
"""List of rules within the segment."""
120+
feature_states: NotRequired[list[FeatureState]]
121+
"""List of segment overrides."""
122+
123+
124+
class Project(TypedDict):
125+
"""Represents data about a Flagsmith project. For SDKs, this is mainly used to convey segment data."""
126+
127+
segments: list[Segment]
128+
"""List of segments."""
129+
130+
131+
class IdentityOverride(TypedDict):
132+
"""Represents an identity override, defining feature states specific to an identity."""
133+
134+
identifier: str
135+
"""Unique identifier for the identity."""
136+
identity_features: list[FeatureState]
137+
"""List of identity overrides for this identity."""
138+
139+
140+
class InputTrait(TypedDict):
141+
"""Represents a key-value pair trait provided as input when creating or updating an identity."""
142+
143+
trait_key: str
144+
"""Trait key."""
145+
trait_value: ContextValue
146+
"""Trait value. If `null`, the trait will be deleted."""
147+
transient: NotRequired[bool | None]
148+
"""Whether this trait is transient (not persisted). Defaults to `false`."""
149+
150+
151+
class V1Flag(TypedDict):
152+
"""Represents a single flag (feature state) returned by the Flagsmith SDK."""
153+
154+
feature: Feature
155+
"""The feature that this flag represents."""
156+
enabled: bool
157+
"""Whether the feature is enabled or disabled."""
158+
feature_state_value: FeatureValue
159+
"""The value for this feature state."""
160+
161+
162+
### Root request schemas below. ###
163+
164+
165+
class V1IdentitiesRequest(TypedDict):
166+
"""`/api/v1/identities/` request.
167+
168+
Used to retrieve flags for an identity and store its traits.
169+
"""
170+
171+
identifier: str
172+
"""Unique identifier for the identity."""
173+
traits: NotRequired[list[InputTrait] | None]
174+
"""List of traits to set for the identity. If `null` or not provided, no traits are set or updated."""
175+
transient: NotRequired[bool | None]
176+
"""Whether the identity is transient (not persisted). Defaults to `false`."""
177+
178+
179+
### Root response schemas below. ###
180+
181+
182+
class V1EnvironmentDocumentResponse(TypedDict):
183+
"""`/api/v1/environments-document/` response.
184+
185+
Powers Flagsmith SDK's local evaluation mode.
186+
"""
187+
188+
api_key: str
189+
"""Public client-side API key for the environment, used to identify it."""
190+
feature_states: list[FeatureState]
191+
"""List of feature states representing the environment defaults."""
192+
identity_overrides: list[IdentityOverride]
193+
"""List of identity overrides defined for this environment."""
194+
name: str
195+
"""Environment name."""
196+
project: Project
197+
"""Project-specific data for this environment."""
198+
199+
200+
V1FlagsResponse = list[V1Flag]
201+
"""`/api/v1/flags/` response.
202+
203+
A list of flags for the specified environment."""
204+
205+
206+
class V1IdentitiesResponse(TypedDict):
207+
"""`/api/v1/identities/` response.
208+
209+
Represents the identity created or updated, along with its flags.
210+
"""
211+
212+
identifier: str
213+
"""Unique identifier for the identity."""
214+
flags: list[V1Flag]
215+
"""List of flags (feature states) for the identity."""
216+
traits: list[Trait]
217+
"""List of traits associated with the identity."""

src/flagsmith_schemas/dynamodb.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,17 @@
99

1010
from typing import Annotated, Literal
1111

12+
from flag_engine.segments.types import ConditionOperator, RuleType
1213
from typing_extensions import NotRequired, TypedDict
1314

1415
from flagsmith_schemas.constants import PYDANTIC_INSTALLED
1516
from flagsmith_schemas.types import (
16-
ConditionOperator,
1717
DateTimeStr,
1818
DynamoContextValue,
1919
DynamoFeatureValue,
2020
DynamoFloat,
2121
DynamoInt,
2222
FeatureType,
23-
RuleType,
2423
UUIDStr,
2524
)
2625

@@ -99,9 +98,9 @@ class Trait(TypedDict):
9998
"""Represents a key-value pair associated with an identity."""
10099

101100
trait_key: str
102-
"""Key of the trait."""
101+
"""Trait key."""
103102
trait_value: DynamoContextValue
104-
"""Value of the trait."""
103+
"""Trait value."""
105104

106105

107106
class SegmentCondition(TypedDict):
@@ -138,7 +137,7 @@ class Segment(TypedDict):
138137
"""Name of the segment."""
139138
rules: list[SegmentRule]
140139
"""List of rules within the segment."""
141-
feature_states: list[FeatureState]
140+
feature_states: NotRequired[list[FeatureState]]
142141
"""List of segment overrides."""
143142

144143

src/flagsmith_schemas/types.py

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from flagsmith_schemas.constants import PYDANTIC_INSTALLED
55

66
if PYDANTIC_INSTALLED:
7+
from pydantic import WithJsonSchema
8+
79
from flagsmith_schemas.pydantic_types import (
810
ValidateDecimalAsFloat,
911
ValidateDecimalAsInt,
@@ -15,6 +17,9 @@
1517
# This code runs at runtime when Pydantic is not installed.
1618
# We could use PEP 649 strings with `Annotated`, but Pydantic is inconsistent in how it parses them.
1719
# Define dummy types instead.
20+
def WithJsonSchema(_: object) -> object:
21+
return ...
22+
1823
ValidateDecimalAsFloat = ...
1924
ValidateDecimalAsInt = ...
2025
ValidateDynamoFeatureStateValue = ...
@@ -36,7 +41,11 @@
3641
`DynamoFloat` indicates that the value should be treated as a float.
3742
"""
3843

39-
UUIDStr: TypeAlias = Annotated[str, ValidateStrAsUUID]
44+
UUIDStr: TypeAlias = Annotated[
45+
str,
46+
ValidateStrAsUUID,
47+
WithJsonSchema({"type": "string", "format": "uuid"}),
48+
]
4049
"""A string representing a UUID."""
4150

4251
DateTimeStr: TypeAlias = Annotated[str, ValidateStrAsISODateTime]
@@ -49,7 +58,7 @@
4958
DynamoInt | bool | str | None,
5059
ValidateDynamoFeatureStateValue,
5160
]
52-
"""Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string.
61+
"""Represents the value of a Flagsmith feature stored in DynamoDB. Can be stored a boolean, an integer, or a string.
5362
5463
The default (SaaS) maximum length for strings is 20000 characters.
5564
"""
@@ -65,27 +74,5 @@
6574
This type does not include complex structures like lists or dictionaries.
6675
"""
6776

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."""
77+
FeatureValue: TypeAlias = int | bool | str | None
78+
"""Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string."""
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from pydantic import TypeAdapter
2+
3+
from flagsmith_schemas.api import FeatureState
4+
5+
6+
def test_feature_state__featurestate_uuid__expected_json_schema() -> None:
7+
# Given
8+
type_adapter: TypeAdapter[FeatureState] = TypeAdapter(FeatureState)
9+
10+
# When
11+
schema = type_adapter.json_schema()["properties"]["featurestate_uuid"]
12+
13+
# Then
14+
assert schema == {
15+
"format": "uuid",
16+
"title": "Featurestate Uuid",
17+
"type": "string",
18+
}

0 commit comments

Comments
 (0)