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.
Files changed (49) hide show
  1. {flagsmith-flag-engine-5.3.0/flagsmith_flag_engine.egg-info → flagsmith-flag-engine-5.4.0}/PKG-INFO +1 -4
  2. flagsmith-flag-engine-5.4.0/flag_engine/context/mappers.py +39 -0
  3. flagsmith-flag-engine-5.4.0/flag_engine/context/types.py +28 -0
  4. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/engine.py +41 -19
  5. flagsmith-flag-engine-5.4.0/flag_engine/identities/traits/models.py +8 -0
  6. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/identities/traits/types.py +6 -6
  7. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/organisations/models.py +3 -3
  8. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/projects/models.py +1 -1
  9. flagsmith-flag-engine-5.4.0/flag_engine/segments/evaluator.py +229 -0
  10. flagsmith-flag-engine-5.4.0/flag_engine/utils/__init__.py +0 -0
  11. flagsmith-flag-engine-5.4.0/flag_engine/utils/json/__init__.py +0 -0
  12. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/types.py +2 -2
  13. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0/flagsmith_flag_engine.egg-info}/PKG-INFO +1 -4
  14. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flagsmith_flag_engine.egg-info/SOURCES.txt +4 -0
  15. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/setup.py +1 -1
  16. flagsmith-flag-engine-5.3.0/flag_engine/identities/traits/models.py +0 -8
  17. flagsmith-flag-engine-5.3.0/flag_engine/segments/evaluator.py +0 -207
  18. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/LICENSE.txt +0 -0
  19. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/README.md +0 -0
  20. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/__init__.py +0 -0
  21. {flagsmith-flag-engine-5.3.0/flag_engine/environments → flagsmith-flag-engine-5.4.0/flag_engine/context}/__init__.py +0 -0
  22. {flagsmith-flag-engine-5.3.0/flag_engine/environments/integrations → flagsmith-flag-engine-5.4.0/flag_engine/environments}/__init__.py +0 -0
  23. {flagsmith-flag-engine-5.3.0/flag_engine/features → flagsmith-flag-engine-5.4.0/flag_engine/environments/integrations}/__init__.py +0 -0
  24. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/environments/integrations/models.py +0 -0
  25. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/environments/models.py +0 -0
  26. {flagsmith-flag-engine-5.3.0/flag_engine/identities → flagsmith-flag-engine-5.4.0/flag_engine/features}/__init__.py +0 -0
  27. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/features/constants.py +0 -0
  28. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/features/models.py +0 -0
  29. {flagsmith-flag-engine-5.3.0/flag_engine/identities/traits → flagsmith-flag-engine-5.4.0/flag_engine/identities}/__init__.py +0 -0
  30. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/identities/models.py +0 -0
  31. {flagsmith-flag-engine-5.3.0/flag_engine/organisations → flagsmith-flag-engine-5.4.0/flag_engine/identities/traits}/__init__.py +0 -0
  32. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/identities/traits/constants.py +0 -0
  33. {flagsmith-flag-engine-5.3.0/flag_engine/projects → flagsmith-flag-engine-5.4.0/flag_engine/organisations}/__init__.py +0 -0
  34. {flagsmith-flag-engine-5.3.0/flag_engine/segments → flagsmith-flag-engine-5.4.0/flag_engine/projects}/__init__.py +0 -0
  35. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/py.typed +0 -0
  36. {flagsmith-flag-engine-5.3.0/flag_engine/utils → flagsmith-flag-engine-5.4.0/flag_engine/segments}/__init__.py +0 -0
  37. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/segments/constants.py +0 -0
  38. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/segments/models.py +0 -0
  39. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/segments/types.py +0 -0
  40. {flagsmith-flag-engine-5.3.0/flag_engine/utils/json → flagsmith-flag-engine-5.4.0/flag_engine/types}/__init__.py +0 -0
  41. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/datetime.py +0 -0
  42. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/exceptions.py +0 -0
  43. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/hashing.py +0 -0
  44. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/json/encoders.py +0 -0
  45. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flag_engine/utils/semver.py +0 -0
  46. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flagsmith_flag_engine.egg-info/dependency_links.txt +0 -0
  47. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flagsmith_flag_engine.egg-info/requires.txt +0 -0
  48. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/flagsmith_flag_engine.egg-info/top_level.txt +0 -0
  49. {flagsmith-flag-engine-5.3.0 → flagsmith-flag-engine-5.4.0}/setup.cfg +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flagsmith-flag-engine
3
- Version: 5.3.0
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 get_identity_segments
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, identity, override_traits
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, identity, override_traits
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
- override_traits: typing.Optional[typing.List[TraitModel]],
117
+ context: EvaluationContext,
100
118
  ) -> typing.Dict[FeatureModel, FeatureStateModel]:
101
119
  # Get feature states from the environment
102
- feature_states = {fs.feature: fs for fs in environment.feature_states}
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
- identity_segments = get_identity_segments(environment, identity, override_traits)
106
- for matching_segment in identity_segments:
107
- for feature_state in matching_segment.feature_states:
108
- if feature_state.feature in feature_states:
109
- if feature_states[feature_state.feature].is_higher_segment_priority(
110
- feature_state
111
- ):
112
- continue
113
- feature_states[feature_state.feature] = feature_state
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
- feature_states.update(
137
+ feature_states_by_feature.update(
117
138
  {
118
- fs.feature: fs
119
- for fs in identity.identity_features
120
- if fs.feature in feature_states
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 feature_states
146
+ return feature_states_by_feature
@@ -0,0 +1,8 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from flag_engine.identities.traits.types import ContextValue
4
+
5
+
6
+ class TraitModel(BaseModel):
7
+ trait_key: str
8
+ trait_value: ContextValue = Field(...)
@@ -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
- _UnconstrainedTraitValue = Union[None, int, float, bool, str]
11
+ _UnconstrainedContextValue = Union[None, int, float, bool, str]
12
12
 
13
13
 
14
- def map_any_value_to_trait_value(value: Any) -> _UnconstrainedTraitValue:
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) -> _UnconstrainedTraitValue:
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[_UnconstrainedTraitValue]:
48
- return isinstance(value, get_args(_UnconstrainedTraitValue))
47
+ def _is_trait_value(value: Any) -> TypeGuard[_UnconstrainedContextValue]:
48
+ return isinstance(value, get_args(_UnconstrainedContextValue))
49
49
 
50
50
 
51
- TraitValue = Annotated[
51
+ ContextValue = Annotated[
52
52
  Union[
53
53
  None,
54
54
  StrictBool,
@@ -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
+ }
@@ -3,7 +3,7 @@ from functools import singledispatch
3
3
 
4
4
  import semver
5
5
 
6
- from flag_engine.identities.traits.types import TraitValue
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[..., TraitValue]:
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
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flagsmith-flag-engine
3
- Version: 5.3.0
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
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="flagsmith-flag-engine",
5
- version="5.3.0",
5
+ version="5.4.0",
6
6
  author="Flagsmith",
7
7
  author_email="support@flagsmith.com",
8
8
  packages=find_packages(include=["flag_engine", "flag_engine.*"]),
@@ -1,8 +0,0 @@
1
- from pydantic import BaseModel, Field
2
-
3
- from flag_engine.identities.traits.types import TraitValue
4
-
5
-
6
- class TraitModel(BaseModel):
7
- trait_key: str
8
- trait_value: TraitValue = Field(...)
@@ -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
- }