flagsmith 5.2.0__tar.gz → 5.3.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-5.2.0 → flagsmith-5.3.0}/PKG-INFO +2 -2
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/flagsmith.py +34 -7
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/models.py +126 -5
- {flagsmith-5.2.0 → flagsmith-5.3.0}/pyproject.toml +2 -2
- {flagsmith-5.2.0 → flagsmith-5.3.0}/LICENSE +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/README.md +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/__init__.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/analytics.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/api/__init__.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/api/types.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/exceptions.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/mappers.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/offline_handlers.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/polling_manager.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/py.typed +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/streaming_manager.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/types.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/utils/__init__.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/utils/datetime.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/utils/identities.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/version.py +0 -0
- {flagsmith-5.2.0 → flagsmith-5.3.0}/flagsmith/webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flagsmith
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.0
|
|
4
4
|
Summary: Flagsmith Python SDK
|
|
5
5
|
License: BSD3
|
|
6
6
|
License-File: LICENSE
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
-
Requires-Dist: flagsmith-flag-engine (>=10.0.
|
|
19
|
+
Requires-Dist: flagsmith-flag-engine (>=10.0.4,<11.0.0)
|
|
20
20
|
Requires-Dist: iso8601 (>=2.1.0,<3.0.0) ; python_version < "3.11"
|
|
21
21
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
22
22
|
Requires-Dist: requests-futures (>=1.0.1,<2.0.0)
|
|
@@ -21,7 +21,13 @@ from flagsmith.mappers import (
|
|
|
21
21
|
map_segment_results_to_identity_segments,
|
|
22
22
|
resolve_trait_values,
|
|
23
23
|
)
|
|
24
|
-
from flagsmith.models import
|
|
24
|
+
from flagsmith.models import (
|
|
25
|
+
DefaultFlag,
|
|
26
|
+
Flags,
|
|
27
|
+
Segment,
|
|
28
|
+
SegmentOverridesIndex,
|
|
29
|
+
build_segment_overrides_index,
|
|
30
|
+
)
|
|
25
31
|
from flagsmith.offline_handlers import OfflineHandler
|
|
26
32
|
from flagsmith.polling_manager import EnvironmentDataPollingManager
|
|
27
33
|
from flagsmith.streaming_manager import EventStreamManager
|
|
@@ -117,7 +123,8 @@ class Flagsmith:
|
|
|
117
123
|
self._pipeline_analytics_processor: typing.Optional[
|
|
118
124
|
PipelineAnalyticsProcessor
|
|
119
125
|
] = None
|
|
120
|
-
self.
|
|
126
|
+
self.__evaluation_context: typing.Optional[SDKEvaluationContext] = None
|
|
127
|
+
self._segment_overrides_index: SegmentOverridesIndex = {}
|
|
121
128
|
self._environment_updated_at: typing.Optional[datetime] = None
|
|
122
129
|
|
|
123
130
|
# argument validation
|
|
@@ -356,6 +363,26 @@ class Flagsmith:
|
|
|
356
363
|
except (KeyError, TypeError, ValueError):
|
|
357
364
|
logger.exception("Error parsing environment document")
|
|
358
365
|
|
|
366
|
+
@property
|
|
367
|
+
def _evaluation_context(self) -> typing.Optional[SDKEvaluationContext]:
|
|
368
|
+
return self.__evaluation_context
|
|
369
|
+
|
|
370
|
+
@_evaluation_context.setter
|
|
371
|
+
def _evaluation_context(
|
|
372
|
+
self, context: typing.Optional[SDKEvaluationContext]
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Swap in a new evaluation context and rebuild the overrides index.
|
|
375
|
+
|
|
376
|
+
The index maps feature_name -> segments that override it. Built once
|
|
377
|
+
per refresh and reused across every subsequent per-identity lazy
|
|
378
|
+
resolution; rebuilding here keeps it in sync with the current doc
|
|
379
|
+
without any hot-path cost.
|
|
380
|
+
"""
|
|
381
|
+
self.__evaluation_context = context
|
|
382
|
+
self._segment_overrides_index = (
|
|
383
|
+
build_segment_overrides_index(context) if context is not None else {}
|
|
384
|
+
)
|
|
385
|
+
|
|
359
386
|
def _get_headers(
|
|
360
387
|
self,
|
|
361
388
|
environment_key: str,
|
|
@@ -407,12 +434,12 @@ class Flagsmith:
|
|
|
407
434
|
identifier=identifier,
|
|
408
435
|
traits=traits,
|
|
409
436
|
)
|
|
410
|
-
|
|
437
|
+
# Lazy: defer per-feature evaluation until the caller actually reads
|
|
438
|
+
# a flag. Hot for callers that only read one or a few flags out of a
|
|
439
|
+
# large environment.
|
|
440
|
+
return Flags.from_evaluation_context(
|
|
411
441
|
context=context,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return Flags.from_evaluation_result(
|
|
415
|
-
evaluation_result=evaluation_result,
|
|
442
|
+
overrides_index=self._segment_overrides_index,
|
|
416
443
|
analytics_processor=self._analytics_processor,
|
|
417
444
|
default_flag_handler=self.default_flag_handler,
|
|
418
445
|
pipeline_analytics_processor=self._pipeline_analytics_processor,
|
|
@@ -3,9 +3,37 @@ from __future__ import annotations
|
|
|
3
3
|
import typing
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
|
|
6
|
+
from flag_engine import engine
|
|
7
|
+
from flag_engine.context.types import SegmentContext
|
|
8
|
+
|
|
6
9
|
from flagsmith.analytics import AnalyticsProcessor, PipelineAnalyticsProcessor
|
|
7
10
|
from flagsmith.exceptions import FlagsmithFeatureDoesNotExistError
|
|
8
|
-
from flagsmith.types import
|
|
11
|
+
from flagsmith.types import (
|
|
12
|
+
FeatureMetadata,
|
|
13
|
+
SDKEvaluationContext,
|
|
14
|
+
SDKEvaluationResult,
|
|
15
|
+
SDKFlagResult,
|
|
16
|
+
SegmentMetadata,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
SegmentOverridesIndex = typing.Dict[
|
|
20
|
+
str, typing.List[SegmentContext[SegmentMetadata, FeatureMetadata]]
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_segment_overrides_index(
|
|
25
|
+
context: SDKEvaluationContext,
|
|
26
|
+
) -> SegmentOverridesIndex:
|
|
27
|
+
"""Map feature_name -> segments that carry an override for that feature.
|
|
28
|
+
|
|
29
|
+
Computed once per environment-document refresh so the lazy eval path
|
|
30
|
+
can walk only the segments actually relevant to a given flag.
|
|
31
|
+
"""
|
|
32
|
+
index: SegmentOverridesIndex = {}
|
|
33
|
+
for segment_context in (context.get("segments") or {}).values():
|
|
34
|
+
for override in segment_context.get("overrides") or ():
|
|
35
|
+
index.setdefault(override["name"], []).append(segment_context)
|
|
36
|
+
return index
|
|
9
37
|
|
|
10
38
|
|
|
11
39
|
@dataclass
|
|
@@ -60,6 +88,14 @@ class Flags:
|
|
|
60
88
|
_pipeline_analytics_processor: typing.Optional[PipelineAnalyticsProcessor] = None
|
|
61
89
|
_identity_identifier: typing.Optional[str] = None
|
|
62
90
|
_traits: typing.Optional[typing.Dict[str, typing.Any]] = None
|
|
91
|
+
# Lazy-evaluation state. When `_context` is set, `flags` is a
|
|
92
|
+
# per-feature memo rather than a fully-materialised snapshot; unseen
|
|
93
|
+
# features are resolved on demand via the engine primitives and
|
|
94
|
+
# cached back into `flags`. Left as `None` by the eager code
|
|
95
|
+
# paths (`from_evaluation_result` / `from_api_flags`).
|
|
96
|
+
_context: typing.Optional[SDKEvaluationContext] = None
|
|
97
|
+
_overrides_index: typing.Optional[SegmentOverridesIndex] = None
|
|
98
|
+
_fully_materialised: bool = False
|
|
63
99
|
|
|
64
100
|
@classmethod
|
|
65
101
|
def from_evaluation_result(
|
|
@@ -86,6 +122,37 @@ class Flags:
|
|
|
86
122
|
_traits=traits,
|
|
87
123
|
)
|
|
88
124
|
|
|
125
|
+
@classmethod
|
|
126
|
+
def from_evaluation_context(
|
|
127
|
+
cls,
|
|
128
|
+
context: SDKEvaluationContext,
|
|
129
|
+
overrides_index: SegmentOverridesIndex,
|
|
130
|
+
analytics_processor: typing.Optional[AnalyticsProcessor],
|
|
131
|
+
default_flag_handler: typing.Optional[typing.Callable[[str], DefaultFlag]],
|
|
132
|
+
pipeline_analytics_processor: typing.Optional[
|
|
133
|
+
PipelineAnalyticsProcessor
|
|
134
|
+
] = None,
|
|
135
|
+
identity_identifier: typing.Optional[str] = None,
|
|
136
|
+
traits: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
137
|
+
) -> Flags:
|
|
138
|
+
"""Build a lazy `Flags` backed by an evaluation context.
|
|
139
|
+
|
|
140
|
+
No engine work is done here — flags are resolved on first access
|
|
141
|
+
via :meth:`_resolve_flag`. Reusing the same `overrides_index`
|
|
142
|
+
across calls amortises its construction cost (it's rebuilt only
|
|
143
|
+
when the environment doc refreshes, not per identity).
|
|
144
|
+
"""
|
|
145
|
+
return cls(
|
|
146
|
+
flags={},
|
|
147
|
+
default_flag_handler=default_flag_handler,
|
|
148
|
+
_analytics_processor=analytics_processor,
|
|
149
|
+
_pipeline_analytics_processor=pipeline_analytics_processor,
|
|
150
|
+
_identity_identifier=identity_identifier,
|
|
151
|
+
_traits=traits,
|
|
152
|
+
_context=context,
|
|
153
|
+
_overrides_index=overrides_index,
|
|
154
|
+
)
|
|
155
|
+
|
|
89
156
|
@classmethod
|
|
90
157
|
def from_api_flags(
|
|
91
158
|
cls,
|
|
@@ -116,8 +183,21 @@ class Flags:
|
|
|
116
183
|
"""
|
|
117
184
|
Get a list of all Flag objects.
|
|
118
185
|
|
|
186
|
+
In lazy mode, the caller has signalled they want every flag, so
|
|
187
|
+
we run the bulk evaluator once on the full context and copy the
|
|
188
|
+
results into the per-flag cache. Cheaper than asking the engine
|
|
189
|
+
for each feature one at a time.
|
|
190
|
+
|
|
119
191
|
:return: list of Flag objects.
|
|
120
192
|
"""
|
|
193
|
+
if self._context is not None and not self._fully_materialised:
|
|
194
|
+
result = engine.get_evaluation_result(self._context)
|
|
195
|
+
for feature_name, flag_result in result["flags"].items():
|
|
196
|
+
if feature_name not in self.flags:
|
|
197
|
+
self.flags[feature_name] = Flag.from_evaluation_result(
|
|
198
|
+
flag_result,
|
|
199
|
+
)
|
|
200
|
+
self._fully_materialised = True
|
|
121
201
|
return list(self.flags.values())
|
|
122
202
|
|
|
123
203
|
def is_feature_enabled(self, feature_name: str) -> bool:
|
|
@@ -151,11 +231,23 @@ class Flags:
|
|
|
151
231
|
try:
|
|
152
232
|
flag = self.flags[feature_name]
|
|
153
233
|
except KeyError:
|
|
154
|
-
if
|
|
234
|
+
# Lazy path: if this `Flags` wraps an evaluation context and
|
|
235
|
+
# the feature exists in it, resolve and memoise now. Otherwise
|
|
236
|
+
# fall through to the default_flag_handler / not-found error,
|
|
237
|
+
# preserving the eager-mode behaviour byte-for-byte.
|
|
238
|
+
if (
|
|
239
|
+
self._context is not None
|
|
240
|
+
and self._overrides_index is not None
|
|
241
|
+
and feature_name in (self._context.get("features") or {})
|
|
242
|
+
):
|
|
243
|
+
flag = self._resolve_flag(feature_name)
|
|
244
|
+
self.flags[feature_name] = flag
|
|
245
|
+
elif self.default_flag_handler:
|
|
155
246
|
return self.default_flag_handler(feature_name)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
247
|
+
else:
|
|
248
|
+
raise FlagsmithFeatureDoesNotExistError(
|
|
249
|
+
"Feature does not exist: %s" % feature_name
|
|
250
|
+
)
|
|
159
251
|
|
|
160
252
|
if self._analytics_processor and hasattr(flag, "feature_name"):
|
|
161
253
|
self._analytics_processor.track_feature(flag.feature_name)
|
|
@@ -171,6 +263,35 @@ class Flags:
|
|
|
171
263
|
|
|
172
264
|
return flag
|
|
173
265
|
|
|
266
|
+
def _resolve_flag(self, feature_name: str) -> Flag:
|
|
267
|
+
"""Evaluate a single feature against the lazy context.
|
|
268
|
+
|
|
269
|
+
Goes through the engine's public `get_evaluation_result` so
|
|
270
|
+
identity-key enrichment, multivariate hashing, percentage-split
|
|
271
|
+
rules and override-priority handling all stay where they
|
|
272
|
+
belong (in the engine). The performance win comes from passing
|
|
273
|
+
a *trimmed* context — just the queried feature plus the segments
|
|
274
|
+
that could override it, looked up in O(1) via the precomputed
|
|
275
|
+
reverse index — so the engine's full pipeline runs against an
|
|
276
|
+
input small enough to evaluate in ~1 µs.
|
|
277
|
+
"""
|
|
278
|
+
context = self._context
|
|
279
|
+
overrides_index = self._overrides_index
|
|
280
|
+
# `get_flag` / `all_flags` gate this call behind the same
|
|
281
|
+
# non-None checks; assert here so type checkers can narrow.
|
|
282
|
+
assert context is not None and overrides_index is not None
|
|
283
|
+
|
|
284
|
+
trimmed: SDKEvaluationContext = {
|
|
285
|
+
**context,
|
|
286
|
+
"features": {feature_name: context["features"][feature_name]},
|
|
287
|
+
"segments": {
|
|
288
|
+
segment_context["key"]: segment_context
|
|
289
|
+
for segment_context in overrides_index.get(feature_name, ())
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
result = engine.get_evaluation_result(trimmed)
|
|
293
|
+
return Flag.from_evaluation_result(result["flags"][feature_name])
|
|
294
|
+
|
|
174
295
|
|
|
175
296
|
@dataclass
|
|
176
297
|
class Segment:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "flagsmith"
|
|
3
|
-
version = "5.
|
|
3
|
+
version = "5.3.0"
|
|
4
4
|
description = "Flagsmith Python SDK"
|
|
5
5
|
authors = ["Flagsmith <support@flagsmith.com>"]
|
|
6
6
|
license = "BSD3"
|
|
@@ -10,7 +10,7 @@ documentation = "https://docs.flagsmith.com"
|
|
|
10
10
|
packages = [{ include = "flagsmith" }]
|
|
11
11
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
|
-
flagsmith-flag-engine = "^10.0.
|
|
13
|
+
flagsmith-flag-engine = "^10.0.4"
|
|
14
14
|
iso8601 = { version = "^2.1.0", python = "<3.11" }
|
|
15
15
|
python = ">=3.9,<4"
|
|
16
16
|
requests = "^2.32.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|