Skip to content

Commit 65768e3

Browse files
committed
feat: Support feature metadata
1 parent 9b28614 commit 65768e3

File tree

7 files changed

+77
-47
lines changed

7 files changed

+77
-47
lines changed

flagsmith/mappers.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
)
2323
from flagsmith.models import Segment
2424
from flagsmith.types import (
25+
FeatureMetadata,
2526
SDKEvaluationContext,
2627
SegmentMetadata,
2728
StreamEvent,
2829
TraitConfig,
2930
)
3031

3132
OverrideKey = typing.Tuple[
32-
str,
33+
int,
3334
str,
3435
bool,
3536
typing.Any,
@@ -148,7 +149,7 @@ def map_environment_document_to_context(
148149

149150
def _map_identity_overrides_to_segments(
150151
identity_overrides: list[IdentityModel],
151-
) -> dict[str, SegmentContext[SegmentMetadata]]:
152+
) -> dict[str, SegmentContext[SegmentMetadata, FeatureMetadata]]:
152153
features_to_identifiers: typing.Dict[
153154
OverridesKey,
154155
typing.List[str],
@@ -159,7 +160,7 @@ def _map_identity_overrides_to_segments(
159160
continue
160161
overrides_key = tuple(
161162
(
162-
str(feature_state["feature"]["id"]),
163+
feature_state["feature"]["id"],
163164
feature_state["feature"]["name"],
164165
feature_state["enabled"],
165166
feature_state["feature_state_value"],
@@ -170,7 +171,13 @@ def _map_identity_overrides_to_segments(
170171
)
171172
)
172173
features_to_identifiers[overrides_key].append(identity_override["identifier"])
173-
segment_contexts: typing.Dict[str, SegmentContext[SegmentMetadata]] = {}
174+
segment_contexts: typing.Dict[
175+
str,
176+
SegmentContext[
177+
SegmentMetadata,
178+
FeatureMetadata,
179+
],
180+
] = {}
174181
for overrides_key, identifiers in features_to_identifiers.items():
175182
# Create a segment context for each unique set of overrides
176183
# Generate a unique key to avoid collisions
@@ -193,13 +200,14 @@ def _map_identity_overrides_to_segments(
193200
overrides=[
194201
{
195202
"key": "", # Identity overrides never carry multivariate options
196-
"feature_key": feature_key,
203+
"feature_key": str(flagsmith_id),
197204
"name": feature_name,
198205
"enabled": feature_enabled,
199206
"value": feature_value,
200207
"priority": float("-inf"), # Highest possible priority
208+
"metadata": {"flagsmith_id": int(flagsmith_id)},
201209
}
202-
for feature_key, feature_name, feature_enabled, feature_value in overrides_key
210+
for flagsmith_id, feature_name, feature_enabled, feature_value in overrides_key
203211
],
204212
metadata=SegmentMetadata(source="identity_overrides"),
205213
)
@@ -230,16 +238,18 @@ def _map_environment_document_rules_to_context_rules(
230238

231239
def _map_environment_document_feature_states_to_feature_contexts(
232240
feature_states: list[FeatureStateModel],
233-
) -> typing.Iterable[FeatureContext]:
241+
) -> typing.Iterable[FeatureContext[FeatureMetadata]]:
234242
for feature_state in feature_states:
235-
feature_context = FeatureContext(
243+
metadata: FeatureMetadata = {"flagsmith_id": feature_state["feature"]["id"]}
244+
feature_context = FeatureContext[FeatureMetadata](
236245
key=str(
237246
feature_state.get("django_id") or feature_state["featurestate_uuid"]
238247
),
239248
feature_key=str(feature_state["feature"]["id"]),
240249
name=feature_state["feature"]["name"],
241250
enabled=feature_state["enabled"],
242251
value=feature_state["feature_state_value"],
252+
metadata=metadata,
243253
)
244254
if multivariate_feature_state_values := feature_state.get(
245255
"multivariate_feature_state_values"

flagsmith/models.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
import typing
44
from dataclasses import dataclass, field
55

6-
from flag_engine.result.types import FlagResult
7-
86
from flagsmith.analytics import AnalyticsProcessor
97
from flagsmith.exceptions import FlagsmithFeatureDoesNotExistError
10-
from flagsmith.types import SDKEvaluationResult
8+
from flagsmith.types import SDKEvaluationResult, SDKFlagResult
119

1210

1311
@dataclass
@@ -30,14 +28,18 @@ class Flag(BaseFlag):
3028
@classmethod
3129
def from_evaluation_result(
3230
cls,
33-
flag: FlagResult,
34-
) -> Flag:
35-
return Flag(
36-
enabled=flag["enabled"],
37-
value=flag["value"],
38-
feature_name=flag["name"],
39-
feature_id=int(flag["feature_key"]),
40-
)
31+
flag_result: SDKFlagResult,
32+
) -> typing.Optional[Flag]:
33+
if (
34+
flagsmith_id := (flag_result.get("metadata") or {}).get("flagsmith_id")
35+
) is not None:
36+
return Flag(
37+
enabled=flag_result["enabled"],
38+
value=flag_result["value"],
39+
feature_name=flag_result["name"],
40+
feature_id=flagsmith_id,
41+
)
42+
return None
4143

4244
@classmethod
4345
def from_api_flag(cls, flag_data: typing.Mapping[str, typing.Any]) -> Flag:
@@ -64,13 +66,9 @@ def from_evaluation_result(
6466
) -> Flags:
6567
return cls(
6668
flags={
67-
flag_name: Flag(
68-
enabled=flag["enabled"],
69-
value=flag["value"],
70-
feature_name=flag["name"],
71-
feature_id=int(flag["feature_key"]),
72-
)
73-
for flag_name, flag in evaluation_result["flags"].items()
69+
flag_name: flag
70+
for flag_name, flag_result in evaluation_result["flags"].items()
71+
if (flag := Flag.from_evaluation_result(flag_result))
7472
},
7573
default_flag_handler=default_flag_handler,
7674
_analytics_processor=analytics_processor,

flagsmith/types.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from flag_engine.context.types import EvaluationContext
55
from flag_engine.engine import ContextValue
6-
from flag_engine.result.types import EvaluationResult
6+
from flag_engine.result.types import EvaluationResult, FlagResult
77
from typing_extensions import NotRequired, TypeAlias
88

99
_JsonScalarType: TypeAlias = typing.Union[
@@ -44,5 +44,11 @@ class SegmentMetadata(typing.TypedDict):
4444
"""The source of the segment, e.g. 'api', 'identity_overrides'."""
4545

4646

47-
SDKEvaluationContext = EvaluationContext[SegmentMetadata]
48-
SDKEvaluationResult = EvaluationResult[SegmentMetadata]
47+
class FeatureMetadata(typing.TypedDict):
48+
flagsmith_id: NotRequired[int]
49+
"""The ID of the feature used in Flagsmith API."""
50+
51+
52+
SDKEvaluationContext = EvaluationContext[SegmentMetadata, FeatureMetadata]
53+
SDKEvaluationResult = EvaluationResult[SegmentMetadata, FeatureMetadata]
54+
SDKFlagResult = FlagResult[FeatureMetadata]

poetry.lock

Lines changed: 10 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ documentation = "https://docs.flagsmith.com"
1010
packages = [{ include = "flagsmith" }]
1111

1212
[tool.poetry.dependencies]
13-
flagsmith-flag-engine = "^9.0.0"
13+
flagsmith-flag-engine = { git = "https://github.com/Flagsmith/flagsmith-engine.git", branch = "feat/feature-metadata" }
1414
python = ">=3.9,<4"
1515
requests = "^2.32.3"
1616
requests-futures = "^1.0.1"

tests/test_flagsmith.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ def test_get_identity_flags_uses_local_environment_when_available(
166166
"enabled": True,
167167
"value": "some-feature-state-value",
168168
"feature_key": "1",
169+
"metadata": {"flagsmith_id": 1},
169170
}
170171
},
171172
"segments": [],

tests/test_models.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,25 @@
44
from flag_engine.result.types import FlagResult
55

66
from flagsmith.models import Flag, Flags
7-
from flagsmith.types import SDKEvaluationResult
7+
from flagsmith.types import SDKEvaluationResult, SDKFlagResult
88

99

1010
def test_flag_from_evaluation_result() -> None:
1111
# Given
12-
flag_result: FlagResult = {
12+
flag_result: SDKFlagResult = {
1313
"enabled": True,
1414
"feature_key": "123",
1515
"name": "test_feature",
1616
"reason": "DEFAULT",
1717
"value": "test-value",
18+
"metadata": {"flagsmith_id": 123},
1819
}
1920

2021
# When
21-
flag: Flag = Flag.from_evaluation_result(flag_result)
22+
flag: typing.Optional[Flag] = Flag.from_evaluation_result(flag_result)
2223

2324
# Then
25+
assert flag
2426
assert flag.enabled is True
2527
assert flag.value == "test-value"
2628
assert flag.feature_name == "test_feature"
@@ -29,9 +31,9 @@ def test_flag_from_evaluation_result() -> None:
2931

3032

3133
@pytest.mark.parametrize(
32-
"flags_result,expected_count,expected_names",
34+
"flags_result,expected_names",
3335
[
34-
({}, 0, []),
36+
({}, []),
3537
(
3638
{
3739
"feature1": {
@@ -40,9 +42,9 @@ def test_flag_from_evaluation_result() -> None:
4042
"name": "feature1",
4143
"reason": "DEFAULT",
4244
"value": "value1",
45+
"metadata": {"flagsmith_id": 1},
4346
}
4447
},
45-
1,
4648
["feature1"],
4749
),
4850
(
@@ -53,9 +55,9 @@ def test_flag_from_evaluation_result() -> None:
5355
"name": "feature1",
5456
"reason": "DEFAULT",
5557
"value": "value1",
58+
"metadata": {"flagsmith_id": 1},
5659
}
5760
},
58-
1,
5961
["feature1"],
6062
),
6163
(
@@ -66,30 +68,38 @@ def test_flag_from_evaluation_result() -> None:
6668
"name": "feature1",
6769
"reason": "DEFAULT",
6870
"value": "value1",
71+
"metadata": {"flagsmith_id": 1},
6972
},
7073
"feature2": {
7174
"enabled": True,
7275
"feature_key": "2",
7376
"name": "feature2",
7477
"reason": "DEFAULT",
7578
"value": "value2",
79+
"metadata": {"flagsmith_id": 2},
7680
},
7781
"feature3": {
7882
"enabled": True,
7983
"feature_key": "3",
8084
"name": "feature3",
8185
"reason": "DEFAULT",
8286
"value": 42,
87+
"metadata": {"flagsmith_id": 3},
88+
},
89+
"feature4": {
90+
"enabled": True,
91+
"feature_key": "4",
92+
"name": "feature4",
93+
"reason": "DEFAULT",
94+
"value": 42,
8395
},
8496
},
85-
3,
8697
["feature1", "feature2", "feature3"],
8798
),
8899
],
89100
)
90101
def test_flags_from_evaluation_result(
91-
flags_result: typing.Dict[str, FlagResult],
92-
expected_count: int,
102+
flags_result: typing.Dict[str, SDKFlagResult],
93103
expected_names: typing.List[str],
94104
) -> None:
95105
# Given
@@ -106,7 +116,7 @@ def test_flags_from_evaluation_result(
106116
)
107117

108118
# Then
109-
assert len(flags.flags) == expected_count
119+
assert len(flags.flags) == len(expected_names)
110120

111121
for name in expected_names:
112122
assert name in flags.flags
@@ -136,6 +146,7 @@ def test_flag_from_evaluation_result_value_types(
136146
"name": "test_feature",
137147
"reason": "DEFAULT",
138148
"value": value,
149+
"metadata": {"flagsmith_id": 123},
139150
}
140151

141152
# When

0 commit comments

Comments
 (0)