flagsmith-flag-engine 6.0.2__tar.gz → 7.0.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-7.0.0}/PKG-INFO +8 -6
- flagsmith_flag_engine-7.0.0/flag_engine/context/mappers.py +38 -0
- flagsmith_flag_engine-7.0.0/flag_engine/context/types.py +72 -0
- flagsmith_flag_engine-7.0.0/flag_engine/engine.py +11 -0
- flagsmith_flag_engine-7.0.0/flag_engine/result/types.py +30 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flag_engine/segments/constants.py +3 -0
- flagsmith_flag_engine-7.0.0/flag_engine/segments/evaluator.py +374 -0
- flagsmith_flag_engine-7.0.0/flag_engine/segments/types.py +38 -0
- flagsmith_flag_engine-7.0.0/flag_engine/segments/utils.py +24 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flag_engine/utils/types.py +1 -1
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0/flagsmith_flag_engine.egg-info}/PKG-INFO +8 -6
- flagsmith_flag_engine-7.0.0/flagsmith_flag_engine.egg-info/SOURCES.txt +29 -0
- flagsmith_flag_engine-7.0.0/flagsmith_flag_engine.egg-info/requires.txt +3 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/setup.py +8 -6
- 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/engine.py +0 -146
- flagsmith_flag_engine-6.0.2/flag_engine/environments/integrations/models.py +0 -9
- flagsmith_flag_engine-6.0.2/flag_engine/environments/models.py +0 -95
- flagsmith_flag_engine-6.0.2/flag_engine/features/constants.py +0 -3
- flagsmith_flag_engine-6.0.2/flag_engine/features/models.py +0 -159
- flagsmith_flag_engine-6.0.2/flag_engine/identities/models.py +0 -93
- flagsmith_flag_engine-6.0.2/flag_engine/identities/traits/constants.py +0 -1
- flagsmith_flag_engine-6.0.2/flag_engine/identities/traits/models.py +0 -8
- flagsmith_flag_engine-6.0.2/flag_engine/identities/traits/types.py +0 -62
- flagsmith_flag_engine-6.0.2/flag_engine/organisations/__init__.py +0 -0
- flagsmith_flag_engine-6.0.2/flag_engine/organisations/models.py +0 -13
- flagsmith_flag_engine-6.0.2/flag_engine/projects/__init__.py +0 -0
- flagsmith_flag_engine-6.0.2/flag_engine/projects/models.py +0 -16
- flagsmith_flag_engine-6.0.2/flag_engine/segments/__init__.py +0 -0
- flagsmith_flag_engine-6.0.2/flag_engine/segments/evaluator.py +0 -255
- flagsmith_flag_engine-6.0.2/flag_engine/segments/models.py +0 -41
- flagsmith_flag_engine-6.0.2/flag_engine/segments/types.py +0 -24
- flagsmith_flag_engine-6.0.2/flag_engine/types/__init__.py +0 -0
- flagsmith_flag_engine-6.0.2/flag_engine/utils/__init__.py +0 -0
- flagsmith_flag_engine-6.0.2/flag_engine/utils/datetime.py +0 -5
- flagsmith_flag_engine-6.0.2/flag_engine/utils/exceptions.py +0 -10
- flagsmith_flag_engine-6.0.2/flag_engine/utils/json/__init__.py +0 -0
- flagsmith_flag_engine-6.0.2/flagsmith_flag_engine.egg-info/SOURCES.txt +0 -46
- flagsmith_flag_engine-6.0.2/flagsmith_flag_engine.egg-info/requires.txt +0 -3
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/LICENSE.txt +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/README.md +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flag_engine/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flag_engine/context/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flag_engine/py.typed +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/environments → flagsmith_flag_engine-7.0.0/flag_engine/result}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/environments/integrations → flagsmith_flag_engine-7.0.0/flag_engine/segments}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/features → flagsmith_flag_engine-7.0.0/flag_engine/types}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/identities → flagsmith_flag_engine-7.0.0/flag_engine/utils}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flag_engine/utils/hashing.py +0 -0
- {flagsmith_flag_engine-6.0.2/flag_engine/identities/traits → flagsmith_flag_engine-7.0.0/flag_engine/utils/json}/__init__.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flag_engine/utils/json/encoders.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flag_engine/utils/semver.py +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flagsmith_flag_engine.egg-info/dependency_links.txt +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/flagsmith_flag_engine.egg-info/top_level.txt +0 -0
- {flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0}/setup.cfg +0 -0
{flagsmith_flag_engine-6.0.2/flagsmith_flag_engine.egg-info → flagsmith_flag_engine-7.0.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flagsmith-flag-engine
|
|
3
|
-
Version:
|
|
3
|
+
Version: 7.0.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,14 +8,16 @@ 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
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
13
11
|
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
16
|
Description-Content-Type: text/markdown
|
|
15
17
|
License-File: LICENSE.txt
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: jsonpath-rfc9535<1,>=0.1.5
|
|
19
|
+
Requires-Dist: semver<4,>=3.0.4
|
|
20
|
+
Requires-Dist: typing-extensions<5,>=4.14.1
|
|
19
21
|
Dynamic: author
|
|
20
22
|
Dynamic: author-email
|
|
21
23
|
Dynamic: classifier
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import typing
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from flag_engine.segments.types import ContextValue, is_context_value
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def map_any_value_to_context_value(value: typing.Any) -> ContextValue:
|
|
9
|
+
"""
|
|
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.
|
|
15
|
+
|
|
16
|
+
Can be used as a `pydantic.BeforeValidator`.
|
|
17
|
+
"""
|
|
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)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_int_pattern = re.compile(r"-?[0-9]+")
|
|
30
|
+
_float_pattern = re.compile(r"-?[0-9]+\.[0-9]+")
|
|
31
|
+
|
|
32
|
+
|
|
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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json # noqa: E501
|
|
3
|
+
# timestamp: 2025-08-25T11:10:31+00:00
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
|
8
|
+
|
|
9
|
+
from typing_extensions import NotRequired
|
|
10
|
+
|
|
11
|
+
from flag_engine.segments.types import ConditionOperator, ContextValue, 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, ContextValue]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StrValueSegmentCondition(TypedDict):
|
|
31
|
+
property: str
|
|
32
|
+
operator: ConditionOperator
|
|
33
|
+
value: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InOperatorSegmentCondition(TypedDict):
|
|
37
|
+
property: str
|
|
38
|
+
operator: Literal["IN"]
|
|
39
|
+
value: List[str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
SegmentCondition = Union[StrValueSegmentCondition, InOperatorSegmentCondition]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SegmentRule(TypedDict):
|
|
46
|
+
type: RuleType
|
|
47
|
+
conditions: NotRequired[List[SegmentCondition]]
|
|
48
|
+
rules: NotRequired[List[SegmentRule]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FeatureContext(TypedDict):
|
|
52
|
+
key: str
|
|
53
|
+
feature_key: str
|
|
54
|
+
name: str
|
|
55
|
+
enabled: bool
|
|
56
|
+
value: Any
|
|
57
|
+
variants: NotRequired[List[FeatureValue]]
|
|
58
|
+
priority: NotRequired[float]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SegmentContext(TypedDict):
|
|
62
|
+
key: str
|
|
63
|
+
name: str
|
|
64
|
+
rules: List[SegmentRule]
|
|
65
|
+
overrides: NotRequired[List[FeatureContext]]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EvaluationContext(TypedDict):
|
|
69
|
+
environment: EnvironmentContext
|
|
70
|
+
identity: NotRequired[Optional[IdentityContext]]
|
|
71
|
+
segments: NotRequired[Dict[str, SegmentContext]]
|
|
72
|
+
features: NotRequired[Dict[str, FeatureContext]]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from flag_engine.context.types import EvaluationContext
|
|
2
|
+
from flag_engine.result.types import EvaluationResult
|
|
3
|
+
from flag_engine.segments.evaluator import get_evaluation_result
|
|
4
|
+
from flag_engine.segments.types import ContextValue
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
"ContextValue",
|
|
8
|
+
"EvaluationContext",
|
|
9
|
+
"EvaluationResult",
|
|
10
|
+
"get_evaluation_result",
|
|
11
|
+
)
|
|
@@ -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]
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import operator
|
|
3
|
+
import re
|
|
4
|
+
import typing
|
|
5
|
+
import warnings
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from functools import lru_cache, wraps
|
|
8
|
+
|
|
9
|
+
import jsonpath_rfc9535
|
|
10
|
+
import semver
|
|
11
|
+
|
|
12
|
+
from flag_engine.context.mappers import map_any_value_to_context_value
|
|
13
|
+
from flag_engine.context.types import (
|
|
14
|
+
EvaluationContext,
|
|
15
|
+
FeatureContext,
|
|
16
|
+
SegmentCondition,
|
|
17
|
+
SegmentContext,
|
|
18
|
+
SegmentRule,
|
|
19
|
+
StrValueSegmentCondition,
|
|
20
|
+
)
|
|
21
|
+
from flag_engine.result.types import EvaluationResult, FlagResult, SegmentResult
|
|
22
|
+
from flag_engine.segments import constants
|
|
23
|
+
from flag_engine.segments.types import ConditionOperator, ContextValue, is_context_value
|
|
24
|
+
from flag_engine.segments.utils import escape_double_quotes, get_matching_function
|
|
25
|
+
from flag_engine.utils.hashing import get_hashed_percentage_for_object_ids
|
|
26
|
+
from flag_engine.utils.semver import is_semver
|
|
27
|
+
from flag_engine.utils.types import SupportsStr, get_casting_function
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_evaluation_result(context: EvaluationContext) -> EvaluationResult:
|
|
31
|
+
"""
|
|
32
|
+
Get the evaluation result for a given context.
|
|
33
|
+
|
|
34
|
+
:param context: the evaluation context
|
|
35
|
+
:return: EvaluationResult containing the context, flags, and segments
|
|
36
|
+
"""
|
|
37
|
+
segments: list[SegmentResult] = []
|
|
38
|
+
segment_feature_contexts: dict[SupportsStr, FeatureContext] = {}
|
|
39
|
+
for segment_context in (context.get("segments") or {}).values():
|
|
40
|
+
if not is_context_in_segment(context, segment_context):
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
segments.append(
|
|
44
|
+
{
|
|
45
|
+
"key": segment_context["key"],
|
|
46
|
+
"name": segment_context["name"],
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if overrides := segment_context.get("overrides"):
|
|
51
|
+
for override_feature_context in overrides:
|
|
52
|
+
feature_key = override_feature_context["feature_key"]
|
|
53
|
+
if (
|
|
54
|
+
feature_key not in segment_feature_contexts
|
|
55
|
+
or override_feature_context.get(
|
|
56
|
+
"priority",
|
|
57
|
+
constants.DEFAULT_PRIORITY,
|
|
58
|
+
)
|
|
59
|
+
< segment_feature_contexts[feature_key].get(
|
|
60
|
+
"priority",
|
|
61
|
+
constants.DEFAULT_PRIORITY,
|
|
62
|
+
)
|
|
63
|
+
):
|
|
64
|
+
segment_feature_contexts[feature_key] = override_feature_context
|
|
65
|
+
|
|
66
|
+
identity_key = get_context_value(context, "$.identity.key")
|
|
67
|
+
flags: list[FlagResult] = [
|
|
68
|
+
(
|
|
69
|
+
{
|
|
70
|
+
"enabled": segment_feature_context["enabled"],
|
|
71
|
+
"feature_key": segment_feature_context["feature_key"],
|
|
72
|
+
"name": segment_feature_context["name"],
|
|
73
|
+
"reason": f"TARGETING_MATCH; segment={segment_context['name']}",
|
|
74
|
+
"value": segment_feature_context.get("value"),
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
segment_feature_context := segment_feature_contexts.get(
|
|
78
|
+
feature_context["feature_key"],
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
else get_flag_result_from_feature_context(
|
|
82
|
+
feature_context=feature_context,
|
|
83
|
+
key=identity_key,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
for feature_context in (context.get("features") or {}).values()
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"context": context,
|
|
91
|
+
"flags": flags,
|
|
92
|
+
"segments": segments,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_flag_result_from_feature_context(
|
|
97
|
+
feature_context: FeatureContext,
|
|
98
|
+
key: typing.Optional[SupportsStr],
|
|
99
|
+
) -> FlagResult:
|
|
100
|
+
"""
|
|
101
|
+
Get a feature value from the feature context
|
|
102
|
+
for a given key.
|
|
103
|
+
|
|
104
|
+
:param feature_context: the feature context
|
|
105
|
+
:param key: the key to get the value for
|
|
106
|
+
:return: the value for the key in the feature context
|
|
107
|
+
"""
|
|
108
|
+
if key is not None and (variants := feature_context.get("variants")):
|
|
109
|
+
percentage_value = get_hashed_percentage_for_object_ids(
|
|
110
|
+
[feature_context["key"], key]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# We expect `variants` to be pre-sorted in order of persistence. This gives us a
|
|
114
|
+
# way to ensure that the same value is returned every time we use the same
|
|
115
|
+
# percentage value.
|
|
116
|
+
start_percentage = 0.0
|
|
117
|
+
|
|
118
|
+
for variant in variants:
|
|
119
|
+
limit = (weight := variant["weight"]) + start_percentage
|
|
120
|
+
if start_percentage <= percentage_value < limit:
|
|
121
|
+
return {
|
|
122
|
+
"enabled": feature_context["enabled"],
|
|
123
|
+
"feature_key": feature_context["feature_key"],
|
|
124
|
+
"name": feature_context["name"],
|
|
125
|
+
"reason": f"SPLIT; weight={weight}",
|
|
126
|
+
"value": variant["value"],
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
start_percentage = limit
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"enabled": feature_context["enabled"],
|
|
133
|
+
"feature_key": feature_context["feature_key"],
|
|
134
|
+
"name": feature_context["name"],
|
|
135
|
+
"reason": "DEFAULT",
|
|
136
|
+
"value": feature_context["value"],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def is_context_in_segment(
|
|
141
|
+
context: EvaluationContext,
|
|
142
|
+
segment_context: SegmentContext,
|
|
143
|
+
) -> bool:
|
|
144
|
+
return bool(rules := segment_context["rules"]) and all(
|
|
145
|
+
context_matches_rule(
|
|
146
|
+
context=context, rule=rule, segment_key=segment_context["key"]
|
|
147
|
+
)
|
|
148
|
+
for rule in rules
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def context_matches_rule(
|
|
153
|
+
context: EvaluationContext,
|
|
154
|
+
rule: SegmentRule,
|
|
155
|
+
segment_key: SupportsStr,
|
|
156
|
+
) -> bool:
|
|
157
|
+
matches_conditions = (
|
|
158
|
+
get_matching_function(rule["type"])(
|
|
159
|
+
[
|
|
160
|
+
context_matches_condition(
|
|
161
|
+
context=context,
|
|
162
|
+
condition=condition,
|
|
163
|
+
segment_key=segment_key,
|
|
164
|
+
)
|
|
165
|
+
for condition in conditions
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
if (conditions := rule.get("conditions"))
|
|
169
|
+
else True
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return matches_conditions and all(
|
|
173
|
+
context_matches_rule(
|
|
174
|
+
context=context,
|
|
175
|
+
rule=rule,
|
|
176
|
+
segment_key=segment_key,
|
|
177
|
+
)
|
|
178
|
+
for rule in rule.get("rules") or []
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def context_matches_condition(
|
|
183
|
+
context: EvaluationContext,
|
|
184
|
+
condition: SegmentCondition,
|
|
185
|
+
segment_key: SupportsStr,
|
|
186
|
+
) -> bool:
|
|
187
|
+
context_value = (
|
|
188
|
+
get_context_value(context, condition_property)
|
|
189
|
+
if (condition_property := condition.get("property"))
|
|
190
|
+
else None
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if condition["operator"] == constants.IN:
|
|
194
|
+
if isinstance(segment_value := condition["value"], list):
|
|
195
|
+
in_values = segment_value
|
|
196
|
+
else:
|
|
197
|
+
try:
|
|
198
|
+
in_values = json.loads(segment_value)
|
|
199
|
+
# Only accept JSON lists.
|
|
200
|
+
# Ideally, we should use something like pydantic.TypeAdapter[list[str]],
|
|
201
|
+
# but we aim to ditch the pydantic dependency in the future.
|
|
202
|
+
if not isinstance(in_values, list):
|
|
203
|
+
raise ValueError
|
|
204
|
+
except ValueError:
|
|
205
|
+
in_values = segment_value.split(",")
|
|
206
|
+
in_values = [str(value) for value in in_values]
|
|
207
|
+
# Guard against comparing boolean values to numeric strings.
|
|
208
|
+
if isinstance(context_value, int) and not (
|
|
209
|
+
context_value is True or context_value is False
|
|
210
|
+
):
|
|
211
|
+
context_value = str(context_value)
|
|
212
|
+
return context_value in in_values
|
|
213
|
+
|
|
214
|
+
condition = typing.cast(StrValueSegmentCondition, condition)
|
|
215
|
+
|
|
216
|
+
if condition["operator"] == constants.PERCENTAGE_SPLIT:
|
|
217
|
+
if context_value is not None:
|
|
218
|
+
object_ids = [segment_key, context_value]
|
|
219
|
+
else:
|
|
220
|
+
object_ids = [segment_key, get_context_value(context, "$.identity.key")]
|
|
221
|
+
|
|
222
|
+
float_value = float(condition["value"])
|
|
223
|
+
return get_hashed_percentage_for_object_ids(object_ids) <= float_value
|
|
224
|
+
|
|
225
|
+
if condition["operator"] == constants.IS_NOT_SET:
|
|
226
|
+
return context_value is None
|
|
227
|
+
|
|
228
|
+
if condition["operator"] == constants.IS_SET:
|
|
229
|
+
return context_value is not None
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
_matches_context_value(condition, context_value)
|
|
233
|
+
if context_value is not None
|
|
234
|
+
else False
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_context_value(
|
|
239
|
+
context: EvaluationContext,
|
|
240
|
+
property: str,
|
|
241
|
+
) -> ContextValue:
|
|
242
|
+
value = None
|
|
243
|
+
if property.startswith("$."):
|
|
244
|
+
value = _get_context_value_getter(property)(context)
|
|
245
|
+
elif identity_context := context.get("identity"):
|
|
246
|
+
if traits := identity_context.get("traits"):
|
|
247
|
+
value = traits.get(property)
|
|
248
|
+
return map_any_value_to_context_value(value)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _matches_context_value(
|
|
252
|
+
condition: StrValueSegmentCondition,
|
|
253
|
+
context_value: ContextValue,
|
|
254
|
+
) -> bool:
|
|
255
|
+
if matcher := MATCHERS_BY_OPERATOR.get(condition["operator"]):
|
|
256
|
+
return matcher(condition["value"], context_value)
|
|
257
|
+
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _evaluate_not_contains(
|
|
262
|
+
segment_value: typing.Optional[str],
|
|
263
|
+
context_value: ContextValue,
|
|
264
|
+
) -> bool:
|
|
265
|
+
return isinstance(context_value, str) and str(segment_value) not in context_value
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _evaluate_regex(
|
|
269
|
+
segment_value: typing.Optional[str],
|
|
270
|
+
context_value: ContextValue,
|
|
271
|
+
) -> bool:
|
|
272
|
+
return (
|
|
273
|
+
context_value is not None
|
|
274
|
+
and re.compile(str(segment_value)).match(str(context_value)) is not None
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _evaluate_modulo(
|
|
279
|
+
segment_value: typing.Optional[str],
|
|
280
|
+
context_value: ContextValue,
|
|
281
|
+
) -> bool:
|
|
282
|
+
if not isinstance(context_value, (int, float)):
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
if not segment_value:
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
divisor_part, remainder_part = segment_value.split("|")
|
|
290
|
+
divisor = float(divisor_part)
|
|
291
|
+
remainder = float(remainder_part)
|
|
292
|
+
except ValueError:
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
return context_value % divisor == remainder
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _context_value_typed(
|
|
299
|
+
func: typing.Callable[..., bool],
|
|
300
|
+
) -> typing.Callable[[typing.Optional[str], ContextValue], bool]:
|
|
301
|
+
@wraps(func)
|
|
302
|
+
def inner(
|
|
303
|
+
segment_value: typing.Optional[str],
|
|
304
|
+
context_value: typing.Union[ContextValue, semver.Version],
|
|
305
|
+
) -> bool:
|
|
306
|
+
with suppress(TypeError, ValueError):
|
|
307
|
+
if isinstance(context_value, str) and is_semver(segment_value):
|
|
308
|
+
context_value = semver.Version.parse(
|
|
309
|
+
context_value,
|
|
310
|
+
)
|
|
311
|
+
match_value = get_casting_function(context_value)(segment_value)
|
|
312
|
+
return func(context_value, match_value)
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
return inner
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
MATCHERS_BY_OPERATOR: dict[
|
|
319
|
+
ConditionOperator, typing.Callable[[typing.Optional[str], ContextValue], bool]
|
|
320
|
+
] = {
|
|
321
|
+
constants.NOT_CONTAINS: _evaluate_not_contains,
|
|
322
|
+
constants.REGEX: _evaluate_regex,
|
|
323
|
+
constants.MODULO: _evaluate_modulo,
|
|
324
|
+
constants.EQUAL: _context_value_typed(operator.eq),
|
|
325
|
+
constants.GREATER_THAN: _context_value_typed(operator.gt),
|
|
326
|
+
constants.GREATER_THAN_INCLUSIVE: _context_value_typed(operator.ge),
|
|
327
|
+
constants.LESS_THAN: _context_value_typed(operator.lt),
|
|
328
|
+
constants.LESS_THAN_INCLUSIVE: _context_value_typed(operator.le),
|
|
329
|
+
constants.NOT_EQUAL: _context_value_typed(operator.ne),
|
|
330
|
+
constants.CONTAINS: _context_value_typed(operator.contains),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@lru_cache
|
|
335
|
+
def _get_context_value_getter(
|
|
336
|
+
property: str,
|
|
337
|
+
) -> typing.Callable[[EvaluationContext], ContextValue]:
|
|
338
|
+
"""
|
|
339
|
+
Get a function to retrieve a context value based on property value,
|
|
340
|
+
assumed to be either a JSONPath string or a trait key.
|
|
341
|
+
|
|
342
|
+
:param property: The property to retrieve the value for.
|
|
343
|
+
:return: A function that takes an EvaluationContext and returns the value.
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
compiled_query = jsonpath_rfc9535.compile(property)
|
|
347
|
+
except jsonpath_rfc9535.JSONPathSyntaxError:
|
|
348
|
+
# This covers a rare case when a trait starting with "$.",
|
|
349
|
+
# but not a valid JSONPath, is used.
|
|
350
|
+
compiled_query = jsonpath_rfc9535.compile(
|
|
351
|
+
f'$.identity.traits["{escape_double_quotes(property)}"]',
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def getter(context: EvaluationContext) -> ContextValue:
|
|
355
|
+
if typing.TYPE_CHECKING: # pragma: no cover
|
|
356
|
+
# Ugly hack to satisfy mypy :(
|
|
357
|
+
data = dict(context)
|
|
358
|
+
else:
|
|
359
|
+
data = context
|
|
360
|
+
try:
|
|
361
|
+
if result := compiled_query.find_one(data):
|
|
362
|
+
if is_context_value(value := result.value):
|
|
363
|
+
return value
|
|
364
|
+
return None
|
|
365
|
+
except jsonpath_rfc9535.JSONPathError: # pragma: no cover
|
|
366
|
+
# This is supposed to be unreachable, but if it happens,
|
|
367
|
+
# we log a warning and return None.
|
|
368
|
+
warnings.warn(
|
|
369
|
+
f"Failed to evaluate JSONPath query '{property}' in context: {context}",
|
|
370
|
+
RuntimeWarning,
|
|
371
|
+
)
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
return getter
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Any, Literal, Union, get_args
|
|
2
|
+
|
|
3
|
+
from typing_extensions import TypeGuard
|
|
4
|
+
|
|
5
|
+
ConditionOperator = Literal[
|
|
6
|
+
"EQUAL",
|
|
7
|
+
"GREATER_THAN",
|
|
8
|
+
"LESS_THAN",
|
|
9
|
+
"LESS_THAN_INCLUSIVE",
|
|
10
|
+
"CONTAINS",
|
|
11
|
+
"GREATER_THAN_INCLUSIVE",
|
|
12
|
+
"NOT_CONTAINS",
|
|
13
|
+
"NOT_EQUAL",
|
|
14
|
+
"REGEX",
|
|
15
|
+
"PERCENTAGE_SPLIT",
|
|
16
|
+
"MODULO",
|
|
17
|
+
"IS_SET",
|
|
18
|
+
"IS_NOT_SET",
|
|
19
|
+
"IN",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
RuleType = Literal[
|
|
23
|
+
"ALL",
|
|
24
|
+
"ANY",
|
|
25
|
+
"NONE",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ContextValue = Union[None, int, float, bool, str]
|
|
30
|
+
_context_value_types = get_args(ContextValue)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_context_value(value: Any) -> TypeGuard[ContextValue]:
|
|
34
|
+
"""
|
|
35
|
+
Check if the value is a valid context value type.
|
|
36
|
+
This function is used to determine if a value can be treated as a context value.
|
|
37
|
+
"""
|
|
38
|
+
return isinstance(value, _context_value_types)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from flag_engine.segments.types import RuleType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_matching_function(
|
|
7
|
+
segment_type: RuleType,
|
|
8
|
+
) -> typing.Callable[[typing.Iterable[object]], bool]:
|
|
9
|
+
return {
|
|
10
|
+
"ANY": any,
|
|
11
|
+
"ALL": all,
|
|
12
|
+
"NONE": none,
|
|
13
|
+
}[segment_type]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def none(iterable: typing.Iterable[object]) -> bool:
|
|
17
|
+
return not any(iterable)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def escape_double_quotes(value: str) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Escape double quotes in a string for JSONPath compatibility.
|
|
23
|
+
"""
|
|
24
|
+
return value.replace('"', '\\"')
|
{flagsmith_flag_engine-6.0.2 → flagsmith_flag_engine-7.0.0/flagsmith_flag_engine.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flagsmith-flag-engine
|
|
3
|
-
Version:
|
|
3
|
+
Version: 7.0.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,14 +8,16 @@ 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
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
13
11
|
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
16
|
Description-Content-Type: text/markdown
|
|
15
17
|
License-File: LICENSE.txt
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: jsonpath-rfc9535<1,>=0.1.5
|
|
19
|
+
Requires-Dist: semver<4,>=3.0.4
|
|
20
|
+
Requires-Dist: typing-extensions<5,>=4.14.1
|
|
19
21
|
Dynamic: author
|
|
20
22
|
Dynamic: author-email
|
|
21
23
|
Dynamic: classifier
|