flagsmith-flag-engine 6.0.2__tar.gz → 6.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {flagsmith_flag_engine-6.0.2/flagsmith_flag_engine.egg-info → flagsmith_flag_engine-6.1.0}/PKG-INFO +4 -2
  2. flagsmith_flag_engine-6.1.0/flag_engine/context/mappers.py +253 -0
  3. flagsmith_flag_engine-6.1.0/flag_engine/context/types.py +63 -0
  4. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/engine.py +45 -64
  5. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/features/models.py +7 -68
  6. flagsmith_flag_engine-6.1.0/flag_engine/result/types.py +30 -0
  7. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/segments/constants.py +3 -0
  8. flagsmith_flag_engine-6.1.0/flag_engine/segments/evaluator.py +392 -0
  9. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/segments/models.py +2 -10
  10. flagsmith_flag_engine-6.1.0/flag_engine/segments/utils.py +17 -0
  11. flagsmith_flag_engine-6.1.0/flag_engine/utils/json/__init__.py +0 -0
  12. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0/flagsmith_flag_engine.egg-info}/PKG-INFO +4 -2
  13. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flagsmith_flag_engine.egg-info/SOURCES.txt +3 -0
  14. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/setup.py +4 -2
  15. flagsmith_flag_engine-6.0.2/flag_engine/context/mappers.py +0 -39
  16. flagsmith_flag_engine-6.0.2/flag_engine/context/types.py +0 -28
  17. flagsmith_flag_engine-6.0.2/flag_engine/segments/evaluator.py +0 -255
  18. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/LICENSE.txt +0 -0
  19. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/README.md +0 -0
  20. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/__init__.py +0 -0
  21. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/context/__init__.py +0 -0
  22. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/environments/__init__.py +0 -0
  23. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/environments/integrations/__init__.py +0 -0
  24. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/environments/integrations/models.py +0 -0
  25. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/environments/models.py +0 -0
  26. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/features/__init__.py +0 -0
  27. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/features/constants.py +0 -0
  28. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/__init__.py +0 -0
  29. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/models.py +0 -0
  30. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/traits/__init__.py +0 -0
  31. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/traits/constants.py +0 -0
  32. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/traits/models.py +0 -0
  33. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/traits/types.py +0 -0
  34. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/organisations/__init__.py +0 -0
  35. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/organisations/models.py +0 -0
  36. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/projects/__init__.py +0 -0
  37. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/projects/models.py +0 -0
  38. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/py.typed +0 -0
  39. {flagsmith_flag_engine-6.0.2/flag_engine/segments → flagsmith_flag_engine-6.1.0/flag_engine/result}/__init__.py +0 -0
  40. {flagsmith_flag_engine-6.0.2/flag_engine/types → flagsmith_flag_engine-6.1.0/flag_engine/segments}/__init__.py +0 -0
  41. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/segments/types.py +0 -0
  42. {flagsmith_flag_engine-6.0.2/flag_engine/utils → flagsmith_flag_engine-6.1.0/flag_engine/types}/__init__.py +0 -0
  43. {flagsmith_flag_engine-6.0.2/flag_engine/utils/json → flagsmith_flag_engine-6.1.0/flag_engine/utils}/__init__.py +0 -0
  44. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/datetime.py +0 -0
  45. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/exceptions.py +0 -0
  46. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/hashing.py +0 -0
  47. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/json/encoders.py +0 -0
  48. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/semver.py +0 -0
  49. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/types.py +0 -0
  50. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flagsmith_flag_engine.egg-info/dependency_links.txt +0 -0
  51. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flagsmith_flag_engine.egg-info/requires.txt +0 -0
  52. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flagsmith_flag_engine.egg-info/top_level.txt +0 -0
  53. {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flagsmith-flag-engine
3
- Version: 6.0.2
3
+ Version: 6.1.0
4
4
  Summary: Flag engine for the Flagsmith API.
5
5
  Home-page: https://github.com/Flagsmith/flagsmith-engine
6
6
  Author: Flagsmith
@@ -8,9 +8,11 @@ Author-email: support@flagsmith.com
8
8
  License: BSD3
9
9
  Classifier: License :: OSI Approved :: BSD License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.7
12
11
  Classifier: Programming Language :: Python :: 3.8
13
12
  Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
14
16
  Description-Content-Type: text/markdown
15
17
  License-File: LICENSE.txt
16
18
  Requires-Dist: pydantic<3,>=2.3.0
@@ -0,0 +1,253 @@
1
+ import json
2
+ import typing
3
+ from collections import defaultdict
4
+
5
+ from flag_engine.context.types import (
6
+ EvaluationContext,
7
+ FeatureContext,
8
+ SegmentContext,
9
+ SegmentRule,
10
+ )
11
+ from flag_engine.environments.models import EnvironmentModel
12
+ from flag_engine.features.models import (
13
+ FeatureModel,
14
+ FeatureStateModel,
15
+ MultivariateFeatureStateValueModel,
16
+ )
17
+ from flag_engine.identities.models import IdentityModel
18
+ from flag_engine.identities.traits.models import TraitModel
19
+ from flag_engine.result.types import FlagResult
20
+ from flag_engine.segments.models import SegmentRuleModel
21
+
22
+ OverrideKey = typing.Tuple[
23
+ str,
24
+ str,
25
+ bool,
26
+ typing.Any,
27
+ ]
28
+ OverridesKey = typing.Tuple[OverrideKey, ...]
29
+
30
+
31
+ def map_environment_identity_to_context(
32
+ environment: EnvironmentModel,
33
+ identity: typing.Optional[IdentityModel],
34
+ override_traits: typing.Optional[typing.List[TraitModel]],
35
+ ) -> EvaluationContext:
36
+ """
37
+ Map an EnvironmentModel and IdentityModel to an EvaluationContext.
38
+
39
+ :param environment: The environment model object.
40
+ :param identity: The identity model object.
41
+ :param override_traits: A list of TraitModel objects, to be used in place of `identity.identity_traits` if provided.
42
+ :return: An EvaluationContext containing the environment and identity.
43
+ """
44
+ features = _map_feature_states_to_feature_contexts(environment.feature_states)
45
+ segments: typing.Dict[str, SegmentContext] = {}
46
+ for segment in environment.project.segments:
47
+ segment_ctx_data: SegmentContext = {
48
+ "key": str(segment.id),
49
+ "name": segment.name,
50
+ "rules": _map_segment_rules_to_segment_context_rules(segment.rules),
51
+ }
52
+ if segment_feature_states := segment.feature_states:
53
+ segment_ctx_data["overrides"] = list(
54
+ _map_feature_states_to_feature_contexts(segment_feature_states).values()
55
+ )
56
+ segments[str(segment.id)] = segment_ctx_data
57
+ identity_overrides = environment.identity_overrides + [identity] if identity else []
58
+ segments.update(_map_identity_overrides_to_segment_contexts(identity_overrides))
59
+ return {
60
+ "environment": {
61
+ "key": environment.api_key,
62
+ "name": environment.name or "",
63
+ },
64
+ "identity": (
65
+ {
66
+ "identifier": identity.identifier,
67
+ "key": str(identity.django_id or identity.composite_key),
68
+ "traits": {
69
+ trait.trait_key: trait.trait_value
70
+ for trait in (
71
+ override_traits
72
+ if override_traits is not None
73
+ else identity.identity_traits
74
+ )
75
+ },
76
+ }
77
+ if identity
78
+ else None
79
+ ),
80
+ "features": features,
81
+ "segments": segments,
82
+ }
83
+
84
+
85
+ def _map_identity_overrides_to_segment_contexts(
86
+ identity_overrides: typing.List[IdentityModel],
87
+ ) -> typing.Dict[str, SegmentContext]:
88
+ """
89
+ Map identity overrides to segment contexts.
90
+
91
+ :param identity_overrides: A list of IdentityModel objects.
92
+ :return: A dictionary mapping segment ids to SegmentContext objects.
93
+ """
94
+ features_to_identifiers: typing.Dict[
95
+ OverridesKey,
96
+ typing.List[str],
97
+ ] = defaultdict(list)
98
+ for identity_override in identity_overrides:
99
+ identity_features: typing.List[FeatureStateModel] = (
100
+ identity_override.identity_features
101
+ )
102
+ if not identity_features:
103
+ continue
104
+ overrides_key = tuple(
105
+ (
106
+ str(feature_state.feature.id),
107
+ feature_state.feature.name,
108
+ feature_state.enabled,
109
+ feature_state.feature_state_value,
110
+ )
111
+ for feature_state in sorted(identity_features, key=_get_name)
112
+ )
113
+ features_to_identifiers[overrides_key].append(identity_override.identifier)
114
+ segment_contexts: typing.Dict[str, SegmentContext] = {}
115
+ for overrides_key, identifiers in features_to_identifiers.items():
116
+ # Create a segment context for each unique set of overrides
117
+ # Generate a unique key to avoid collisions
118
+ segment_key = str(hash(overrides_key))
119
+ segment_contexts[segment_key] = SegmentContext(
120
+ key="", # Identity override segments never use % Split operator
121
+ name="identity_overrides",
122
+ rules=[
123
+ {
124
+ "type": "ALL",
125
+ "conditions": [
126
+ {
127
+ "property": "$.identity.identifier",
128
+ "operator": "IN",
129
+ "value": json.dumps(identifiers),
130
+ }
131
+ ],
132
+ }
133
+ ],
134
+ overrides=[
135
+ {
136
+ "key": "", # Identity overrides never carry multivariate options
137
+ "feature_key": feature_key,
138
+ "name": feature_name,
139
+ "enabled": feature_enabled,
140
+ "value": feature_value,
141
+ "priority": float("-inf"), # Highest possible priority
142
+ }
143
+ for feature_key, feature_name, feature_enabled, feature_value in overrides_key
144
+ ],
145
+ )
146
+ return segment_contexts
147
+
148
+
149
+ def _map_feature_states_to_feature_contexts(
150
+ feature_states: typing.List[FeatureStateModel],
151
+ ) -> typing.Dict[str, FeatureContext]:
152
+ """
153
+ Map feature states to feature contexts.
154
+
155
+ :param feature_states: A list of FeatureStateModel objects.
156
+ :return: A dictionary mapping feature names to their contexts.
157
+ """
158
+ features: typing.Dict[str, FeatureContext] = {}
159
+ for feature_state in feature_states:
160
+ feature_context: FeatureContext = {
161
+ "key": str(feature_state.django_id or feature_state.featurestate_uuid),
162
+ "feature_key": str(feature_state.feature.id),
163
+ "name": feature_state.feature.name,
164
+ "enabled": feature_state.enabled,
165
+ "value": feature_state.feature_state_value,
166
+ }
167
+ multivariate_feature_state_values: typing.List[
168
+ MultivariateFeatureStateValueModel
169
+ ]
170
+ if (
171
+ multivariate_feature_state_values := feature_state.multivariate_feature_state_values
172
+ ):
173
+ feature_context["variants"] = [
174
+ {
175
+ "value": multivariate_feature_state_value.multivariate_feature_option.value,
176
+ "weight": multivariate_feature_state_value.percentage_allocation,
177
+ }
178
+ for multivariate_feature_state_value in sorted(
179
+ multivariate_feature_state_values,
180
+ key=_get_multivariate_feature_state_value_id,
181
+ )
182
+ ]
183
+ if feature_segment := feature_state.feature_segment:
184
+ if (priority := feature_segment.priority) is not None:
185
+ feature_context["priority"] = priority
186
+ features[feature_state.feature.name] = feature_context
187
+ return features
188
+
189
+
190
+ def _map_segment_rules_to_segment_context_rules(
191
+ rules: typing.List[SegmentRuleModel],
192
+ ) -> typing.List[SegmentRule]:
193
+ """
194
+ Map segment rules to segment rules for the evaluation context.
195
+
196
+ :param rules: A list of SegmentRuleModel objects.
197
+ :return: A list of SegmentRule objects.
198
+ """
199
+ return [
200
+ {
201
+ "type": rule.type,
202
+ "conditions": [
203
+ {
204
+ "property": condition.property_ or "",
205
+ "operator": condition.operator,
206
+ "value": condition.value or "",
207
+ }
208
+ for condition in rule.conditions
209
+ ],
210
+ "rules": _map_segment_rules_to_segment_context_rules(rule.rules),
211
+ }
212
+ for rule in rules
213
+ ]
214
+
215
+
216
+ def map_flag_results_to_feature_states(
217
+ flag_results: typing.List[FlagResult],
218
+ ) -> typing.List[FeatureStateModel]:
219
+ """
220
+ Map flag results to feature states.
221
+
222
+ :param flag_results: A list of FlagResult objects.
223
+ :return: A list of FeatureStateModel objects.
224
+ """
225
+ return [
226
+ FeatureStateModel(
227
+ feature=FeatureModel(
228
+ id=flag_result["feature_key"],
229
+ name=flag_result["name"],
230
+ type=(
231
+ "MULTIVARIATE"
232
+ if flag_result.get("reason", "").startswith("SPLIT")
233
+ else "STANDARD"
234
+ ),
235
+ ),
236
+ enabled=flag_result["enabled"],
237
+ feature_state_value=flag_result["value"],
238
+ )
239
+ for flag_result in flag_results
240
+ ]
241
+
242
+
243
+ def _get_multivariate_feature_state_value_id(
244
+ multivariate_feature_state_value: MultivariateFeatureStateValueModel,
245
+ ) -> int:
246
+ return (
247
+ multivariate_feature_state_value.id
248
+ or multivariate_feature_state_value.mv_fs_value_uuid.int
249
+ )
250
+
251
+
252
+ def _get_name(feature_state: FeatureStateModel) -> str:
253
+ return feature_state.feature.name
@@ -0,0 +1,63 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/chore/features-contexts-in-eval-context-schema/sdk/evaluation-context.json # noqa: E501
3
+ # timestamp: 2025-08-11T18:17:29+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Dict, List, Optional, TypedDict, Union
8
+
9
+ from typing_extensions import NotRequired
10
+
11
+ from flag_engine.segments.types import ConditionOperator, RuleType
12
+
13
+
14
+ class EnvironmentContext(TypedDict):
15
+ key: str
16
+ name: str
17
+
18
+
19
+ class FeatureValue(TypedDict):
20
+ value: Any
21
+ weight: float
22
+
23
+
24
+ class IdentityContext(TypedDict):
25
+ identifier: str
26
+ key: str
27
+ traits: NotRequired[Dict[str, Optional[Union[str, float, bool]]]]
28
+
29
+
30
+ class SegmentCondition(TypedDict):
31
+ property: NotRequired[str]
32
+ operator: ConditionOperator
33
+ value: str
34
+
35
+
36
+ class SegmentRule(TypedDict):
37
+ type: RuleType
38
+ conditions: NotRequired[List[SegmentCondition]]
39
+ rules: NotRequired[List[SegmentRule]]
40
+
41
+
42
+ class FeatureContext(TypedDict):
43
+ key: str
44
+ feature_key: str
45
+ name: str
46
+ enabled: bool
47
+ value: Any
48
+ variants: NotRequired[List[FeatureValue]]
49
+ priority: NotRequired[float]
50
+
51
+
52
+ class SegmentContext(TypedDict):
53
+ key: str
54
+ name: str
55
+ rules: List[SegmentRule]
56
+ overrides: NotRequired[List[FeatureContext]]
57
+
58
+
59
+ class EvaluationContext(TypedDict):
60
+ environment: EnvironmentContext
61
+ identity: NotRequired[Optional[IdentityContext]]
62
+ segments: NotRequired[Dict[str, SegmentContext]]
63
+ features: NotRequired[Dict[str, FeatureContext]]
@@ -1,14 +1,25 @@
1
1
  import typing
2
+ import warnings
2
3
 
3
- from flag_engine.context.mappers import map_environment_identity_to_context
4
- from flag_engine.context.types import EvaluationContext
4
+ from flag_engine.context.mappers import (
5
+ map_environment_identity_to_context,
6
+ map_flag_results_to_feature_states,
7
+ )
5
8
  from flag_engine.environments.models import EnvironmentModel
6
- from flag_engine.features.models import FeatureModel, FeatureStateModel
9
+ from flag_engine.features.models import FeatureStateModel
7
10
  from flag_engine.identities.models import IdentityModel
8
11
  from flag_engine.identities.traits.models import TraitModel
9
- from flag_engine.segments.evaluator import get_context_segments
12
+ from flag_engine.segments.evaluator import get_evaluation_result
10
13
  from flag_engine.utils.exceptions import FeatureStateNotFound
11
14
 
15
+ __all__ = (
16
+ "get_environment_feature_states",
17
+ "get_environment_feature_state",
18
+ "get_identity_feature_states",
19
+ "get_identity_feature_state",
20
+ "get_evaluation_result",
21
+ )
22
+
12
23
 
13
24
  def get_environment_feature_states(
14
25
  environment: EnvironmentModel,
@@ -18,6 +29,10 @@ def get_environment_feature_states(
18
29
 
19
30
  :param environment: the environment model object
20
31
  """
32
+ warnings.warn(
33
+ "`get_environment_feature_states` is deprecated, use `get_evaluation_result` instead.",
34
+ DeprecationWarning,
35
+ )
21
36
  if environment.get_hide_disabled_flags():
22
37
  return [fs for fs in environment.feature_states if fs.enabled]
23
38
  return environment.feature_states
@@ -32,13 +47,16 @@ def get_environment_feature_state(
32
47
  :param environment: the environment model object
33
48
  :param feature_name: the name of the feature to get the feature state for
34
49
  """
35
- try:
36
- return next(
37
- filter(lambda f: f.feature.name == feature_name, environment.feature_states)
38
- )
50
+ warnings.warn(
51
+ "`get_environment_feature_state` is deprecated, use `get_evaluation_result` instead.",
52
+ DeprecationWarning,
53
+ )
39
54
 
40
- except StopIteration:
41
- raise FeatureStateNotFound()
55
+ for feature_state in environment.feature_states:
56
+ if feature_state.feature.name == feature_name:
57
+ return feature_state
58
+
59
+ raise FeatureStateNotFound()
42
60
 
43
61
 
44
62
  def get_identity_feature_states(
@@ -55,19 +73,20 @@ def get_identity_feature_states(
55
73
  :return: list of feature state models based on the environment, any matching
56
74
  segments and any specific identity overrides
57
75
  """
76
+ warnings.warn(
77
+ "`get_identity_feature_states` is deprecated, use `get_evaluation_result` instead.",
78
+ DeprecationWarning,
79
+ )
58
80
  context = map_environment_identity_to_context(
59
81
  environment=environment,
60
82
  identity=identity,
61
83
  override_traits=override_traits,
62
84
  )
63
85
 
64
- feature_states = list(
65
- _get_identity_feature_states_dict(
66
- environment=environment,
67
- identity=identity,
68
- context=context,
69
- ).values()
70
- )
86
+ result = get_evaluation_result(context)
87
+
88
+ feature_states = map_flag_results_to_feature_states(result["flags"])
89
+
71
90
  if environment.get_hide_disabled_flags():
72
91
  return [fs for fs in feature_states if fs.enabled]
73
92
  return feature_states
@@ -89,58 +108,20 @@ def get_identity_feature_state(
89
108
  :return: feature state model based on the environment, any matching
90
109
  segments and any specific identity overrides
91
110
  """
111
+ warnings.warn(
112
+ "`get_identity_feature_state` is deprecated, use `get_evaluation_result` instead.",
113
+ DeprecationWarning,
114
+ )
92
115
  context = map_environment_identity_to_context(
93
116
  environment=environment,
94
117
  identity=identity,
95
118
  override_traits=override_traits,
96
119
  )
97
120
 
98
- feature_states = _get_identity_feature_states_dict(
99
- environment=environment,
100
- identity=identity,
101
- context=context,
102
- )
103
- matching_feature = next(
104
- filter(lambda feature: feature.name == feature_name, feature_states.keys()),
105
- None,
106
- )
107
-
108
- if not matching_feature:
109
- raise FeatureStateNotFound()
110
-
111
- return feature_states[matching_feature]
121
+ result = get_evaluation_result(context)
112
122
 
123
+ for feature_state in map_flag_results_to_feature_states(result["flags"]):
124
+ if feature_state.feature.name == feature_name:
125
+ return feature_state
113
126
 
114
- def _get_identity_feature_states_dict(
115
- environment: EnvironmentModel,
116
- identity: IdentityModel,
117
- context: EvaluationContext,
118
- ) -> typing.Dict[FeatureModel, FeatureStateModel]:
119
- # Get feature states from the environment
120
- feature_states_by_feature = {fs.feature: fs for fs in environment.feature_states}
121
-
122
- # Override with any feature states defined by matching segments
123
- for context_segment in get_context_segments(
124
- context=context,
125
- segments=environment.project.segments,
126
- ):
127
- for segment_feature_state in context_segment.feature_states:
128
- if (
129
- feature_state := feature_states_by_feature.get(
130
- segment_feature := segment_feature_state.feature
131
- )
132
- ) and feature_state.is_higher_segment_priority(segment_feature_state):
133
- continue
134
- feature_states_by_feature[segment_feature] = segment_feature_state
135
-
136
- # Override with any feature states defined directly the identity
137
- feature_states_by_feature.update(
138
- {
139
- identity_feature: identity_feature_state
140
- for identity_feature_state in identity.identity_features
141
- if (identity_feature := identity_feature_state.feature)
142
- in feature_states_by_feature
143
- }
144
- )
145
-
146
- return feature_states_by_feature
127
+ raise FeatureStateNotFound()
@@ -1,14 +1,13 @@
1
- import math
2
1
  import typing
3
2
  import uuid
3
+ import warnings
4
4
 
5
- from annotated_types import Ge, Le, SupportsLt
5
+ from annotated_types import Ge, Le
6
6
  from pydantic import UUID4, BaseModel, Field, model_validator
7
7
  from pydantic_collections import BaseCollectionModel
8
8
  from typing_extensions import Annotated
9
9
 
10
10
  from flag_engine.utils.exceptions import InvalidPercentageAllocation
11
- from flag_engine.utils.hashing import get_hashed_percentage_for_object_ids
12
11
 
13
12
 
14
13
  class FeatureModel(BaseModel):
@@ -19,9 +18,6 @@ class FeatureModel(BaseModel):
19
18
  def __eq__(self, other: object) -> bool:
20
19
  return isinstance(other, FeatureModel) and self.id == other.id
21
20
 
22
- def __hash__(self) -> int:
23
- return hash(self.id)
24
-
25
21
 
26
22
  class MultivariateFeatureOptionModel(BaseModel):
27
23
  value: typing.Any
@@ -88,72 +84,15 @@ class FeatureStateModel(BaseModel, validate_assignment=True):
88
84
 
89
85
  def get_value(self, identity_id: typing.Union[None, int, str] = None) -> typing.Any:
90
86
  """
91
- Get the value of the feature state.
87
+ DEPRECATED: Get the value of the feature state.
88
+ Use the `feature_state_value` field directly instead.
92
89
 
93
90
  :param identity_id: a unique identifier for the identity, can be either a
94
91
  numeric id or a string but must be unique for the identity.
95
92
  :return: the value of the feature state.
96
93
  """
97
- if identity_id and len(self.multivariate_feature_state_values) > 0:
98
- return self._get_multivariate_value(identity_id)
99
- return self.feature_state_value
100
-
101
- def is_higher_segment_priority(self, other: "FeatureStateModel") -> bool:
102
- """
103
- Returns `True` if `self` is higher segment priority than `other`
104
- (i.e. has lower value for feature_segment.priority)
105
-
106
- NOTE:
107
- A segment will be considered higher priority only if:
108
- 1. `other` does not have a feature segment(i.e: it is an environment feature state or it's a
109
- feature state with feature segment but from an old document that does not have `feature_segment.priority`)
110
- but `self` does.
111
-
112
- 2. `other` have a feature segment with high priority
113
-
114
- """
115
-
116
- if other_feature_segment := other.feature_segment:
117
- if (
118
- other_feature_segment_priority := other_feature_segment.priority
119
- ) is not None:
120
- return (
121
- getattr(
122
- self.feature_segment,
123
- "priority",
124
- math.inf,
125
- )
126
- < other_feature_segment_priority
127
- )
128
- return False
129
-
130
- def _get_multivariate_value(
131
- self, identity_id: typing.Union[int, str]
132
- ) -> typing.Any:
133
- percentage_value = get_hashed_percentage_for_object_ids(
134
- [self.django_id or self.featurestate_uuid, identity_id]
94
+ warnings.warn(
95
+ "get_value is deprecated, use feature_state_value directly.",
96
+ DeprecationWarning,
135
97
  )
136
-
137
- # Iterate over the mv options in order of id (so we get the same value each
138
- # time) to determine the correct value to return to the identity based on
139
- # the percentage allocations of the multivariate options. This gives us a
140
- # way to ensure that the same value is returned every time we use the same
141
- # percentage value.
142
- start_percentage = 0.0
143
-
144
- def _mv_fs_sort_key(mv_value: MultivariateFeatureStateValueModel) -> SupportsLt:
145
- return mv_value.id or mv_value.mv_fs_value_uuid
146
-
147
- for mv_value in sorted(
148
- self.multivariate_feature_state_values,
149
- key=_mv_fs_sort_key,
150
- ):
151
- limit = mv_value.percentage_allocation + start_percentage
152
- if start_percentage <= percentage_value < limit:
153
- return mv_value.multivariate_feature_option.value
154
-
155
- start_percentage = limit
156
-
157
- # default to return the control value if no MV values found, although this
158
- # should never happen
159
98
  return self.feature_state_value
@@ -0,0 +1,30 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: result.json
3
+ # timestamp: 2025-08-11T11:47:46+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, List, Optional, TypedDict
8
+
9
+ from typing_extensions import NotRequired
10
+
11
+ from flag_engine.context.types import EvaluationContext
12
+
13
+
14
+ class FlagResult(TypedDict):
15
+ name: str
16
+ feature_key: str
17
+ enabled: bool
18
+ value: NotRequired[Optional[Any]]
19
+ reason: NotRequired[str]
20
+
21
+
22
+ class SegmentResult(TypedDict):
23
+ key: str
24
+ name: str
25
+
26
+
27
+ class EvaluationResult(TypedDict):
28
+ context: EvaluationContext
29
+ flags: List[FlagResult]
30
+ segments: List[SegmentResult]
@@ -20,3 +20,6 @@ MODULO: ConditionOperator = "MODULO"
20
20
  IS_SET: ConditionOperator = "IS_SET"
21
21
  IS_NOT_SET: ConditionOperator = "IS_NOT_SET"
22
22
  IN: ConditionOperator = "IN"
23
+
24
+ # Lowest possible priority for segment overrides
25
+ DEFAULT_PRIORITY = float("inf")