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.
- {flagsmith_flag_engine-6.0.2/flagsmith_flag_engine.egg-info → flagsmith_flag_engine-6.1.0}/PKG-INFO +4 -2
- flagsmith_flag_engine-6.1.0/flag_engine/context/mappers.py +253 -0
- flagsmith_flag_engine-6.1.0/flag_engine/context/types.py +63 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/engine.py +45 -64
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/features/models.py +7 -68
- flagsmith_flag_engine-6.1.0/flag_engine/result/types.py +30 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/segments/constants.py +3 -0
- flagsmith_flag_engine-6.1.0/flag_engine/segments/evaluator.py +392 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/segments/models.py +2 -10
- flagsmith_flag_engine-6.1.0/flag_engine/segments/utils.py +17 -0
- flagsmith_flag_engine-6.1.0/flag_engine/utils/json/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0/flagsmith_flag_engine.egg-info}/PKG-INFO +4 -2
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flagsmith_flag_engine.egg-info/SOURCES.txt +3 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/setup.py +4 -2
- flagsmith_flag_engine-6.0.2/flag_engine/context/mappers.py +0 -39
- flagsmith_flag_engine-6.0.2/flag_engine/context/types.py +0 -28
- flagsmith_flag_engine-6.0.2/flag_engine/segments/evaluator.py +0 -255
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/LICENSE.txt +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/README.md +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/context/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/environments/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/environments/integrations/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/environments/integrations/models.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/environments/models.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/features/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/features/constants.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/models.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/traits/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/traits/constants.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/traits/models.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/identities/traits/types.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/organisations/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/organisations/models.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/projects/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/projects/models.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/py.typed +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/segments → flagsmith_flag_engine-6.1.0/flag_engine/result}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/types → flagsmith_flag_engine-6.1.0/flag_engine/segments}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/segments/types.py +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/utils → flagsmith_flag_engine-6.1.0/flag_engine/types}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/utils/json → flagsmith_flag_engine-6.1.0/flag_engine/utils}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/datetime.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/exceptions.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/hashing.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/json/encoders.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/semver.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flag_engine/utils/types.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flagsmith_flag_engine.egg-info/dependency_links.txt +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flagsmith_flag_engine.egg-info/requires.txt +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/flagsmith_flag_engine.egg-info/top_level.txt +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-6.1.0}/setup.cfg +0 -0
{flagsmith_flag_engine-6.0.2/flagsmith_flag_engine.egg-info → flagsmith_flag_engine-6.1.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flagsmith-flag-engine
|
|
3
|
-
Version: 6.0
|
|
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
|
|
4
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
warnings.warn(
|
|
51
|
+
"`get_environment_feature_state` is deprecated, use `get_evaluation_result` instead.",
|
|
52
|
+
DeprecationWarning,
|
|
53
|
+
)
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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]
|