Skip to content

Commit 53e55f6

Browse files
committed
feat: Context values for Segments
1 parent b931a04 commit 53e55f6

File tree

11 files changed

+511
-209
lines changed

11 files changed

+511
-209
lines changed

flag_engine/context/__init__.py

Whitespace-only changes.

flag_engine/context/mappers.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from flag_engine.context.types import EvaluationContext
2+
from flag_engine.environments.models import EnvironmentModel
3+
from flag_engine.identities.models import IdentityModel
4+
5+
6+
def map_environment_identity_to_context(
7+
environment: EnvironmentModel,
8+
identity: IdentityModel,
9+
) -> EvaluationContext:
10+
"""
11+
Maps an EnvironmentModel and IdentityModel to an EvaluationContext.
12+
13+
:param environment: The environment model object.
14+
:param identity: The identity model object.
15+
:return: An EvaluationContext containing the environment and identity.
16+
"""
17+
return {
18+
"environment": {
19+
"key": environment.api_key,
20+
"name": environment.name or "",
21+
},
22+
"identity": {
23+
"identifier": identity.identifier,
24+
"key": str(identity.django_id or identity.composite_key),
25+
"traits": {
26+
trait.trait_key: trait.trait_value for trait in identity.identity_traits
27+
},
28+
},
29+
}

flag_engine/context/types.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# generated by datamodel-codegen:
2+
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/chore/update-evaluation-context/sdk/evaluation-context.json # noqa: E501
3+
# timestamp: 2025-07-16T10:39:10+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Dict, Optional, TypedDict
8+
9+
from typing_extensions import NotRequired
10+
11+
from flag_engine.identities.traits.types import TraitValue
12+
from flag_engine.utils.types import SupportsStr
13+
14+
15+
class EnvironmentContext(TypedDict):
16+
key: str
17+
name: str
18+
19+
20+
class IdentityContext(TypedDict):
21+
identifier: str
22+
key: SupportsStr
23+
traits: NotRequired[Dict[str, TraitValue]]
24+
25+
26+
class EvaluationContext(TypedDict):
27+
environment: EnvironmentContext
28+
identity: NotRequired[Optional[IdentityContext]]

flag_engine/engine.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import typing
22

3+
from flag_engine.context.mappers import map_environment_identity_to_context
34
from flag_engine.environments.models import EnvironmentModel
45
from flag_engine.features.models import FeatureModel, FeatureStateModel
56
from flag_engine.identities.models import IdentityModel
67
from flag_engine.identities.traits.models import TraitModel
7-
from flag_engine.segments.evaluator import get_identity_segments
8+
from flag_engine.segments.evaluator import get_context_segments
89
from flag_engine.utils.exceptions import FeatureStateNotFound
910

1011

@@ -99,26 +100,43 @@ def _get_identity_feature_states_dict(
99100
override_traits: typing.Optional[typing.List[TraitModel]],
100101
) -> typing.Dict[FeatureModel, FeatureStateModel]:
101102
# Get feature states from the environment
102-
feature_states = {fs.feature: fs for fs in environment.feature_states}
103+
feature_states_by_feature = {fs.feature: fs for fs in environment.feature_states}
104+
105+
context = map_environment_identity_to_context(
106+
environment=environment,
107+
identity=identity,
108+
)
109+
if override_traits:
110+
if typing.TYPE_CHECKING:
111+
assert context["identity"]
112+
context["identity"].setdefault("traits", {}).update(
113+
{trait.trait_key: trait.trait_value for trait in override_traits}
114+
)
103115

104116
# Override with any feature states defined by matching segments
105-
identity_segments = get_identity_segments(environment, identity, override_traits)
106-
for matching_segment in identity_segments:
107-
for feature_state in matching_segment.feature_states:
108-
if feature_state.feature in feature_states:
109-
if feature_states[feature_state.feature].is_higher_segment_priority(
110-
feature_state
111-
):
112-
continue
113-
feature_states[feature_state.feature] = feature_state
117+
for context_segment in get_context_segments(
118+
context=context,
119+
segments=environment.project.segments,
120+
):
121+
for segment_feature_state in context_segment.feature_states:
122+
if (
123+
environment_feature_state := feature_states_by_feature.get(
124+
segment_feature := segment_feature_state.feature
125+
)
126+
) and environment_feature_state.is_higher_segment_priority(
127+
segment_feature_state
128+
):
129+
continue
130+
feature_states_by_feature[segment_feature] = segment_feature_state
114131

115132
# Override with any feature states defined directly the identity
116-
feature_states.update(
133+
feature_states_by_feature.update(
117134
{
118-
fs.feature: fs
119-
for fs in identity.identity_features
120-
if fs.feature in feature_states
135+
identity_feature: identity_feature_state
136+
for identity_feature_state in identity.identity_features
137+
if (identity_feature := identity_feature_state.feature)
138+
in feature_states_by_feature
121139
}
122140
)
123141

124-
return feature_states
142+
return feature_states_by_feature

0 commit comments

Comments
 (0)