Skip to content

Commit ce9cbf3

Browse files
committed
chore: moved-mapper-within-sdk
1 parent 0dfc434 commit ce9cbf3

File tree

2 files changed

+211
-1
lines changed

2 files changed

+211
-1
lines changed

flagsmith/flagsmith.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pydantic
77
import requests
88
from flag_engine import engine
9-
from flag_engine.context.mappers import map_environment_identity_to_context
9+
from flagsmith.mappers import map_environment_identity_to_context
1010
from flag_engine.environments.models import EnvironmentModel
1111
from flag_engine.identities.models import IdentityModel
1212
from flag_engine.identities.traits.models import TraitModel

flagsmith/mappers.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import typing
2+
from collections import defaultdict
3+
4+
from flag_engine.context.types import (
5+
EvaluationContext,
6+
SegmentContext,
7+
FeatureContext,
8+
SegmentRule,
9+
)
10+
from flag_engine.environments.models import EnvironmentModel
11+
from flag_engine.features.models import (
12+
FeatureStateModel,
13+
MultivariateFeatureStateValueModel,
14+
)
15+
from flag_engine.identities.models import IdentityModel
16+
from flag_engine.identities.traits.models import TraitModel
17+
from flag_engine.segments.models import SegmentRuleModel
18+
19+
OverrideKey = typing.Tuple[
20+
str,
21+
str,
22+
bool,
23+
typing.Any,
24+
]
25+
OverridesKey = typing.Tuple[OverrideKey, ...]
26+
27+
28+
def map_environment_identity_to_context(
29+
environment: EnvironmentModel,
30+
identity: IdentityModel,
31+
override_traits: typing.Optional[typing.List[TraitModel]],
32+
) -> EvaluationContext:
33+
"""
34+
Map an EnvironmentModel and IdentityModel to an EvaluationContext.
35+
36+
:param environment: The environment model object.
37+
:param identity: The identity model object.
38+
:param override_traits: A list of TraitModel objects, to be used in place of `identity.identity_traits` if provided.
39+
:return: An EvaluationContext containing the environment and identity.
40+
"""
41+
features = map_feature_states_to_feature_contexts(environment.feature_states)
42+
segments: typing.Dict[str, SegmentContext] = {}
43+
for segment in environment.project.segments:
44+
segment_ctx_data: SegmentContext = {
45+
"key": str(segment.id),
46+
"name": segment.name,
47+
"rules": map_segment_rules_to_segment_context_rules(segment.rules),
48+
}
49+
if segment_feature_states := segment.feature_states:
50+
segment_ctx_data["overrides"] = list(
51+
map_feature_states_to_feature_contexts(segment_feature_states).values()
52+
)
53+
segments[segment.name] = segment_ctx_data
54+
# Concatenate feature states overriden for identities
55+
# to segment contexts
56+
features_to_identifiers: typing.Dict[
57+
OverridesKey,
58+
typing.List[str],
59+
] = defaultdict(list)
60+
for identity_override in (*environment.identity_overrides, identity):
61+
identity_features: typing.List[FeatureStateModel] = (
62+
identity_override.identity_features
63+
)
64+
if not identity_features:
65+
continue
66+
overrides_key = tuple(
67+
(
68+
str(feature_state.feature.id),
69+
feature_state.feature.name,
70+
feature_state.enabled,
71+
feature_state.feature_state_value,
72+
)
73+
for feature_state in sorted(identity_features, key=_get_name)
74+
)
75+
features_to_identifiers[overrides_key].append(identity_override.identifier)
76+
for overrides_key, identifiers in features_to_identifiers.items():
77+
segment_name = f"overrides_{abs(hash(overrides_key))}"
78+
segments[segment_name] = SegmentContext(
79+
key="", # Identity override segments never use % Split operator
80+
name=segment_name,
81+
rules=[
82+
{
83+
"type": "ALL",
84+
"rules": [
85+
{
86+
"type": "ALL",
87+
"conditions": [
88+
{
89+
"property": "$.identity.identifier",
90+
"operator": "IN",
91+
"value": ",".join(identifiers),
92+
}
93+
],
94+
}
95+
],
96+
}
97+
],
98+
overrides=[
99+
{
100+
"key": "", # Identity overrides never carry multivariate options
101+
"feature_key": feature_key,
102+
"name": feature_name,
103+
"enabled": feature_enabled,
104+
"value": feature_value,
105+
"priority": float("-inf"), # Highest possible priority
106+
}
107+
for feature_key, feature_name, feature_enabled, feature_value in overrides_key
108+
],
109+
)
110+
return {
111+
"environment": {
112+
"key": environment.api_key,
113+
"name": environment.name or "",
114+
},
115+
"identity": {
116+
"identifier": identity.identifier,
117+
"key": str(identity.django_id or identity.composite_key),
118+
"traits": {
119+
trait.trait_key: trait.trait_value
120+
for trait in (
121+
override_traits
122+
if override_traits is not None
123+
else identity.identity_traits
124+
)
125+
},
126+
},
127+
"features": features,
128+
"segments": segments,
129+
}
130+
131+
132+
def map_feature_states_to_feature_contexts(
133+
feature_states: typing.List[FeatureStateModel],
134+
) -> typing.Dict[str, FeatureContext]:
135+
"""
136+
Map feature states to feature contexts.
137+
138+
:param feature_states: A list of FeatureStateModel objects.
139+
:return: A dictionary mapping feature names to their contexts.
140+
"""
141+
features: typing.Dict[str, FeatureContext] = {}
142+
for feature_state in feature_states:
143+
feature_ctx_data: FeatureContext = {
144+
"key": str(feature_state.django_id or feature_state.featurestate_uuid),
145+
"feature_key": str(feature_state.feature.id),
146+
"name": feature_state.feature.name,
147+
"enabled": feature_state.enabled,
148+
"value": feature_state.feature_state_value,
149+
}
150+
multivariate_feature_state_values: typing.List[
151+
MultivariateFeatureStateValueModel
152+
]
153+
if (
154+
multivariate_feature_state_values
155+
:= feature_state.multivariate_feature_state_values
156+
):
157+
feature_ctx_data["variants"] = [
158+
{
159+
"value": multivariate_feature_state_value.multivariate_feature_option.value,
160+
"weight": multivariate_feature_state_value.percentage_allocation,
161+
}
162+
for multivariate_feature_state_value in sorted(
163+
multivariate_feature_state_values,
164+
key=_get_multivariate_feature_state_value_id,
165+
)
166+
]
167+
if feature_segment := feature_state.feature_segment:
168+
if (priority := feature_segment.priority) is not None:
169+
feature_ctx_data["priority"] = priority
170+
features[feature_state.feature.name] = feature_ctx_data
171+
return features
172+
173+
174+
def _get_multivariate_feature_state_value_id(
175+
multivariate_feature_state_value: MultivariateFeatureStateValueModel,
176+
) -> int:
177+
return (
178+
multivariate_feature_state_value.id
179+
or multivariate_feature_state_value.mv_fs_value_uuid.int
180+
)
181+
182+
183+
def map_segment_rules_to_segment_context_rules(
184+
rules: typing.List[SegmentRuleModel],
185+
) -> typing.List[SegmentRule]:
186+
"""
187+
Map segment rules to segment rules for the evaluation context.
188+
189+
:param rules: A list of SegmentRuleModel objects.
190+
:return: A list of SegmentRule objects.
191+
"""
192+
return [
193+
{
194+
"type": rule.type,
195+
"conditions": [
196+
{
197+
"property": condition.property_ or "",
198+
"operator": condition.operator,
199+
"value": condition.value or "",
200+
}
201+
for condition in rule.conditions
202+
],
203+
"rules": map_segment_rules_to_segment_context_rules(rule.rules),
204+
}
205+
for rule in rules
206+
]
207+
208+
209+
def _get_name(feature_state: FeatureStateModel) -> str:
210+
return feature_state.feature.name

0 commit comments

Comments
 (0)