Skip to content

Commit 08c17d2

Browse files
committed
feat!: Remove deprecated APIs, Pydantic models
1 parent 63b546c commit 08c17d2

Some content is hidden

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

44 files changed

+546
-2228
lines changed

flag_engine/context/mappers.py

Lines changed: 27 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,8 @@
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

227
OverrideKey = typing.Tuple[
238
str,
@@ -28,226 +13,34 @@
2813
OverridesKey = typing.Tuple[OverrideKey, ...]
2914

3015

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:
16+
def map_any_value_to_context_value(value: typing.Any) -> ContextValue:
3617
"""
37-
Map an EnvironmentModel and IdentityModel to an EvaluationContext.
18+
Try to coerce a value of arbitrary type to a trait value type.
19+
Union member-specific constraints, such as max string value length, are ignored here.
20+
Replicate behaviour from marshmallow/pydantic V1 for number-like strings.
21+
For decimals return an int in case of unset exponent.
22+
When in doubt, return string.
3823
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.
24+
Can be used as a `pydantic.BeforeValidator`.
4325
"""
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-
}
26+
if is_context_value(value):
27+
if isinstance(value, str):
28+
return _map_string_value_to_context_value(value)
29+
return value
30+
if isinstance(value, Decimal):
31+
if value.as_tuple().exponent:
32+
return float(str(value))
33+
return int(value)
34+
return str(value)
8335

8436

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

25140

252-
def _get_name(feature_state: FeatureStateModel) -> str:
253-
return feature_state.feature.name
41+
def _map_string_value_to_context_value(value: str) -> ContextValue:
42+
if _int_pattern.fullmatch(value):
43+
return int(value)
44+
if _float_pattern.fullmatch(value):
45+
return float(value)
46+
return value

0 commit comments

Comments
 (0)