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.
Files changed (42) hide show
  1. flag_engine/__init__.py +0 -0
  2. flag_engine/context/__init__.py +0 -0
  3. flag_engine/context/mappers.py +39 -0
  4. flag_engine/context/types.py +28 -0
  5. flag_engine/engine.py +146 -0
  6. flag_engine/environments/__init__.py +0 -0
  7. flag_engine/environments/integrations/__init__.py +0 -0
  8. flag_engine/environments/integrations/models.py +9 -0
  9. flag_engine/environments/models.py +95 -0
  10. flag_engine/features/__init__.py +0 -0
  11. flag_engine/features/constants.py +3 -0
  12. flag_engine/features/models.py +159 -0
  13. flag_engine/identities/__init__.py +0 -0
  14. flag_engine/identities/models.py +93 -0
  15. flag_engine/identities/traits/__init__.py +0 -0
  16. flag_engine/identities/traits/constants.py +1 -0
  17. flag_engine/identities/traits/models.py +8 -0
  18. flag_engine/identities/traits/types.py +62 -0
  19. flag_engine/organisations/__init__.py +0 -0
  20. flag_engine/organisations/models.py +13 -0
  21. flag_engine/projects/__init__.py +0 -0
  22. flag_engine/projects/models.py +16 -0
  23. flag_engine/py.typed +0 -0
  24. flag_engine/segments/__init__.py +0 -0
  25. flag_engine/segments/constants.py +22 -0
  26. flag_engine/segments/evaluator.py +233 -0
  27. flag_engine/segments/models.py +41 -0
  28. flag_engine/segments/types.py +24 -0
  29. flag_engine/types/__init__.py +0 -0
  30. flag_engine/utils/__init__.py +0 -0
  31. flag_engine/utils/datetime.py +5 -0
  32. flag_engine/utils/exceptions.py +10 -0
  33. flag_engine/utils/hashing.py +33 -0
  34. flag_engine/utils/json/__init__.py +0 -0
  35. flag_engine/utils/json/encoders.py +16 -0
  36. flag_engine/utils/semver.py +26 -0
  37. flag_engine/utils/types.py +46 -0
  38. flagsmith_flag_engine-6.0.1.dist-info/METADATA +50 -0
  39. flagsmith_flag_engine-6.0.1.dist-info/RECORD +42 -0
  40. flagsmith_flag_engine-6.0.1.dist-info/WHEEL +5 -0
  41. flagsmith_flag_engine-6.0.1.dist-info/licenses/LICENSE.txt +12 -0
  42. 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,5 @@
1
+ from datetime import datetime, timezone
2
+
3
+
4
+ def utcnow_with_tz() -> datetime:
5
+ return datetime.now(tz=timezone.utc)
@@ -0,0 +1,10 @@
1
+ class FeatureStateNotFound(Exception):
2
+ pass
3
+
4
+
5
+ class DuplicateFeatureState(ValueError):
6
+ pass
7
+
8
+
9
+ class InvalidPercentageAllocation(ValueError):
10
+ pass
@@ -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
+ [![Feature Flag, Remote Config and A/B Testing platform, Flagsmith](https://github.com/Flagsmith/flagsmith/raw/main/static-files/hero.png)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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