Skip to content

Commit 92de872

Browse files
authored
feat(v7)!: Remove deprecated APIs, Pydantic models (#250)
1 parent f99fa99 commit 92de872

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+616
-2267
lines changed

flag_engine/context/mappers.py

Lines changed: 27 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -1,253 +1,38 @@
1-
import json
1+
import re
22
import typing
3-
from collections import defaultdict
3+
from decimal import Decimal
44

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
5+
from flag_engine.segments.types import ContextValue, is_context_value
216

22-
OverrideKey = typing.Tuple[
23-
str,
24-
str,
25-
bool,
26-
typing.Any,
27-
]
28-
OverridesKey = typing.Tuple[OverrideKey, ...]
297

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]:
8+
def map_any_value_to_context_value(value: typing.Any) -> ContextValue:
2199
"""
220-
Map flag results to feature states.
10+
Try to coerce a value of arbitrary type to a context value type.
11+
Union member-specific constraints, such as max string value length, are ignored here.
12+
Replicate behaviour from marshmallow/pydantic V1 for number-like strings.
13+
For decimals return an int in case of unset exponent.
14+
When in doubt, return string.
22115
222-
:param flag_results: A list of FlagResult objects.
223-
:return: A list of FeatureStateModel objects.
16+
Can be used as a `pydantic.BeforeValidator`.
22417
"""
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-
]
18+
if is_context_value(value):
19+
if isinstance(value, str):
20+
return _map_string_value_to_context_value(value)
21+
return value
22+
if isinstance(value, Decimal):
23+
if value.as_tuple().exponent:
24+
return float(str(value))
25+
return int(value)
26+
return str(value)
24127

24228

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-
)
29+
_int_pattern = re.compile(r"-?[0-9]+")
30+
_float_pattern = re.compile(r"-?[0-9]+\.[0-9]+")
25031

25132

252-
def _get_name(feature_state: FeatureStateModel) -> str:
253-
return feature_state.feature.name
33+
def _map_string_value_to_context_value(value: str) -> ContextValue:
34+
if _int_pattern.fullmatch(value):
35+
return int(value)
36+
if _float_pattern.fullmatch(value):
37+
return float(value)
38+
return value

flag_engine/context/types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from typing_extensions import NotRequired
1010

11-
from flag_engine.segments.types import ConditionOperator, RuleType
11+
from flag_engine.segments.types import ConditionOperator, ContextValue, RuleType
1212

1313

1414
class EnvironmentContext(TypedDict):
@@ -24,7 +24,7 @@ class FeatureValue(TypedDict):
2424
class IdentityContext(TypedDict):
2525
identifier: str
2626
key: str
27-
traits: NotRequired[Dict[str, Optional[Union[str, float, bool]]]]
27+
traits: NotRequired[Dict[str, ContextValue]]
2828

2929

3030
class StrValueSegmentCondition(TypedDict):

0 commit comments

Comments
 (0)