flagsmith 5.3.1__tar.gz → 5.5.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.3.1 → flagsmith-5.5.0}/PKG-INFO +2 -2
- flagsmith-5.5.0/flagsmith/__init__.py +6 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/analytics.py +54 -56
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/api/types.py +1 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/flagsmith.py +91 -30
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/mappers.py +12 -7
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/models.py +4 -37
- {flagsmith-5.3.1 → flagsmith-5.5.0}/pyproject.toml +2 -2
- flagsmith-5.3.1/flagsmith/__init__.py +0 -6
- {flagsmith-5.3.1 → flagsmith-5.5.0}/LICENSE +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/README.md +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/api/__init__.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/exceptions.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/offline_handlers.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/polling_manager.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/py.typed +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/streaming_manager.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/types.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/utils/__init__.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/utils/datetime.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/utils/identities.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.0}/flagsmith/version.py +0 -0
- {flagsmith-5.3.1 → flagsmith-5.5.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.5.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.2.0,<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)
|
|
@@ -2,7 +2,6 @@ import atexit
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import threading
|
|
5
|
-
import time
|
|
6
5
|
import typing
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from datetime import datetime
|
|
@@ -18,6 +17,10 @@ ANALYTICS_ENDPOINT: typing.Final[str] = "analytics/flags/"
|
|
|
18
17
|
# Used to control how often we send data(in seconds)
|
|
19
18
|
ANALYTICS_TIMER: typing.Final[int] = 10
|
|
20
19
|
|
|
20
|
+
FLAG_EXPOSURE_EVENT: typing.Final[str] = "$flag_exposure"
|
|
21
|
+
|
|
22
|
+
DEFAULT_EVENT_API_URL: typing.Final[str] = "https://events.api.flagsmith.com/"
|
|
23
|
+
|
|
21
24
|
session = FuturesSession(max_workers=4)
|
|
22
25
|
|
|
23
26
|
|
|
@@ -72,92 +75,90 @@ class AnalyticsProcessor:
|
|
|
72
75
|
|
|
73
76
|
|
|
74
77
|
@dataclass
|
|
75
|
-
class
|
|
76
|
-
|
|
78
|
+
class EventProcessorConfig:
|
|
79
|
+
events_api_url: str = DEFAULT_EVENT_API_URL
|
|
77
80
|
max_buffer_items: int = 1000
|
|
78
81
|
flush_interval_seconds: float = 10.0
|
|
79
82
|
|
|
80
83
|
|
|
81
|
-
class
|
|
84
|
+
class EventProcessor:
|
|
82
85
|
"""
|
|
83
|
-
Buffered
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Evaluation events are deduplicated within each flush window. Events are
|
|
87
|
-
flushed periodically via a background timer or when the buffer is full.
|
|
86
|
+
Buffered event processor that batches custom events and POSTs them to the
|
|
87
|
+
Flagsmith event endpoint. Flushes on a background timer or when the buffer
|
|
88
|
+
fills.
|
|
88
89
|
"""
|
|
89
90
|
|
|
90
91
|
def __init__(
|
|
91
92
|
self,
|
|
92
|
-
config:
|
|
93
|
+
config: EventProcessorConfig,
|
|
93
94
|
environment_key: str,
|
|
94
95
|
) -> None:
|
|
95
|
-
url = config.
|
|
96
|
+
url = config.events_api_url
|
|
96
97
|
if not url.endswith("/"):
|
|
97
98
|
url = f"{url}/"
|
|
98
|
-
self._batch_endpoint = f"{url}v1/
|
|
99
|
+
self._batch_endpoint = f"{url}v1/events"
|
|
99
100
|
self._environment_key = environment_key
|
|
100
101
|
self._max_buffer = config.max_buffer_items
|
|
101
102
|
self._flush_interval_seconds = config.flush_interval_seconds
|
|
102
103
|
|
|
103
104
|
self._buffer: typing.List[typing.Dict[str, typing.Any]] = []
|
|
104
|
-
self._dedup_keys: typing.Dict[str, str] = {}
|
|
105
105
|
self._lock = threading.Lock()
|
|
106
106
|
self._timer: typing.Optional[threading.Timer] = None
|
|
107
107
|
|
|
108
|
-
def
|
|
108
|
+
def track_event(
|
|
109
109
|
self,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
value: typing.
|
|
113
|
-
identity_identifier: typing.Optional[str] = None,
|
|
110
|
+
event: str,
|
|
111
|
+
identifier: typing.Optional[str] = None,
|
|
112
|
+
value: typing.Optional[typing.Union[str, int, float, bool]] = None,
|
|
114
113
|
traits: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
114
|
+
metadata: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
115
115
|
) -> None:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
"event_id": flag_key,
|
|
126
|
-
"event_type": "flag_evaluation",
|
|
127
|
-
"evaluated_at": int(time.time() * 1000),
|
|
128
|
-
"identity_identifier": identity_identifier,
|
|
129
|
-
"enabled": enabled,
|
|
130
|
-
"value": value,
|
|
131
|
-
"traits": dict(traits) if traits else None,
|
|
132
|
-
"metadata": {"sdk_version": __version__},
|
|
133
|
-
}
|
|
134
|
-
)
|
|
135
|
-
if len(self._buffer) >= self._max_buffer:
|
|
136
|
-
should_flush = True
|
|
137
|
-
|
|
138
|
-
if should_flush:
|
|
139
|
-
self.flush()
|
|
116
|
+
self._buffer_event(
|
|
117
|
+
event=event,
|
|
118
|
+
feature_name=None,
|
|
119
|
+
identifier=identifier,
|
|
120
|
+
value=value,
|
|
121
|
+
traits=traits,
|
|
122
|
+
metadata=metadata,
|
|
123
|
+
)
|
|
140
124
|
|
|
141
|
-
def
|
|
125
|
+
def track_exposure_event(
|
|
142
126
|
self,
|
|
143
|
-
|
|
144
|
-
|
|
127
|
+
feature_name: str,
|
|
128
|
+
identifier: typing.Optional[str] = None,
|
|
129
|
+
value: typing.Optional[typing.Union[str, int, float, bool]] = None,
|
|
145
130
|
traits: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
146
131
|
metadata: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
147
132
|
) -> None:
|
|
148
|
-
|
|
133
|
+
self._buffer_event(
|
|
134
|
+
event=FLAG_EXPOSURE_EVENT,
|
|
135
|
+
feature_name=feature_name,
|
|
136
|
+
identifier=identifier,
|
|
137
|
+
value=value,
|
|
138
|
+
traits=traits,
|
|
139
|
+
metadata=metadata,
|
|
140
|
+
)
|
|
149
141
|
|
|
142
|
+
def _buffer_event(
|
|
143
|
+
self,
|
|
144
|
+
event: str,
|
|
145
|
+
feature_name: typing.Optional[str],
|
|
146
|
+
identifier: typing.Optional[str],
|
|
147
|
+
value: typing.Optional[typing.Union[str, int, float, bool]],
|
|
148
|
+
traits: typing.Optional[typing.Dict[str, typing.Any]],
|
|
149
|
+
metadata: typing.Optional[typing.Dict[str, typing.Any]],
|
|
150
|
+
) -> None:
|
|
151
|
+
should_flush = False
|
|
150
152
|
with self._lock:
|
|
151
153
|
self._buffer.append(
|
|
152
154
|
{
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"enabled": None,
|
|
158
|
-
"value": None,
|
|
155
|
+
"event": event,
|
|
156
|
+
"feature_name": feature_name,
|
|
157
|
+
"identifier": identifier,
|
|
158
|
+
"value": str(value) if value is not None else None,
|
|
159
159
|
"traits": dict(traits) if traits else None,
|
|
160
160
|
"metadata": {**(metadata or {}), "sdk_version": __version__},
|
|
161
|
+
"timestamp": int(datetime.now().timestamp() * 1000),
|
|
161
162
|
}
|
|
162
163
|
)
|
|
163
164
|
if len(self._buffer) >= self._max_buffer:
|
|
@@ -172,11 +173,8 @@ class PipelineAnalyticsProcessor:
|
|
|
172
173
|
return
|
|
173
174
|
events = self._buffer
|
|
174
175
|
self._buffer = []
|
|
175
|
-
self._dedup_keys.clear()
|
|
176
176
|
|
|
177
|
-
payload = json.dumps(
|
|
178
|
-
{"events": events, "environment_key": self._environment_key}
|
|
179
|
-
)
|
|
177
|
+
payload = json.dumps({"events": events})
|
|
180
178
|
try:
|
|
181
179
|
future = session.post(
|
|
182
180
|
self._batch_endpoint,
|
|
@@ -9,9 +9,10 @@ from requests.adapters import HTTPAdapter
|
|
|
9
9
|
from urllib3 import Retry
|
|
10
10
|
|
|
11
11
|
from flagsmith.analytics import (
|
|
12
|
+
FLAG_EXPOSURE_EVENT,
|
|
12
13
|
AnalyticsProcessor,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
EventProcessor,
|
|
15
|
+
EventProcessorConfig,
|
|
15
16
|
)
|
|
16
17
|
from flagsmith.exceptions import FlagsmithAPIError, FlagsmithClientError
|
|
17
18
|
from flagsmith.mappers import (
|
|
@@ -23,6 +24,7 @@ from flagsmith.mappers import (
|
|
|
23
24
|
)
|
|
24
25
|
from flagsmith.models import (
|
|
25
26
|
DefaultFlag,
|
|
27
|
+
Flag,
|
|
26
28
|
Flags,
|
|
27
29
|
Segment,
|
|
28
30
|
SegmentOverridesIndex,
|
|
@@ -74,7 +76,8 @@ class Flagsmith:
|
|
|
74
76
|
environment_refresh_interval_seconds: typing.Union[int, float] = 60,
|
|
75
77
|
retries: typing.Optional[Retry] = None,
|
|
76
78
|
enable_analytics: bool = False,
|
|
77
|
-
|
|
79
|
+
enable_events: bool = False,
|
|
80
|
+
event_processor_config: typing.Optional[EventProcessorConfig] = None,
|
|
78
81
|
default_flag_handler: typing.Optional[
|
|
79
82
|
typing.Callable[[str], DefaultFlag]
|
|
80
83
|
] = None,
|
|
@@ -100,6 +103,12 @@ class Flagsmith:
|
|
|
100
103
|
Flagsmith API
|
|
101
104
|
:param enable_analytics: if enabled, sends additional requests to the Flagsmith
|
|
102
105
|
API to power flag analytics charts
|
|
106
|
+
:param enable_events: if enabled, starts an event processor that buffers
|
|
107
|
+
and sends events (custom and flag-exposure) to the Flagsmith events
|
|
108
|
+
API, powering experimentation analytics.
|
|
109
|
+
:param event_processor_config: optional configuration for the event
|
|
110
|
+
processor (URL override for self-hosted, buffer/flush tuning). Only
|
|
111
|
+
valid when ``enable_events=True``.
|
|
103
112
|
:param default_flag_handler: callable which will be used in the case where
|
|
104
113
|
flags cannot be retrieved from the API or a non-existent feature is
|
|
105
114
|
requested
|
|
@@ -120,9 +129,7 @@ class Flagsmith:
|
|
|
120
129
|
self.default_flag_handler = default_flag_handler
|
|
121
130
|
self.enable_realtime_updates = enable_realtime_updates
|
|
122
131
|
self._analytics_processor: typing.Optional[AnalyticsProcessor] = None
|
|
123
|
-
self.
|
|
124
|
-
PipelineAnalyticsProcessor
|
|
125
|
-
] = None
|
|
132
|
+
self._event_processor: typing.Optional[EventProcessor] = None
|
|
126
133
|
self.__evaluation_context: typing.Optional[SDKEvaluationContext] = None
|
|
127
134
|
self._segment_overrides_index: SegmentOverridesIndex = {}
|
|
128
135
|
self._environment_updated_at: typing.Optional[datetime] = None
|
|
@@ -140,6 +147,11 @@ class Flagsmith:
|
|
|
140
147
|
"Can only use realtime updates when running in local evaluation mode."
|
|
141
148
|
)
|
|
142
149
|
|
|
150
|
+
if event_processor_config is not None and not enable_events:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
"event_processor_config can only be set when enable_events=True."
|
|
153
|
+
)
|
|
154
|
+
|
|
143
155
|
if self.offline_handler:
|
|
144
156
|
self._evaluation_context = map_environment_document_to_context(
|
|
145
157
|
self.offline_handler.get_environment()
|
|
@@ -189,25 +201,35 @@ class Flagsmith:
|
|
|
189
201
|
self._initialise_analytics(
|
|
190
202
|
environment_key=environment_key,
|
|
191
203
|
enable_analytics=enable_analytics,
|
|
192
|
-
|
|
204
|
+
)
|
|
205
|
+
self._initialise_events(
|
|
206
|
+
environment_key=environment_key,
|
|
207
|
+
enable_events=enable_events,
|
|
208
|
+
event_processor_config=event_processor_config,
|
|
193
209
|
)
|
|
194
210
|
|
|
195
211
|
def _initialise_analytics(
|
|
196
212
|
self,
|
|
197
213
|
environment_key: str,
|
|
198
214
|
enable_analytics: bool,
|
|
199
|
-
pipeline_analytics_config: typing.Optional[PipelineAnalyticsConfig],
|
|
200
215
|
) -> None:
|
|
201
216
|
if enable_analytics:
|
|
202
217
|
self._analytics_processor = AnalyticsProcessor(
|
|
203
218
|
environment_key, self.api_url, timeout=self.request_timeout_seconds
|
|
204
219
|
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
|
|
221
|
+
def _initialise_events(
|
|
222
|
+
self,
|
|
223
|
+
environment_key: str,
|
|
224
|
+
enable_events: bool,
|
|
225
|
+
event_processor_config: typing.Optional[EventProcessorConfig],
|
|
226
|
+
) -> None:
|
|
227
|
+
if enable_events:
|
|
228
|
+
self._event_processor = EventProcessor(
|
|
229
|
+
config=event_processor_config or EventProcessorConfig(),
|
|
208
230
|
environment_key=environment_key,
|
|
209
231
|
)
|
|
210
|
-
self.
|
|
232
|
+
self._event_processor.start()
|
|
211
233
|
|
|
212
234
|
def _initialise_local_evaluation(self) -> None:
|
|
213
235
|
# To ensure that the environment is set before allowing subsequent
|
|
@@ -324,21 +346,68 @@ class Flagsmith:
|
|
|
324
346
|
|
|
325
347
|
return map_segment_results_to_identity_segments(evaluation_result["segments"])
|
|
326
348
|
|
|
349
|
+
def get_experiment_flag(
|
|
350
|
+
self,
|
|
351
|
+
feature_name: str,
|
|
352
|
+
identifier: str,
|
|
353
|
+
traits: typing.Optional[TraitMapping] = None,
|
|
354
|
+
) -> typing.Union[DefaultFlag, Flag]:
|
|
355
|
+
"""
|
|
356
|
+
Resolve a flag for an identity and record an exposure event.
|
|
357
|
+
|
|
358
|
+
Skips the exposure event when the resolved flag is a `DefaultFlag`
|
|
359
|
+
(i.e. the feature was not present and was served via the
|
|
360
|
+
`default_flag_handler`), to keep experimentation data clean.
|
|
361
|
+
"""
|
|
362
|
+
if not self._event_processor:
|
|
363
|
+
raise ValueError("Events must be enabled to use experiment flags.")
|
|
364
|
+
flag = self.get_identity_flags(identifier, traits).get_flag(feature_name)
|
|
365
|
+
if isinstance(flag, Flag):
|
|
366
|
+
self.track_exposure_event(
|
|
367
|
+
feature_name=feature_name,
|
|
368
|
+
identifier=identifier,
|
|
369
|
+
value=flag.variant if flag.variant is not None else flag.value,
|
|
370
|
+
traits=traits,
|
|
371
|
+
)
|
|
372
|
+
return flag
|
|
373
|
+
|
|
327
374
|
def track_event(
|
|
328
375
|
self,
|
|
329
|
-
|
|
330
|
-
|
|
376
|
+
event: str,
|
|
377
|
+
identifier: typing.Optional[str] = None,
|
|
378
|
+
value: typing.Optional[typing.Union[str, int, float, bool]] = None,
|
|
331
379
|
traits: typing.Optional[TraitMapping] = None,
|
|
332
380
|
metadata: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
333
381
|
) -> None:
|
|
334
|
-
if not self.
|
|
382
|
+
if not self._event_processor:
|
|
383
|
+
raise ValueError("Events must be enabled to track events.")
|
|
384
|
+
if event.startswith("$") and event != FLAG_EXPOSURE_EVENT:
|
|
335
385
|
raise ValueError(
|
|
336
|
-
"
|
|
337
|
-
"
|
|
386
|
+
f'Event name "{event}" uses the reserved "$" prefix '
|
|
387
|
+
"but is not a known system event."
|
|
338
388
|
)
|
|
339
|
-
self.
|
|
340
|
-
|
|
341
|
-
|
|
389
|
+
self._event_processor.track_event(
|
|
390
|
+
event=event,
|
|
391
|
+
identifier=identifier,
|
|
392
|
+
value=value,
|
|
393
|
+
traits=resolve_trait_values(traits),
|
|
394
|
+
metadata=metadata,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def track_exposure_event(
|
|
398
|
+
self,
|
|
399
|
+
feature_name: str,
|
|
400
|
+
identifier: typing.Optional[str] = None,
|
|
401
|
+
value: typing.Optional[typing.Union[str, int, float, bool]] = None,
|
|
402
|
+
traits: typing.Optional[TraitMapping] = None,
|
|
403
|
+
metadata: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
404
|
+
) -> None:
|
|
405
|
+
if not self._event_processor:
|
|
406
|
+
raise ValueError("Events must be enabled to track exposure events.")
|
|
407
|
+
self._event_processor.track_exposure_event(
|
|
408
|
+
feature_name=feature_name,
|
|
409
|
+
identifier=identifier,
|
|
410
|
+
value=value,
|
|
342
411
|
traits=resolve_trait_values(traits),
|
|
343
412
|
metadata=metadata,
|
|
344
413
|
)
|
|
@@ -418,7 +487,6 @@ class Flagsmith:
|
|
|
418
487
|
evaluation_result=evaluation_result,
|
|
419
488
|
analytics_processor=self._analytics_processor,
|
|
420
489
|
default_flag_handler=self.default_flag_handler,
|
|
421
|
-
pipeline_analytics_processor=self._pipeline_analytics_processor,
|
|
422
490
|
)
|
|
423
491
|
|
|
424
492
|
def _get_identity_flags_from_document(
|
|
@@ -442,9 +510,6 @@ class Flagsmith:
|
|
|
442
510
|
overrides_index=self._segment_overrides_index,
|
|
443
511
|
analytics_processor=self._analytics_processor,
|
|
444
512
|
default_flag_handler=self.default_flag_handler,
|
|
445
|
-
pipeline_analytics_processor=self._pipeline_analytics_processor,
|
|
446
|
-
identity_identifier=identifier,
|
|
447
|
-
traits=resolve_trait_values(traits),
|
|
448
513
|
)
|
|
449
514
|
|
|
450
515
|
def _get_environment_flags_from_api(self) -> Flags:
|
|
@@ -456,7 +521,6 @@ class Flagsmith:
|
|
|
456
521
|
api_flags=json_response,
|
|
457
522
|
analytics_processor=self._analytics_processor,
|
|
458
523
|
default_flag_handler=self.default_flag_handler,
|
|
459
|
-
pipeline_analytics_processor=self._pipeline_analytics_processor,
|
|
460
524
|
)
|
|
461
525
|
except FlagsmithAPIError:
|
|
462
526
|
if self.offline_handler:
|
|
@@ -489,9 +553,6 @@ class Flagsmith:
|
|
|
489
553
|
api_flags=json_response["flags"],
|
|
490
554
|
analytics_processor=self._analytics_processor,
|
|
491
555
|
default_flag_handler=self.default_flag_handler,
|
|
492
|
-
pipeline_analytics_processor=self._pipeline_analytics_processor,
|
|
493
|
-
identity_identifier=identifier,
|
|
494
|
-
traits=resolve_trait_values(traits),
|
|
495
556
|
)
|
|
496
557
|
except FlagsmithAPIError:
|
|
497
558
|
if self.offline_handler:
|
|
@@ -525,5 +586,5 @@ class Flagsmith:
|
|
|
525
586
|
if hasattr(self, "event_stream_thread"):
|
|
526
587
|
self.event_stream_thread.stop()
|
|
527
588
|
|
|
528
|
-
if self.
|
|
529
|
-
self.
|
|
589
|
+
if self._event_processor:
|
|
590
|
+
self._event_processor.stop()
|
|
@@ -7,6 +7,7 @@ from datetime import datetime, timezone
|
|
|
7
7
|
import sseclient
|
|
8
8
|
from flag_engine.context.types import (
|
|
9
9
|
FeatureContext,
|
|
10
|
+
FeatureValue,
|
|
10
11
|
SegmentContext,
|
|
11
12
|
SegmentRule,
|
|
12
13
|
StrValueSegmentCondition,
|
|
@@ -245,11 +246,13 @@ def _map_environment_document_feature_states_to_feature_contexts(
|
|
|
245
246
|
if multivariate_feature_state_values := feature_state.get(
|
|
246
247
|
"multivariate_feature_state_values"
|
|
247
248
|
):
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
variants: list[FeatureValue] = []
|
|
250
|
+
for multivariate_feature_state_value in multivariate_feature_state_values:
|
|
251
|
+
multivariate_feature_option = multivariate_feature_state_value[
|
|
252
|
+
"multivariate_feature_option"
|
|
253
|
+
]
|
|
254
|
+
variant: FeatureValue = {
|
|
255
|
+
"value": multivariate_feature_option["value"],
|
|
253
256
|
"weight": multivariate_feature_state_value["percentage_allocation"],
|
|
254
257
|
"priority": (
|
|
255
258
|
multivariate_feature_state_value.get("id")
|
|
@@ -258,8 +261,10 @@ def _map_environment_document_feature_states_to_feature_contexts(
|
|
|
258
261
|
).int
|
|
259
262
|
),
|
|
260
263
|
}
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
if (key := multivariate_feature_option.get("key")) is not None:
|
|
265
|
+
variant["key"] = key
|
|
266
|
+
variants.append(variant)
|
|
267
|
+
feature_context["variants"] = variants
|
|
263
268
|
|
|
264
269
|
if feature_segment := feature_state.get("feature_segment"):
|
|
265
270
|
feature_context["priority"] = feature_segment["priority"]
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from flag_engine import engine
|
|
7
7
|
from flag_engine.context.types import SegmentContext
|
|
8
8
|
|
|
9
|
-
from flagsmith.analytics import AnalyticsProcessor
|
|
9
|
+
from flagsmith.analytics import AnalyticsProcessor
|
|
10
10
|
from flagsmith.exceptions import FlagsmithFeatureDoesNotExistError
|
|
11
11
|
from flagsmith.types import (
|
|
12
12
|
FeatureMetadata,
|
|
@@ -51,6 +51,7 @@ class DefaultFlag(BaseFlag):
|
|
|
51
51
|
class Flag(BaseFlag):
|
|
52
52
|
feature_id: int
|
|
53
53
|
feature_name: str
|
|
54
|
+
variant: typing.Optional[str] = None
|
|
54
55
|
is_default: bool = field(default=False)
|
|
55
56
|
|
|
56
57
|
@classmethod
|
|
@@ -64,6 +65,7 @@ class Flag(BaseFlag):
|
|
|
64
65
|
value=flag_result["value"],
|
|
65
66
|
feature_name=flag_result["name"],
|
|
66
67
|
feature_id=metadata["id"],
|
|
68
|
+
variant=flag_result.get("variant"),
|
|
67
69
|
)
|
|
68
70
|
raise ValueError(
|
|
69
71
|
"FlagResult metadata is missing. Cannot create Flag instance. "
|
|
@@ -77,6 +79,7 @@ class Flag(BaseFlag):
|
|
|
77
79
|
value=flag_data["feature_state_value"],
|
|
78
80
|
feature_name=flag_data["feature"]["name"],
|
|
79
81
|
feature_id=flag_data["feature"]["id"],
|
|
82
|
+
variant=flag_data.get("variant"),
|
|
80
83
|
)
|
|
81
84
|
|
|
82
85
|
|
|
@@ -85,9 +88,6 @@ class Flags:
|
|
|
85
88
|
flags: typing.Dict[str, Flag] = field(default_factory=dict)
|
|
86
89
|
default_flag_handler: typing.Optional[typing.Callable[[str], DefaultFlag]] = None
|
|
87
90
|
_analytics_processor: typing.Optional[AnalyticsProcessor] = None
|
|
88
|
-
_pipeline_analytics_processor: typing.Optional[PipelineAnalyticsProcessor] = None
|
|
89
|
-
_identity_identifier: typing.Optional[str] = None
|
|
90
|
-
_traits: typing.Optional[typing.Dict[str, typing.Any]] = None
|
|
91
91
|
# Lazy-evaluation state. When `_context` is set, `flags` is a
|
|
92
92
|
# per-feature memo rather than a fully-materialised snapshot; unseen
|
|
93
93
|
# features are resolved on demand via the engine primitives and
|
|
@@ -103,11 +103,6 @@ class Flags:
|
|
|
103
103
|
evaluation_result: SDKEvaluationResult,
|
|
104
104
|
analytics_processor: typing.Optional[AnalyticsProcessor],
|
|
105
105
|
default_flag_handler: typing.Optional[typing.Callable[[str], DefaultFlag]],
|
|
106
|
-
pipeline_analytics_processor: typing.Optional[
|
|
107
|
-
PipelineAnalyticsProcessor
|
|
108
|
-
] = None,
|
|
109
|
-
identity_identifier: typing.Optional[str] = None,
|
|
110
|
-
traits: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
111
106
|
) -> Flags:
|
|
112
107
|
return cls(
|
|
113
108
|
flags={
|
|
@@ -117,9 +112,6 @@ class Flags:
|
|
|
117
112
|
},
|
|
118
113
|
default_flag_handler=default_flag_handler,
|
|
119
114
|
_analytics_processor=analytics_processor,
|
|
120
|
-
_pipeline_analytics_processor=pipeline_analytics_processor,
|
|
121
|
-
_identity_identifier=identity_identifier,
|
|
122
|
-
_traits=traits,
|
|
123
115
|
)
|
|
124
116
|
|
|
125
117
|
@classmethod
|
|
@@ -129,11 +121,6 @@ class Flags:
|
|
|
129
121
|
overrides_index: SegmentOverridesIndex,
|
|
130
122
|
analytics_processor: typing.Optional[AnalyticsProcessor],
|
|
131
123
|
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
124
|
) -> Flags:
|
|
138
125
|
"""Build a lazy `Flags` backed by an evaluation context.
|
|
139
126
|
|
|
@@ -146,9 +133,6 @@ class Flags:
|
|
|
146
133
|
flags={},
|
|
147
134
|
default_flag_handler=default_flag_handler,
|
|
148
135
|
_analytics_processor=analytics_processor,
|
|
149
|
-
_pipeline_analytics_processor=pipeline_analytics_processor,
|
|
150
|
-
_identity_identifier=identity_identifier,
|
|
151
|
-
_traits=traits,
|
|
152
136
|
_context=context,
|
|
153
137
|
_overrides_index=overrides_index,
|
|
154
138
|
)
|
|
@@ -159,11 +143,6 @@ class Flags:
|
|
|
159
143
|
api_flags: typing.Sequence[typing.Mapping[str, typing.Any]],
|
|
160
144
|
analytics_processor: typing.Optional[AnalyticsProcessor],
|
|
161
145
|
default_flag_handler: typing.Optional[typing.Callable[[str], DefaultFlag]],
|
|
162
|
-
pipeline_analytics_processor: typing.Optional[
|
|
163
|
-
PipelineAnalyticsProcessor
|
|
164
|
-
] = None,
|
|
165
|
-
identity_identifier: typing.Optional[str] = None,
|
|
166
|
-
traits: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
167
146
|
) -> Flags:
|
|
168
147
|
flags = {
|
|
169
148
|
flag_data["feature"]["name"]: Flag.from_api_flag(flag_data)
|
|
@@ -174,9 +153,6 @@ class Flags:
|
|
|
174
153
|
flags=flags,
|
|
175
154
|
default_flag_handler=default_flag_handler,
|
|
176
155
|
_analytics_processor=analytics_processor,
|
|
177
|
-
_pipeline_analytics_processor=pipeline_analytics_processor,
|
|
178
|
-
_identity_identifier=identity_identifier,
|
|
179
|
-
_traits=traits,
|
|
180
156
|
)
|
|
181
157
|
|
|
182
158
|
def all_flags(self) -> typing.List[Flag]:
|
|
@@ -252,15 +228,6 @@ class Flags:
|
|
|
252
228
|
if self._analytics_processor and hasattr(flag, "feature_name"):
|
|
253
229
|
self._analytics_processor.track_feature(flag.feature_name)
|
|
254
230
|
|
|
255
|
-
if self._pipeline_analytics_processor and hasattr(flag, "feature_name"):
|
|
256
|
-
self._pipeline_analytics_processor.record_evaluation_event(
|
|
257
|
-
flag_key=flag.feature_name,
|
|
258
|
-
enabled=flag.enabled,
|
|
259
|
-
value=flag.value,
|
|
260
|
-
identity_identifier=self._identity_identifier,
|
|
261
|
-
traits=self._traits,
|
|
262
|
-
)
|
|
263
|
-
|
|
264
231
|
return flag
|
|
265
232
|
|
|
266
233
|
def _resolve_flag(self, feature_name: str) -> Flag:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "flagsmith"
|
|
3
|
-
version = "5.
|
|
3
|
+
version = "5.5.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.2.0"
|
|
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
|