flagsmith-flag-engine 6.0.1__py3-none-any.whl
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.
- flag_engine/__init__.py +0 -0
- flag_engine/context/__init__.py +0 -0
- flag_engine/context/mappers.py +39 -0
- flag_engine/context/types.py +28 -0
- flag_engine/engine.py +146 -0
- flag_engine/environments/__init__.py +0 -0
- flag_engine/environments/integrations/__init__.py +0 -0
- flag_engine/environments/integrations/models.py +9 -0
- flag_engine/environments/models.py +95 -0
- flag_engine/features/__init__.py +0 -0
- flag_engine/features/constants.py +3 -0
- flag_engine/features/models.py +159 -0
- flag_engine/identities/__init__.py +0 -0
- flag_engine/identities/models.py +93 -0
- flag_engine/identities/traits/__init__.py +0 -0
- flag_engine/identities/traits/constants.py +1 -0
- flag_engine/identities/traits/models.py +8 -0
- flag_engine/identities/traits/types.py +62 -0
- flag_engine/organisations/__init__.py +0 -0
- flag_engine/organisations/models.py +13 -0
- flag_engine/projects/__init__.py +0 -0
- flag_engine/projects/models.py +16 -0
- flag_engine/py.typed +0 -0
- flag_engine/segments/__init__.py +0 -0
- flag_engine/segments/constants.py +22 -0
- flag_engine/segments/evaluator.py +233 -0
- flag_engine/segments/models.py +41 -0
- flag_engine/segments/types.py +24 -0
- flag_engine/types/__init__.py +0 -0
- flag_engine/utils/__init__.py +0 -0
- flag_engine/utils/datetime.py +5 -0
- flag_engine/utils/exceptions.py +10 -0
- flag_engine/utils/hashing.py +33 -0
- flag_engine/utils/json/__init__.py +0 -0
- flag_engine/utils/json/encoders.py +16 -0
- flag_engine/utils/semver.py +26 -0
- flag_engine/utils/types.py +46 -0
- flagsmith_flag_engine-6.0.1.dist-info/METADATA +50 -0
- flagsmith_flag_engine-6.0.1.dist-info/RECORD +42 -0
- flagsmith_flag_engine-6.0.1.dist-info/WHEEL +5 -0
- flagsmith_flag_engine-6.0.1.dist-info/licenses/LICENSE.txt +12 -0
- flagsmith_flag_engine-6.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from flag_engine.organisations.models import OrganisationModel
|
|
6
|
+
from flag_engine.segments.models import SegmentModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProjectModel(BaseModel):
|
|
10
|
+
id: int
|
|
11
|
+
name: str
|
|
12
|
+
organisation: OrganisationModel
|
|
13
|
+
hide_disabled_flags: bool = False
|
|
14
|
+
segments: typing.List[SegmentModel] = Field(default_factory=list)
|
|
15
|
+
enable_realtime_updates: bool = False
|
|
16
|
+
server_key_only_feature_ids: typing.List[int] = Field(default_factory=list)
|
flag_engine/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from flag_engine.segments.types import ConditionOperator, RuleType
|
|
2
|
+
|
|
3
|
+
# Segment Rules
|
|
4
|
+
ALL_RULE: RuleType = "ALL"
|
|
5
|
+
ANY_RULE: RuleType = "ANY"
|
|
6
|
+
NONE_RULE: RuleType = "NONE"
|
|
7
|
+
|
|
8
|
+
# Segment Condition Operators
|
|
9
|
+
EQUAL: ConditionOperator = "EQUAL"
|
|
10
|
+
GREATER_THAN: ConditionOperator = "GREATER_THAN"
|
|
11
|
+
LESS_THAN: ConditionOperator = "LESS_THAN"
|
|
12
|
+
LESS_THAN_INCLUSIVE: ConditionOperator = "LESS_THAN_INCLUSIVE"
|
|
13
|
+
CONTAINS: ConditionOperator = "CONTAINS"
|
|
14
|
+
GREATER_THAN_INCLUSIVE: ConditionOperator = "GREATER_THAN_INCLUSIVE"
|
|
15
|
+
NOT_CONTAINS: ConditionOperator = "NOT_CONTAINS"
|
|
16
|
+
NOT_EQUAL: ConditionOperator = "NOT_EQUAL"
|
|
17
|
+
REGEX: ConditionOperator = "REGEX"
|
|
18
|
+
PERCENTAGE_SPLIT: ConditionOperator = "PERCENTAGE_SPLIT"
|
|
19
|
+
MODULO: ConditionOperator = "MODULO"
|
|
20
|
+
IS_SET: ConditionOperator = "IS_SET"
|
|
21
|
+
IS_NOT_SET: ConditionOperator = "IS_NOT_SET"
|
|
22
|
+
IN: ConditionOperator = "IN"
|
|
@@ -0,0 +1,233 @@
|
|
|
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 (
|
|
104
|
+
_matches_context_value(condition, context_value)
|
|
105
|
+
if context_value is not None
|
|
106
|
+
else False
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _get_trait(context: EvaluationContext, trait_key: str) -> ContextValue:
|
|
111
|
+
return (
|
|
112
|
+
identity_context["traits"][trait_key]
|
|
113
|
+
if (identity_context := context["identity"])
|
|
114
|
+
else None
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_context_value(
|
|
119
|
+
context: EvaluationContext,
|
|
120
|
+
property: str,
|
|
121
|
+
) -> ContextValue:
|
|
122
|
+
getter = CONTEXT_VALUE_GETTERS_BY_PROPERTY.get(property) or partial(
|
|
123
|
+
_get_trait,
|
|
124
|
+
trait_key=property,
|
|
125
|
+
)
|
|
126
|
+
try:
|
|
127
|
+
return getter(context)
|
|
128
|
+
except KeyError:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _matches_context_value(
|
|
133
|
+
condition: SegmentConditionModel,
|
|
134
|
+
context_value: ContextValue,
|
|
135
|
+
) -> bool:
|
|
136
|
+
if matcher := MATCHERS_BY_OPERATOR.get(condition.operator):
|
|
137
|
+
return matcher(condition.value, context_value)
|
|
138
|
+
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _evaluate_not_contains(
|
|
143
|
+
segment_value: typing.Optional[str],
|
|
144
|
+
context_value: ContextValue,
|
|
145
|
+
) -> bool:
|
|
146
|
+
return isinstance(context_value, str) and str(segment_value) not in context_value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _evaluate_regex(
|
|
150
|
+
segment_value: typing.Optional[str],
|
|
151
|
+
context_value: ContextValue,
|
|
152
|
+
) -> bool:
|
|
153
|
+
return (
|
|
154
|
+
context_value is not None
|
|
155
|
+
and re.compile(str(segment_value)).match(str(context_value)) is not None
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _evaluate_modulo(
|
|
160
|
+
segment_value: typing.Optional[str],
|
|
161
|
+
context_value: ContextValue,
|
|
162
|
+
) -> bool:
|
|
163
|
+
if not isinstance(context_value, (int, float)):
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
if segment_value is None:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
divisor_part, remainder_part = segment_value.split("|")
|
|
171
|
+
divisor = float(divisor_part)
|
|
172
|
+
remainder = float(remainder_part)
|
|
173
|
+
except ValueError:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
return context_value % divisor == remainder
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _evaluate_in(
|
|
180
|
+
segment_value: typing.Optional[str], context_value: ContextValue
|
|
181
|
+
) -> bool:
|
|
182
|
+
if segment_value:
|
|
183
|
+
if isinstance(context_value, str):
|
|
184
|
+
return context_value in segment_value.split(",")
|
|
185
|
+
if isinstance(context_value, int) and not any(
|
|
186
|
+
context_value is x for x in (False, True)
|
|
187
|
+
):
|
|
188
|
+
return str(context_value) in segment_value.split(",")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _context_value_typed(
|
|
193
|
+
func: typing.Callable[..., bool],
|
|
194
|
+
) -> typing.Callable[[typing.Optional[str], ContextValue], bool]:
|
|
195
|
+
@wraps(func)
|
|
196
|
+
def inner(
|
|
197
|
+
segment_value: typing.Optional[str],
|
|
198
|
+
context_value: typing.Union[ContextValue, semver.Version],
|
|
199
|
+
) -> bool:
|
|
200
|
+
with suppress(TypeError, ValueError):
|
|
201
|
+
if isinstance(context_value, str) and is_semver(segment_value):
|
|
202
|
+
context_value = semver.Version.parse(
|
|
203
|
+
context_value,
|
|
204
|
+
)
|
|
205
|
+
match_value = get_casting_function(context_value)(segment_value)
|
|
206
|
+
return func(context_value, match_value)
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
return inner
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
MATCHERS_BY_OPERATOR: typing.Dict[
|
|
213
|
+
ConditionOperator, typing.Callable[[typing.Optional[str], ContextValue], bool]
|
|
214
|
+
] = {
|
|
215
|
+
constants.NOT_CONTAINS: _evaluate_not_contains,
|
|
216
|
+
constants.REGEX: _evaluate_regex,
|
|
217
|
+
constants.MODULO: _evaluate_modulo,
|
|
218
|
+
constants.IN: _evaluate_in,
|
|
219
|
+
constants.EQUAL: _context_value_typed(operator.eq),
|
|
220
|
+
constants.GREATER_THAN: _context_value_typed(operator.gt),
|
|
221
|
+
constants.GREATER_THAN_INCLUSIVE: _context_value_typed(operator.ge),
|
|
222
|
+
constants.LESS_THAN: _context_value_typed(operator.lt),
|
|
223
|
+
constants.LESS_THAN_INCLUSIVE: _context_value_typed(operator.le),
|
|
224
|
+
constants.NOT_EQUAL: _context_value_typed(operator.ne),
|
|
225
|
+
constants.CONTAINS: _context_value_typed(operator.contains),
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
CONTEXT_VALUE_GETTERS_BY_PROPERTY = {
|
|
230
|
+
"$.identity.identifier": lambda context: context["identity"]["identifier"],
|
|
231
|
+
"$.identity.key": lambda context: context["identity"]["key"],
|
|
232
|
+
"$.environment.name": lambda context: context["environment"]["name"],
|
|
233
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, BeforeValidator, Field
|
|
4
|
+
from typing_extensions import Annotated
|
|
5
|
+
|
|
6
|
+
from flag_engine.features.models import FeatureStateModel
|
|
7
|
+
from flag_engine.segments import constants
|
|
8
|
+
from flag_engine.segments.types import ConditionOperator, RuleType
|
|
9
|
+
|
|
10
|
+
LaxStr = Annotated[str, BeforeValidator(lambda x: str(x))]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SegmentConditionModel(BaseModel):
|
|
14
|
+
operator: ConditionOperator
|
|
15
|
+
value: typing.Optional[LaxStr] = None
|
|
16
|
+
property_: typing.Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SegmentRuleModel(BaseModel):
|
|
20
|
+
type: RuleType
|
|
21
|
+
rules: typing.List["SegmentRuleModel"] = Field(default_factory=list)
|
|
22
|
+
conditions: typing.List[SegmentConditionModel] = Field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def none(iterable: typing.Iterable[object]) -> bool:
|
|
26
|
+
return not any(iterable)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def matching_function(self) -> typing.Callable[[typing.Iterable[object]], bool]:
|
|
30
|
+
return {
|
|
31
|
+
constants.ANY_RULE: any,
|
|
32
|
+
constants.ALL_RULE: all,
|
|
33
|
+
constants.NONE_RULE: SegmentRuleModel.none,
|
|
34
|
+
}[self.type]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SegmentModel(BaseModel):
|
|
38
|
+
id: int
|
|
39
|
+
name: str
|
|
40
|
+
rules: typing.List[SegmentRuleModel] = Field(default_factory=list)
|
|
41
|
+
feature_states: typing.List[FeatureStateModel] = Field(default_factory=list)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
ConditionOperator = Literal[
|
|
4
|
+
"EQUAL",
|
|
5
|
+
"GREATER_THAN",
|
|
6
|
+
"LESS_THAN",
|
|
7
|
+
"LESS_THAN_INCLUSIVE",
|
|
8
|
+
"CONTAINS",
|
|
9
|
+
"GREATER_THAN_INCLUSIVE",
|
|
10
|
+
"NOT_CONTAINS",
|
|
11
|
+
"NOT_EQUAL",
|
|
12
|
+
"REGEX",
|
|
13
|
+
"PERCENTAGE_SPLIT",
|
|
14
|
+
"MODULO",
|
|
15
|
+
"IS_SET",
|
|
16
|
+
"IS_NOT_SET",
|
|
17
|
+
"IN",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
RuleType = Literal[
|
|
21
|
+
"ALL",
|
|
22
|
+
"ANY",
|
|
23
|
+
"NONE",
|
|
24
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from flag_engine.utils.types import SupportsStr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_hashed_percentage_for_object_ids(
|
|
8
|
+
object_ids: typing.Iterable[SupportsStr], iterations: int = 1
|
|
9
|
+
) -> float:
|
|
10
|
+
"""
|
|
11
|
+
Given a list of object ids, get a floating point number between 0 (inclusive) and
|
|
12
|
+
100 (exclusive) based on the hash of those ids. This should give the same value
|
|
13
|
+
every time for any list of ids.
|
|
14
|
+
|
|
15
|
+
:param object_ids: list of object ids to calculate the hash for
|
|
16
|
+
:param iterations: num times to include each id in the generated string to hash
|
|
17
|
+
:return: (float) number between 0 (inclusive) and 100 (exclusive)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
to_hash = ",".join(str(id_) for id_ in list(object_ids) * iterations)
|
|
21
|
+
hashed_value = hashlib.md5(to_hash.encode("utf-8"))
|
|
22
|
+
hashed_value_as_int = int(hashed_value.hexdigest(), base=16)
|
|
23
|
+
value = ((hashed_value_as_int % 9999) / 9998) * 100
|
|
24
|
+
|
|
25
|
+
if value == 100:
|
|
26
|
+
# since we want a number between 0 (inclusive) and 100 (exclusive), in the
|
|
27
|
+
# unlikely case that we get the exact number 100, we call the method again
|
|
28
|
+
# and increase the number of iterations to ensure we get a different result
|
|
29
|
+
return get_hashed_percentage_for_object_ids(
|
|
30
|
+
object_ids=object_ids, iterations=iterations + 1
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return value
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import decimal
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DecimalEncoder(json.JSONEncoder):
|
|
6
|
+
"""
|
|
7
|
+
Convert decimal to int/float because decimals are nothing but
|
|
8
|
+
int/float(for us) converted to decimal by boto3/dynamodb.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def default(self, obj: object) -> object:
|
|
12
|
+
if isinstance(obj, decimal.Decimal):
|
|
13
|
+
if obj % 1 == 0:
|
|
14
|
+
return int(obj)
|
|
15
|
+
return float(obj)
|
|
16
|
+
return json.JSONEncoder.default(self, obj)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import semver
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_semver(value: Optional[str]) -> bool:
|
|
7
|
+
"""
|
|
8
|
+
Checks if the given string have `:semver` suffix or not
|
|
9
|
+
>>> is_semver("2.1.41-beta:semver")
|
|
10
|
+
True
|
|
11
|
+
>>> is_semver("2.1.41-beta")
|
|
12
|
+
False
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
return value is not None and value[-7:] == ":semver"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def remove_semver_suffix(value: semver.Version) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Remove the semver suffix(i.e: last 7 characters) from the given value
|
|
21
|
+
>>> remove_semver_suffix("2.1.41-beta:semver")
|
|
22
|
+
'2.1.41-beta'
|
|
23
|
+
>>> remove_semver_suffix("2.1.41:semver")
|
|
24
|
+
'2.1.41'
|
|
25
|
+
"""
|
|
26
|
+
return str(value)[:-7]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from functools import singledispatch
|
|
3
|
+
|
|
4
|
+
import semver
|
|
5
|
+
|
|
6
|
+
from flag_engine.identities.traits.types import ContextValue
|
|
7
|
+
from flag_engine.utils.semver import remove_semver_suffix
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SupportsStr(typing.Protocol):
|
|
11
|
+
def __str__(self) -> str: # pragma: no cover
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@singledispatch
|
|
16
|
+
def get_casting_function(
|
|
17
|
+
input_: object,
|
|
18
|
+
) -> typing.Callable[..., ContextValue]:
|
|
19
|
+
"""
|
|
20
|
+
This function returns a callable to cast a value to the same type as input_
|
|
21
|
+
>>> assert get_casting_function("a string") == str
|
|
22
|
+
>>> assert get_casting_function(10) == int
|
|
23
|
+
>>> assert get_casting_function(1.2) == float
|
|
24
|
+
>>> assert get_casting_function(semver.Version.parse("3.4.5")) == remove_semver_suffix
|
|
25
|
+
"""
|
|
26
|
+
return str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@get_casting_function.register
|
|
30
|
+
def _(input_: bool) -> typing.Callable[..., bool]:
|
|
31
|
+
return lambda v: v not in ("False", "false")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@get_casting_function.register
|
|
35
|
+
def _(input_: int) -> typing.Callable[..., int]:
|
|
36
|
+
return int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@get_casting_function.register
|
|
40
|
+
def _(input_: float) -> typing.Callable[..., float]:
|
|
41
|
+
return float
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@get_casting_function.register
|
|
45
|
+
def _(input_: semver.Version) -> typing.Callable[..., str]:
|
|
46
|
+
return remove_semver_suffix
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flagsmith-flag-engine
|
|
3
|
+
Version: 6.0.1
|
|
4
|
+
Summary: Flag engine for the Flagsmith API.
|
|
5
|
+
Home-page: https://github.com/Flagsmith/flagsmith-engine
|
|
6
|
+
Author: Flagsmith
|
|
7
|
+
Author-email: support@flagsmith.com
|
|
8
|
+
License: BSD3
|
|
9
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE.txt
|
|
16
|
+
Requires-Dist: pydantic<3,>=2.3.0
|
|
17
|
+
Requires-Dist: pydantic-collections<1,>=0.5.1
|
|
18
|
+
Requires-Dist: semver>=3.0.1
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
[](https://www.flagsmith.com/)
|
|
31
|
+
|
|
32
|
+
[Flagsmith](https://www.flagsmith.com/) is an open source, fully featured, Feature Flag and Remote Config service. Use
|
|
33
|
+
our hosted API, deploy to your own private cloud, or run on-premise.
|
|
34
|
+
|
|
35
|
+
# Flagsmith Flag Engine
|
|
36
|
+
|
|
37
|
+
This project powers the core [Flagsmith API](https://github.com/Flagsmith/flagsmith-api) flag evaluations engine.
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m venv .venv
|
|
43
|
+
source .venv/bin/activate
|
|
44
|
+
python -m pip install -r requirements-dev.txt
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Design
|
|
48
|
+
|
|
49
|
+
- Marshmallow Schemas
|
|
50
|
+
- Plain Python
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
flag_engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
flag_engine/engine.py,sha256=IyA2wgUx5wojlRqHXI5rjyoZFafcbFQvZeuPicyfnCg,5244
|
|
3
|
+
flag_engine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
flag_engine/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
flag_engine/context/mappers.py,sha256=cYQx3VLmTjfvr42v39IgcQkv6Db6sfLwwcwA7_LTf3c,1394
|
|
6
|
+
flag_engine/context/types.py,sha256=D7pEyELvR15kLZ2OuaZUhtdGoRhXzLXGNj3_aVPk-yQ,772
|
|
7
|
+
flag_engine/environments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
flag_engine/environments/models.py,sha256=bBiITUVX-yQrcBG9BuVMAXSklKegrRHZ2-UyZ6a-omY,3198
|
|
9
|
+
flag_engine/environments/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
flag_engine/environments/integrations/models.py,sha256=_LC_3XOAN9tEhz4wEB7HCxD3NX5si046C6H0mZFA8e4,208
|
|
11
|
+
flag_engine/features/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
flag_engine/features/constants.py,sha256=oinYCtAUD6jw09wVuBSDC0R2CndEZkjXMUXCck1DuXo,68
|
|
13
|
+
flag_engine/features/models.py,sha256=BaW1LAiwSBrND16WAzJgsM0lhRKrwQ4MyBTgxYzqZ9U,5757
|
|
14
|
+
flag_engine/identities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
flag_engine/identities/models.py,sha256=9Si3uPWoMSXhd_nzuKCZI3bBPHrE-jDUzG9OPg66JP4,3440
|
|
16
|
+
flag_engine/identities/traits/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
flag_engine/identities/traits/constants.py,sha256=pQHoyiBxx7mwO0x3jO029Z4P_vvR0FBOLgFGNfbT6xk,42
|
|
18
|
+
flag_engine/identities/traits/models.py,sha256=NOWvvwJgNweEe9WdcBBjFEkpPECf48nX91sID73vXTo,193
|
|
19
|
+
flag_engine/identities/traits/types.py,sha256=iQhuxZnY2Q81YZXbZW9n4rdf0t34tmEH1Ufh0MIKdlw,1936
|
|
20
|
+
flag_engine/organisations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
flag_engine/organisations/models.py,sha256=AvYhkDv6Tb8241HrgpKY0kqw8XaIN42LTdA0N348uXA,299
|
|
22
|
+
flag_engine/projects/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
flag_engine/projects/models.py,sha256=Y4IKMwf1EKbSohamzO8cvKssrw8WyXggR44KZLkA77g,495
|
|
24
|
+
flag_engine/segments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
flag_engine/segments/constants.py,sha256=nu73rNXNvRmtHYOwVeQcXLYD3C1L6_FAK93EoqhblpQ,830
|
|
26
|
+
flag_engine/segments/evaluator.py,sha256=CyPnepaJOBjTR11PkF2IRGQ2yLvHwtOhOIsJD1cllq4,6719
|
|
27
|
+
flag_engine/segments/models.py,sha256=gABacer5WJgMEu68ndNairyPkR7v7XsAh29NHTjtNmY,1296
|
|
28
|
+
flag_engine/segments/types.py,sha256=G17HPxcI_hGWzUQPvP4XAp5xTl9Xe2K0qW3ewOGD8P4,369
|
|
29
|
+
flag_engine/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
flag_engine/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
flag_engine/utils/datetime.py,sha256=f2KMnR3jFwXyUz7UhAhLKY1UyxnKroEhY5KT2hjgt8g,117
|
|
32
|
+
flag_engine/utils/exceptions.py,sha256=6T7W7QuOshNDWQv1JWzIzPoKLUjKB4vhNJzD6YbJng4,158
|
|
33
|
+
flag_engine/utils/hashing.py,sha256=tCaDV4MSUpD9jclJ86VAYuwE2-EAkkuEfQLJgORETEE,1311
|
|
34
|
+
flag_engine/utils/semver.py,sha256=aMW-f3wkGJGXCC9AruQc1SSKO7RgQs8BAIsaSAGzN4U,619
|
|
35
|
+
flag_engine/utils/types.py,sha256=HJ0ZqoG8pZVE9n13_aVXCDwqy_PNZOsnNIsPsaPaoDk,1203
|
|
36
|
+
flag_engine/utils/json/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
flag_engine/utils/json/encoders.py,sha256=MYXmhgJ6stSt0ffQDP3DWFh3m_G_fwN-vImkTJF_M0g,447
|
|
38
|
+
flagsmith_flag_engine-6.0.1.dist-info/licenses/LICENSE.txt,sha256=ODfaqV7JJbGSIlIuOA-GHHyc4cGi-QWinwCa2O5nse0,1546
|
|
39
|
+
flagsmith_flag_engine-6.0.1.dist-info/METADATA,sha256=WoVrBt-BB3vHzAl8pSjaDNJMJK-mMWBtjOevxyDXhhY,1532
|
|
40
|
+
flagsmith_flag_engine-6.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
41
|
+
flagsmith_flag_engine-6.0.1.dist-info/top_level.txt,sha256=mmD1_GUabsUgkPJ9i9GU5NSOG4dlu9KTViP0ypclpVI,12
|
|
42
|
+
flagsmith_flag_engine-6.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (c) 2021 Bullet Train Ltd (https://www.flagsmith.com/) and individual contributors.
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
7
|
+
|
|
8
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
9
|
+
|
|
10
|
+
3. Neither the name of the Sentry nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
11
|
+
|
|
12
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flag_engine
|