flagsmith-flag-engine 5.3.0__tar.gz → 5.4.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-5.3.0/flagsmith_flag_engine.egg-info → flagsmith-flag-engine-5.4.0}/PKG-INFO +1 -4
- flagsmith-flag-engine-5.4.0/flag_engine/context/mappers.py +39 -0
- flagsmith-flag-engine-5.4.0/flag_engine/context/types.py +28 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/engine.py +41 -19
- flagsmith-flag-engine-5.4.0/flag_engine/identities/traits/models.py +8 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/identities/traits/types.py +6 -6
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/organisations/models.py +3 -3
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/projects/models.py +1 -1
- flagsmith-flag-engine-5.4.0/flag_engine/segments/evaluator.py +229 -0
- flagsmith-flag-engine-5.4.0/flag_engine/utils/__init__.py +0 -0
- flagsmith-flag-engine-5.4.0/flag_engine/utils/json/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/types.py +2 -2
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0/flagsmith_flag_engine.egg-info}/PKG-INFO +1 -4
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flagsmith_flag_engine.egg-info/SOURCES.txt +4 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/setup.py +1 -1
- flagsmith-flag-engine-5.3.0/flag_engine/identities/traits/models.py +0 -8
- flagsmith-flag-engine-5.3.0/flag_engine/segments/evaluator.py +0 -207
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/LICENSE.txt +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/README.md +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/environments → flagsmith-flag-engine-5.4.0/flag_engine/context}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/environments/integrations → flagsmith-flag-engine-5.4.0/flag_engine/environments}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/features → flagsmith-flag-engine-5.4.0/flag_engine/environments/integrations}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/environments/integrations/models.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/environments/models.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/identities → flagsmith-flag-engine-5.4.0/flag_engine/features}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/features/constants.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/features/models.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/identities/traits → flagsmith-flag-engine-5.4.0/flag_engine/identities}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/identities/models.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/organisations → flagsmith-flag-engine-5.4.0/flag_engine/identities/traits}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/identities/traits/constants.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/projects → flagsmith-flag-engine-5.4.0/flag_engine/organisations}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/segments → flagsmith-flag-engine-5.4.0/flag_engine/projects}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/py.typed +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/utils → flagsmith-flag-engine-5.4.0/flag_engine/segments}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/segments/constants.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/segments/models.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/segments/types.py +0 -0
- {flagsmith-flag-engine-5.3.0/flag_engine/utils/json → flagsmith-flag-engine-5.4.0/flag_engine/types}/__init__.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/datetime.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/exceptions.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/hashing.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/json/encoders.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/semver.py +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flagsmith_flag_engine.egg-info/dependency_links.txt +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flagsmith_flag_engine.egg-info/requires.txt +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flagsmith_flag_engine.egg-info/top_level.txt +0 -0
- {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/setup.cfg +0 -0
{flagsmith-flag-engine-5.3.0/flagsmith_flag_engine.egg-info → flagsmith-flag-engine-5.4.0}/PKG-INFO
RENAMED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: flagsmith-flag-engine
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.4.0
|
|
4
4
|
Summary: Flag engine for the Flagsmith API.
|
|
5
5
|
Home-page: https://github.com/Flagsmith/flagsmith-engine
|
|
6
6
|
Author: Flagsmith
|
|
7
7
|
Author-email: support@flagsmith.com
|
|
8
8
|
License: BSD3
|
|
9
|
-
Platform: UNKNOWN
|
|
10
9
|
Classifier: License :: OSI Approved :: BSD License
|
|
11
10
|
Classifier: Programming Language :: Python :: 3
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.7
|
|
@@ -36,5 +35,3 @@ python -m pip install -r requirements-dev.txt
|
|
|
36
35
|
|
|
37
36
|
- Marshmallow Schemas
|
|
38
37
|
- Plain Python
|
|
39
|
-
|
|
40
|
-
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from flag_engine.context.types import EvaluationContext
|
|
4
|
+
from flag_engine.environments.models import EnvironmentModel
|
|
5
|
+
from flag_engine.identities.models import IdentityModel
|
|
6
|
+
from flag_engine.identities.traits.models import TraitModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def map_environment_identity_to_context(
|
|
10
|
+
environment: EnvironmentModel,
|
|
11
|
+
identity: IdentityModel,
|
|
12
|
+
override_traits: typing.Optional[typing.List[TraitModel]],
|
|
13
|
+
) -> EvaluationContext:
|
|
14
|
+
"""
|
|
15
|
+
Maps an EnvironmentModel and IdentityModel to an EvaluationContext.
|
|
16
|
+
|
|
17
|
+
:param environment: The environment model object.
|
|
18
|
+
:param identity: The identity model object.
|
|
19
|
+
:param override_traits: A list of TraitModel objects, to be used in place of `identity.identity_traits` if provided.
|
|
20
|
+
:return: An EvaluationContext containing the environment and identity.
|
|
21
|
+
"""
|
|
22
|
+
return {
|
|
23
|
+
"environment": {
|
|
24
|
+
"key": environment.api_key,
|
|
25
|
+
"name": environment.name or "",
|
|
26
|
+
},
|
|
27
|
+
"identity": {
|
|
28
|
+
"identifier": identity.identifier,
|
|
29
|
+
"key": str(identity.django_id or identity.composite_key),
|
|
30
|
+
"traits": {
|
|
31
|
+
trait.trait_key: trait.trait_value
|
|
32
|
+
for trait in (
|
|
33
|
+
override_traits
|
|
34
|
+
if override_traits is not None
|
|
35
|
+
else identity.identity_traits
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/chore/update-evaluation-context/sdk/evaluation-context.json # noqa: E501
|
|
3
|
+
# timestamp: 2025-07-16T10:39:10+00:00
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Optional, TypedDict
|
|
8
|
+
|
|
9
|
+
from typing_extensions import NotRequired
|
|
10
|
+
|
|
11
|
+
from flag_engine.identities.traits.types import ContextValue
|
|
12
|
+
from flag_engine.utils.types import SupportsStr
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EnvironmentContext(TypedDict):
|
|
16
|
+
key: str
|
|
17
|
+
name: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IdentityContext(TypedDict):
|
|
21
|
+
identifier: str
|
|
22
|
+
key: SupportsStr
|
|
23
|
+
traits: NotRequired[Dict[str, ContextValue]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EvaluationContext(TypedDict):
|
|
27
|
+
environment: EnvironmentContext
|
|
28
|
+
identity: NotRequired[Optional[IdentityContext]]
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
+
from flag_engine.context.mappers import map_environment_identity_to_context
|
|
4
|
+
from flag_engine.context.types import EvaluationContext
|
|
3
5
|
from flag_engine.environments.models import EnvironmentModel
|
|
4
6
|
from flag_engine.features.models import FeatureModel, FeatureStateModel
|
|
5
7
|
from flag_engine.identities.models import IdentityModel
|
|
6
8
|
from flag_engine.identities.traits.models import TraitModel
|
|
7
|
-
from flag_engine.segments.evaluator import
|
|
9
|
+
from flag_engine.segments.evaluator import get_context_segments
|
|
8
10
|
from flag_engine.utils.exceptions import FeatureStateNotFound
|
|
9
11
|
|
|
10
12
|
|
|
@@ -53,9 +55,17 @@ def get_identity_feature_states(
|
|
|
53
55
|
:return: list of feature state models based on the environment, any matching
|
|
54
56
|
segments and any specific identity overrides
|
|
55
57
|
"""
|
|
58
|
+
context = map_environment_identity_to_context(
|
|
59
|
+
environment=environment,
|
|
60
|
+
identity=identity,
|
|
61
|
+
override_traits=override_traits,
|
|
62
|
+
)
|
|
63
|
+
|
|
56
64
|
feature_states = list(
|
|
57
65
|
_get_identity_feature_states_dict(
|
|
58
|
-
environment,
|
|
66
|
+
environment=environment,
|
|
67
|
+
identity=identity,
|
|
68
|
+
context=context,
|
|
59
69
|
).values()
|
|
60
70
|
)
|
|
61
71
|
if environment.get_hide_disabled_flags():
|
|
@@ -79,8 +89,16 @@ def get_identity_feature_state(
|
|
|
79
89
|
:return: feature state model based on the environment, any matching
|
|
80
90
|
segments and any specific identity overrides
|
|
81
91
|
"""
|
|
92
|
+
context = map_environment_identity_to_context(
|
|
93
|
+
environment=environment,
|
|
94
|
+
identity=identity,
|
|
95
|
+
override_traits=override_traits,
|
|
96
|
+
)
|
|
97
|
+
|
|
82
98
|
feature_states = _get_identity_feature_states_dict(
|
|
83
|
-
environment,
|
|
99
|
+
environment=environment,
|
|
100
|
+
identity=identity,
|
|
101
|
+
context=context,
|
|
84
102
|
)
|
|
85
103
|
matching_feature = next(
|
|
86
104
|
filter(lambda feature: feature.name == feature_name, feature_states.keys()),
|
|
@@ -96,29 +114,33 @@ def get_identity_feature_state(
|
|
|
96
114
|
def _get_identity_feature_states_dict(
|
|
97
115
|
environment: EnvironmentModel,
|
|
98
116
|
identity: IdentityModel,
|
|
99
|
-
|
|
117
|
+
context: EvaluationContext,
|
|
100
118
|
) -> typing.Dict[FeatureModel, FeatureStateModel]:
|
|
101
119
|
# Get feature states from the environment
|
|
102
|
-
|
|
120
|
+
feature_states_by_feature = {fs.feature: fs for fs in environment.feature_states}
|
|
103
121
|
|
|
104
122
|
# Override with any feature states defined by matching segments
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
135
|
|
|
115
136
|
# Override with any feature states defined directly the identity
|
|
116
|
-
|
|
137
|
+
feature_states_by_feature.update(
|
|
117
138
|
{
|
|
118
|
-
|
|
119
|
-
for
|
|
120
|
-
if
|
|
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
|
|
121
143
|
}
|
|
122
144
|
)
|
|
123
145
|
|
|
124
|
-
return
|
|
146
|
+
return feature_states_by_feature
|
{flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/identities/traits/types.py
RENAMED
|
@@ -8,10 +8,10 @@ from typing_extensions import Annotated, TypeGuard
|
|
|
8
8
|
|
|
9
9
|
from flag_engine.identities.traits.constants import TRAIT_STRING_VALUE_MAX_LENGTH
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
_UnconstrainedContextValue = Union[None, int, float, bool, str]
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def map_any_value_to_trait_value(value: Any) ->
|
|
14
|
+
def map_any_value_to_trait_value(value: Any) -> _UnconstrainedContextValue:
|
|
15
15
|
"""
|
|
16
16
|
Try to coerce a value of arbitrary type to a trait value type.
|
|
17
17
|
Union member-specific constraints, such as max string value length, are ignored here.
|
|
@@ -36,7 +36,7 @@ _int_pattern = re.compile(r"-?[0-9]+")
|
|
|
36
36
|
_float_pattern = re.compile(r"-?[0-9]+\.[0-9]+")
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def _map_string_value_to_trait_value(value: str) ->
|
|
39
|
+
def _map_string_value_to_trait_value(value: str) -> _UnconstrainedContextValue:
|
|
40
40
|
if _int_pattern.fullmatch(value):
|
|
41
41
|
return int(value)
|
|
42
42
|
if _float_pattern.fullmatch(value):
|
|
@@ -44,11 +44,11 @@ def _map_string_value_to_trait_value(value: str) -> _UnconstrainedTraitValue:
|
|
|
44
44
|
return value
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def _is_trait_value(value: Any) -> TypeGuard[
|
|
48
|
-
return isinstance(value, get_args(
|
|
47
|
+
def _is_trait_value(value: Any) -> TypeGuard[_UnconstrainedContextValue]:
|
|
48
|
+
return isinstance(value, get_args(_UnconstrainedContextValue))
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
ContextValue = Annotated[
|
|
52
52
|
Union[
|
|
53
53
|
None,
|
|
54
54
|
StrictBool,
|
{flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/organisations/models.py
RENAMED
|
@@ -4,9 +4,9 @@ from pydantic import BaseModel
|
|
|
4
4
|
class OrganisationModel(BaseModel):
|
|
5
5
|
id: int
|
|
6
6
|
name: str
|
|
7
|
-
feature_analytics: bool
|
|
8
|
-
stop_serving_flags: bool
|
|
9
|
-
persist_trait_data: bool
|
|
7
|
+
feature_analytics: bool = False
|
|
8
|
+
stop_serving_flags: bool = False
|
|
9
|
+
persist_trait_data: bool = True
|
|
10
10
|
|
|
11
11
|
@property
|
|
12
12
|
def unique_slug(self) -> str:
|
|
@@ -10,7 +10,7 @@ class ProjectModel(BaseModel):
|
|
|
10
10
|
id: int
|
|
11
11
|
name: str
|
|
12
12
|
organisation: OrganisationModel
|
|
13
|
-
hide_disabled_flags: bool
|
|
13
|
+
hide_disabled_flags: bool = False
|
|
14
14
|
segments: typing.List[SegmentModel] = Field(default_factory=list)
|
|
15
15
|
enable_realtime_updates: bool = False
|
|
16
16
|
server_key_only_feature_ids: typing.List[int] = Field(default_factory=list)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
import re
|
|
3
|
+
import typing
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from functools import partial, wraps
|
|
6
|
+
|
|
7
|
+
import semver
|
|
8
|
+
|
|
9
|
+
from flag_engine.context.types import EvaluationContext
|
|
10
|
+
from flag_engine.identities.traits.types import ContextValue
|
|
11
|
+
from flag_engine.segments import constants
|
|
12
|
+
from flag_engine.segments.models import (
|
|
13
|
+
SegmentConditionModel,
|
|
14
|
+
SegmentModel,
|
|
15
|
+
SegmentRuleModel,
|
|
16
|
+
)
|
|
17
|
+
from flag_engine.segments.types import ConditionOperator
|
|
18
|
+
from flag_engine.utils.hashing import get_hashed_percentage_for_object_ids
|
|
19
|
+
from flag_engine.utils.semver import is_semver
|
|
20
|
+
from flag_engine.utils.types import SupportsStr, get_casting_function
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_context_segments(
|
|
24
|
+
context: EvaluationContext,
|
|
25
|
+
segments: typing.List[SegmentModel],
|
|
26
|
+
) -> typing.List[SegmentModel]:
|
|
27
|
+
return [
|
|
28
|
+
segment
|
|
29
|
+
for segment in segments
|
|
30
|
+
if is_context_in_segment(
|
|
31
|
+
context=context,
|
|
32
|
+
segment=segment,
|
|
33
|
+
)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_context_in_segment(
|
|
38
|
+
context: EvaluationContext,
|
|
39
|
+
segment: SegmentModel,
|
|
40
|
+
) -> bool:
|
|
41
|
+
return bool(rules := segment.rules) and all(
|
|
42
|
+
context_matches_rule(context=context, rule=rule, segment_key=segment.id)
|
|
43
|
+
for rule in rules
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def context_matches_rule(
|
|
48
|
+
context: EvaluationContext,
|
|
49
|
+
rule: SegmentRuleModel,
|
|
50
|
+
segment_key: SupportsStr,
|
|
51
|
+
) -> bool:
|
|
52
|
+
matches_conditions = (
|
|
53
|
+
rule.matching_function(
|
|
54
|
+
[
|
|
55
|
+
context_matches_condition(
|
|
56
|
+
context=context,
|
|
57
|
+
condition=condition,
|
|
58
|
+
segment_key=segment_key,
|
|
59
|
+
)
|
|
60
|
+
for condition in conditions
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
if (conditions := rule.conditions)
|
|
64
|
+
else True
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return matches_conditions and all(
|
|
68
|
+
context_matches_rule(
|
|
69
|
+
context=context,
|
|
70
|
+
rule=rule,
|
|
71
|
+
segment_key=segment_key,
|
|
72
|
+
)
|
|
73
|
+
for rule in rule.rules
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def context_matches_condition(
|
|
78
|
+
context: EvaluationContext,
|
|
79
|
+
condition: SegmentConditionModel,
|
|
80
|
+
segment_key: SupportsStr,
|
|
81
|
+
) -> bool:
|
|
82
|
+
context_value = (
|
|
83
|
+
get_context_value(context, condition.property_) if condition.property_ else None
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if condition.operator == constants.PERCENTAGE_SPLIT:
|
|
87
|
+
assert condition.value
|
|
88
|
+
|
|
89
|
+
if context_value is not None:
|
|
90
|
+
object_ids = [segment_key, context_value]
|
|
91
|
+
else:
|
|
92
|
+
object_ids = [segment_key, get_context_value(context, "$.identity.key")]
|
|
93
|
+
|
|
94
|
+
float_value = float(condition.value)
|
|
95
|
+
return get_hashed_percentage_for_object_ids(object_ids) <= float_value
|
|
96
|
+
|
|
97
|
+
if condition.operator == constants.IS_NOT_SET:
|
|
98
|
+
return context_value is None
|
|
99
|
+
|
|
100
|
+
if condition.operator == constants.IS_SET:
|
|
101
|
+
return context_value is not None
|
|
102
|
+
|
|
103
|
+
return _matches_context_value(condition, context_value) if context_value else False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _get_trait(context: EvaluationContext, trait_key: str) -> ContextValue:
|
|
107
|
+
return (
|
|
108
|
+
identity_context["traits"][trait_key]
|
|
109
|
+
if (identity_context := context["identity"])
|
|
110
|
+
else None
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_context_value(
|
|
115
|
+
context: EvaluationContext,
|
|
116
|
+
property: str,
|
|
117
|
+
) -> ContextValue:
|
|
118
|
+
getter = CONTEXT_VALUE_GETTERS_BY_PROPERTY.get(property) or partial(
|
|
119
|
+
_get_trait,
|
|
120
|
+
trait_key=property,
|
|
121
|
+
)
|
|
122
|
+
try:
|
|
123
|
+
return getter(context)
|
|
124
|
+
except KeyError:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _matches_context_value(
|
|
129
|
+
condition: SegmentConditionModel,
|
|
130
|
+
context_value: ContextValue,
|
|
131
|
+
) -> bool:
|
|
132
|
+
if matcher := MATCHERS_BY_OPERATOR.get(condition.operator):
|
|
133
|
+
return matcher(condition.value, context_value)
|
|
134
|
+
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _evaluate_not_contains(
|
|
139
|
+
segment_value: typing.Optional[str],
|
|
140
|
+
context_value: ContextValue,
|
|
141
|
+
) -> bool:
|
|
142
|
+
return isinstance(context_value, str) and str(segment_value) not in context_value
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _evaluate_regex(
|
|
146
|
+
segment_value: typing.Optional[str],
|
|
147
|
+
context_value: ContextValue,
|
|
148
|
+
) -> bool:
|
|
149
|
+
return (
|
|
150
|
+
context_value is not None
|
|
151
|
+
and re.compile(str(segment_value)).match(str(context_value)) is not None
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _evaluate_modulo(
|
|
156
|
+
segment_value: typing.Optional[str],
|
|
157
|
+
context_value: ContextValue,
|
|
158
|
+
) -> bool:
|
|
159
|
+
if not isinstance(context_value, (int, float)):
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
if segment_value is None:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
divisor_part, remainder_part = segment_value.split("|")
|
|
167
|
+
divisor = float(divisor_part)
|
|
168
|
+
remainder = float(remainder_part)
|
|
169
|
+
except ValueError:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
return context_value % divisor == remainder
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _evaluate_in(
|
|
176
|
+
segment_value: typing.Optional[str], context_value: ContextValue
|
|
177
|
+
) -> bool:
|
|
178
|
+
if segment_value:
|
|
179
|
+
if isinstance(context_value, str):
|
|
180
|
+
return context_value in segment_value.split(",")
|
|
181
|
+
if isinstance(context_value, int) and not any(
|
|
182
|
+
context_value is x for x in (False, True)
|
|
183
|
+
):
|
|
184
|
+
return str(context_value) in segment_value.split(",")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _context_value_typed(
|
|
189
|
+
func: typing.Callable[..., bool],
|
|
190
|
+
) -> typing.Callable[[typing.Optional[str], ContextValue], bool]:
|
|
191
|
+
@wraps(func)
|
|
192
|
+
def inner(
|
|
193
|
+
segment_value: typing.Optional[str],
|
|
194
|
+
context_value: typing.Union[ContextValue, semver.Version],
|
|
195
|
+
) -> bool:
|
|
196
|
+
with suppress(TypeError, ValueError):
|
|
197
|
+
if isinstance(context_value, str) and is_semver(segment_value):
|
|
198
|
+
context_value = semver.Version.parse(
|
|
199
|
+
context_value,
|
|
200
|
+
)
|
|
201
|
+
match_value = get_casting_function(context_value)(segment_value)
|
|
202
|
+
return func(context_value, match_value)
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
return inner
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
MATCHERS_BY_OPERATOR: typing.Dict[
|
|
209
|
+
ConditionOperator, typing.Callable[[typing.Optional[str], ContextValue], bool]
|
|
210
|
+
] = {
|
|
211
|
+
constants.NOT_CONTAINS: _evaluate_not_contains,
|
|
212
|
+
constants.REGEX: _evaluate_regex,
|
|
213
|
+
constants.MODULO: _evaluate_modulo,
|
|
214
|
+
constants.IN: _evaluate_in,
|
|
215
|
+
constants.EQUAL: _context_value_typed(operator.eq),
|
|
216
|
+
constants.GREATER_THAN: _context_value_typed(operator.gt),
|
|
217
|
+
constants.GREATER_THAN_INCLUSIVE: _context_value_typed(operator.ge),
|
|
218
|
+
constants.LESS_THAN: _context_value_typed(operator.lt),
|
|
219
|
+
constants.LESS_THAN_INCLUSIVE: _context_value_typed(operator.le),
|
|
220
|
+
constants.NOT_EQUAL: _context_value_typed(operator.ne),
|
|
221
|
+
constants.CONTAINS: _context_value_typed(operator.contains),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
CONTEXT_VALUE_GETTERS_BY_PROPERTY = {
|
|
226
|
+
"$.identity.identifier": lambda context: context["identity"]["identifier"],
|
|
227
|
+
"$.identity.key": lambda context: context["identity"]["key"],
|
|
228
|
+
"$.environment.name": lambda context: context["environment"]["name"],
|
|
229
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -3,7 +3,7 @@ from functools import singledispatch
|
|
|
3
3
|
|
|
4
4
|
import semver
|
|
5
5
|
|
|
6
|
-
from flag_engine.identities.traits.types import
|
|
6
|
+
from flag_engine.identities.traits.types import ContextValue
|
|
7
7
|
from flag_engine.utils.semver import remove_semver_suffix
|
|
8
8
|
|
|
9
9
|
|
|
@@ -15,7 +15,7 @@ class SupportsStr(typing.Protocol):
|
|
|
15
15
|
@singledispatch
|
|
16
16
|
def get_casting_function(
|
|
17
17
|
input_: object,
|
|
18
|
-
) -> typing.Callable[...,
|
|
18
|
+
) -> typing.Callable[..., ContextValue]:
|
|
19
19
|
"""
|
|
20
20
|
This function returns a callable to cast a value to the same type as input_
|
|
21
21
|
>>> assert get_casting_function("a string") == str
|
{flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0/flagsmith_flag_engine.egg-info}/PKG-INFO
RENAMED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: flagsmith-flag-engine
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.4.0
|
|
4
4
|
Summary: Flag engine for the Flagsmith API.
|
|
5
5
|
Home-page: https://github.com/Flagsmith/flagsmith-engine
|
|
6
6
|
Author: Flagsmith
|
|
7
7
|
Author-email: support@flagsmith.com
|
|
8
8
|
License: BSD3
|
|
9
|
-
Platform: UNKNOWN
|
|
10
9
|
Classifier: License :: OSI Approved :: BSD License
|
|
11
10
|
Classifier: Programming Language :: Python :: 3
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.7
|
|
@@ -36,5 +35,3 @@ python -m pip install -r requirements-dev.txt
|
|
|
36
35
|
|
|
37
36
|
- Marshmallow Schemas
|
|
38
37
|
- Plain Python
|
|
39
|
-
|
|
40
|
-
|
|
@@ -5,6 +5,9 @@ setup.py
|
|
|
5
5
|
flag_engine/__init__.py
|
|
6
6
|
flag_engine/engine.py
|
|
7
7
|
flag_engine/py.typed
|
|
8
|
+
flag_engine/context/__init__.py
|
|
9
|
+
flag_engine/context/mappers.py
|
|
10
|
+
flag_engine/context/types.py
|
|
8
11
|
flag_engine/environments/__init__.py
|
|
9
12
|
flag_engine/environments/models.py
|
|
10
13
|
flag_engine/environments/integrations/__init__.py
|
|
@@ -27,6 +30,7 @@ flag_engine/segments/constants.py
|
|
|
27
30
|
flag_engine/segments/evaluator.py
|
|
28
31
|
flag_engine/segments/models.py
|
|
29
32
|
flag_engine/segments/types.py
|
|
33
|
+
flag_engine/types/__init__.py
|
|
30
34
|
flag_engine/utils/__init__.py
|
|
31
35
|
flag_engine/utils/datetime.py
|
|
32
36
|
flag_engine/utils/exceptions.py
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import operator
|
|
2
|
-
import re
|
|
3
|
-
import typing
|
|
4
|
-
from contextlib import suppress
|
|
5
|
-
from functools import wraps
|
|
6
|
-
|
|
7
|
-
import semver
|
|
8
|
-
|
|
9
|
-
from flag_engine.environments.models import EnvironmentModel
|
|
10
|
-
from flag_engine.identities.models import IdentityModel
|
|
11
|
-
from flag_engine.identities.traits.models import TraitModel
|
|
12
|
-
from flag_engine.identities.traits.types import TraitValue
|
|
13
|
-
from flag_engine.segments import constants
|
|
14
|
-
from flag_engine.segments.models import (
|
|
15
|
-
SegmentConditionModel,
|
|
16
|
-
SegmentModel,
|
|
17
|
-
SegmentRuleModel,
|
|
18
|
-
)
|
|
19
|
-
from flag_engine.segments.types import ConditionOperator
|
|
20
|
-
from flag_engine.utils.hashing import get_hashed_percentage_for_object_ids
|
|
21
|
-
from flag_engine.utils.semver import is_semver
|
|
22
|
-
from flag_engine.utils.types import get_casting_function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def get_identity_segments(
|
|
26
|
-
environment: EnvironmentModel,
|
|
27
|
-
identity: IdentityModel,
|
|
28
|
-
override_traits: typing.Optional[typing.List[TraitModel]] = None,
|
|
29
|
-
) -> typing.List[SegmentModel]:
|
|
30
|
-
return list(
|
|
31
|
-
filter(
|
|
32
|
-
lambda s: evaluate_identity_in_segment(identity, s, override_traits),
|
|
33
|
-
environment.project.segments,
|
|
34
|
-
)
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def evaluate_identity_in_segment(
|
|
39
|
-
identity: IdentityModel,
|
|
40
|
-
segment: SegmentModel,
|
|
41
|
-
override_traits: typing.Optional[typing.List[TraitModel]] = None,
|
|
42
|
-
) -> bool:
|
|
43
|
-
"""
|
|
44
|
-
Evaluates whether a given identity is in the provided segment.
|
|
45
|
-
|
|
46
|
-
:param identity: identity model object to evaluate
|
|
47
|
-
:param segment: segment model object to evaluate
|
|
48
|
-
:param override_traits: pass in a list of traits to use instead of those on the
|
|
49
|
-
identity model itself
|
|
50
|
-
:return: True if the identity is in the segment, False otherwise
|
|
51
|
-
"""
|
|
52
|
-
return len(segment.rules) > 0 and all(
|
|
53
|
-
_traits_match_segment_rule(
|
|
54
|
-
override_traits or identity.identity_traits,
|
|
55
|
-
rule,
|
|
56
|
-
segment.id,
|
|
57
|
-
identity.django_id or identity.composite_key,
|
|
58
|
-
)
|
|
59
|
-
for rule in segment.rules
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _traits_match_segment_rule(
|
|
64
|
-
identity_traits: typing.List[TraitModel],
|
|
65
|
-
rule: SegmentRuleModel,
|
|
66
|
-
segment_id: typing.Union[int, str],
|
|
67
|
-
identity_id: typing.Union[int, str],
|
|
68
|
-
) -> bool:
|
|
69
|
-
matches_conditions = (
|
|
70
|
-
rule.matching_function(
|
|
71
|
-
[
|
|
72
|
-
_traits_match_segment_condition(
|
|
73
|
-
identity_traits, condition, segment_id, identity_id
|
|
74
|
-
)
|
|
75
|
-
for condition in rule.conditions
|
|
76
|
-
]
|
|
77
|
-
)
|
|
78
|
-
if len(rule.conditions) > 0
|
|
79
|
-
else True
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
return matches_conditions and all(
|
|
83
|
-
_traits_match_segment_rule(identity_traits, rule, segment_id, identity_id)
|
|
84
|
-
for rule in rule.rules
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _traits_match_segment_condition(
|
|
89
|
-
identity_traits: typing.List[TraitModel],
|
|
90
|
-
condition: SegmentConditionModel,
|
|
91
|
-
segment_id: typing.Union[int, str],
|
|
92
|
-
identity_id: typing.Union[int, str],
|
|
93
|
-
) -> bool:
|
|
94
|
-
if condition.operator == constants.PERCENTAGE_SPLIT:
|
|
95
|
-
assert condition.value
|
|
96
|
-
float_value = float(condition.value)
|
|
97
|
-
return (
|
|
98
|
-
get_hashed_percentage_for_object_ids([segment_id, identity_id])
|
|
99
|
-
<= float_value
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
trait = next(
|
|
103
|
-
filter(lambda t: t.trait_key == condition.property_, identity_traits), None
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
if condition.operator == constants.IS_NOT_SET:
|
|
107
|
-
return trait is None
|
|
108
|
-
|
|
109
|
-
if condition.operator == constants.IS_SET:
|
|
110
|
-
return trait is not None
|
|
111
|
-
|
|
112
|
-
return _matches_trait_value(condition, trait.trait_value) if trait else False
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _matches_trait_value(
|
|
116
|
-
condition: SegmentConditionModel,
|
|
117
|
-
trait_value: TraitValue,
|
|
118
|
-
) -> bool:
|
|
119
|
-
if match_func := MATCH_FUNCS_BY_OPERATOR.get(condition.operator):
|
|
120
|
-
return match_func(condition.value, trait_value)
|
|
121
|
-
|
|
122
|
-
return False
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _evaluate_not_contains(
|
|
126
|
-
segment_value: typing.Optional[str],
|
|
127
|
-
trait_value: TraitValue,
|
|
128
|
-
) -> bool:
|
|
129
|
-
return isinstance(trait_value, str) and str(segment_value) not in trait_value
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _evaluate_regex(
|
|
133
|
-
segment_value: typing.Optional[str],
|
|
134
|
-
trait_value: TraitValue,
|
|
135
|
-
) -> bool:
|
|
136
|
-
return (
|
|
137
|
-
trait_value is not None
|
|
138
|
-
and re.compile(str(segment_value)).match(str(trait_value)) is not None
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def _evaluate_modulo(
|
|
143
|
-
segment_value: typing.Optional[str],
|
|
144
|
-
trait_value: TraitValue,
|
|
145
|
-
) -> bool:
|
|
146
|
-
if not isinstance(trait_value, (int, float)):
|
|
147
|
-
return False
|
|
148
|
-
|
|
149
|
-
if segment_value is None:
|
|
150
|
-
return False
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
divisor_part, remainder_part = segment_value.split("|")
|
|
154
|
-
divisor = float(divisor_part)
|
|
155
|
-
remainder = float(remainder_part)
|
|
156
|
-
except ValueError:
|
|
157
|
-
return False
|
|
158
|
-
|
|
159
|
-
return trait_value % divisor == remainder
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _evaluate_in(segment_value: typing.Optional[str], trait_value: TraitValue) -> bool:
|
|
163
|
-
if segment_value:
|
|
164
|
-
if isinstance(trait_value, str):
|
|
165
|
-
return trait_value in segment_value.split(",")
|
|
166
|
-
if isinstance(trait_value, int) and not any(
|
|
167
|
-
trait_value is x for x in (False, True)
|
|
168
|
-
):
|
|
169
|
-
return str(trait_value) in segment_value.split(",")
|
|
170
|
-
return False
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _trait_value_typed(
|
|
174
|
-
func: typing.Callable[..., bool],
|
|
175
|
-
) -> typing.Callable[[typing.Optional[str], TraitValue], bool]:
|
|
176
|
-
@wraps(func)
|
|
177
|
-
def inner(
|
|
178
|
-
segment_value: typing.Optional[str],
|
|
179
|
-
trait_value: typing.Union[TraitValue, semver.Version],
|
|
180
|
-
) -> bool:
|
|
181
|
-
with suppress(TypeError, ValueError):
|
|
182
|
-
if isinstance(trait_value, str) and is_semver(segment_value):
|
|
183
|
-
trait_value = semver.Version.parse(
|
|
184
|
-
trait_value,
|
|
185
|
-
)
|
|
186
|
-
match_value = get_casting_function(trait_value)(segment_value)
|
|
187
|
-
return func(trait_value, match_value)
|
|
188
|
-
return False
|
|
189
|
-
|
|
190
|
-
return inner
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
MATCH_FUNCS_BY_OPERATOR: typing.Dict[
|
|
194
|
-
ConditionOperator, typing.Callable[[typing.Optional[str], TraitValue], bool]
|
|
195
|
-
] = {
|
|
196
|
-
constants.NOT_CONTAINS: _evaluate_not_contains,
|
|
197
|
-
constants.REGEX: _evaluate_regex,
|
|
198
|
-
constants.MODULO: _evaluate_modulo,
|
|
199
|
-
constants.IN: _evaluate_in,
|
|
200
|
-
constants.EQUAL: _trait_value_typed(operator.eq),
|
|
201
|
-
constants.GREATER_THAN: _trait_value_typed(operator.gt),
|
|
202
|
-
constants.GREATER_THAN_INCLUSIVE: _trait_value_typed(operator.ge),
|
|
203
|
-
constants.LESS_THAN: _trait_value_typed(operator.lt),
|
|
204
|
-
constants.LESS_THAN_INCLUSIVE: _trait_value_typed(operator.le),
|
|
205
|
-
constants.NOT_EQUAL: _trait_value_typed(operator.ne),
|
|
206
|
-
constants.CONTAINS: _trait_value_typed(operator.contains),
|
|
207
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/environments/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/features/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/identities/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/segments/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/json/encoders.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|