flagsmith 3.7.0__tar.gz → 3.9.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-3.7.0 → flagsmith-3.9.0}/PKG-INFO +6 -6
- {flagsmith-3.7.0 → flagsmith-3.9.0}/README.md +2 -2
- flagsmith-3.9.0/flagsmith/__init__.py +4 -0
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/flagsmith.py +51 -61
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/polling_manager.py +1 -8
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/streaming_manager.py +9 -15
- flagsmith-3.9.0/flagsmith/types.py +25 -0
- flagsmith-3.9.0/flagsmith/utils/identities.py +26 -0
- flagsmith-3.9.0/flagsmith/webhooks.py +41 -0
- {flagsmith-3.7.0 → flagsmith-3.9.0}/pyproject.toml +8 -9
- flagsmith-3.7.0/flagsmith/__init__.py +0 -3
- flagsmith-3.7.0/flagsmith/utils/identities.py +0 -21
- {flagsmith-3.7.0 → flagsmith-3.9.0}/LICENSE +0 -0
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/analytics.py +0 -0
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/exceptions.py +0 -0
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/models.py +0 -0
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/offline_handlers.py +0 -0
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/py.typed +0 -0
- {flagsmith-3.7.0 → flagsmith-3.9.0}/flagsmith/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: flagsmith
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.0
|
|
4
4
|
Summary: Flagsmith Python SDK
|
|
5
5
|
License: BSD3
|
|
6
6
|
Keywords: feature,flag,flagsmith,remote,config
|
|
@@ -12,10 +12,10 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
16
15
|
Requires-Dist: flagsmith-flag-engine (>=5.1.0,<6.0.0)
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: pydantic (>=2,<3)
|
|
17
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
18
|
+
Requires-Dist: requests-futures (>=1.0.1,<2.0.0)
|
|
19
19
|
Requires-Dist: sseclient-py (>=1.8.0,<2.0.0)
|
|
20
20
|
Project-URL: Documentation, https://docs.flagsmith.com
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
@@ -36,8 +36,8 @@ For full documentation visit
|
|
|
36
36
|
|
|
37
37
|
## Contributing
|
|
38
38
|
|
|
39
|
-
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code
|
|
40
|
-
|
|
39
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull
|
|
40
|
+
requests
|
|
41
41
|
|
|
42
42
|
## Getting Help
|
|
43
43
|
|
|
@@ -14,8 +14,8 @@ For full documentation visit
|
|
|
14
14
|
|
|
15
15
|
## Contributing
|
|
16
16
|
|
|
17
|
-
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code
|
|
18
|
-
|
|
17
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull
|
|
18
|
+
requests
|
|
19
19
|
|
|
20
20
|
## Getting Help
|
|
21
21
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import logging
|
|
3
2
|
import typing
|
|
4
|
-
from datetime import
|
|
3
|
+
from datetime import timezone
|
|
5
4
|
|
|
5
|
+
import pydantic
|
|
6
6
|
import requests
|
|
7
7
|
from flag_engine import engine
|
|
8
8
|
from flag_engine.environments.models import EnvironmentModel
|
|
@@ -19,23 +19,14 @@ from flagsmith.models import DefaultFlag, Flags, Segment
|
|
|
19
19
|
from flagsmith.offline_handlers import BaseOfflineHandler
|
|
20
20
|
from flagsmith.polling_manager import EnvironmentDataPollingManager
|
|
21
21
|
from flagsmith.streaming_manager import EventStreamManager, StreamEvent
|
|
22
|
-
from flagsmith.
|
|
22
|
+
from flagsmith.types import JsonType, TraitConfig, TraitMapping
|
|
23
|
+
from flagsmith.utils.identities import generate_identity_data
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
|
|
26
27
|
DEFAULT_API_URL = "https://edge.api.flagsmith.com/api/v1/"
|
|
27
28
|
DEFAULT_REALTIME_API_URL = "https://realtime.flagsmith.com/"
|
|
28
29
|
|
|
29
|
-
JsonType = typing.Union[
|
|
30
|
-
None,
|
|
31
|
-
int,
|
|
32
|
-
str,
|
|
33
|
-
bool,
|
|
34
|
-
typing.List["JsonType"],
|
|
35
|
-
typing.List[typing.Mapping[str, "JsonType"]],
|
|
36
|
-
typing.Dict[str, "JsonType"],
|
|
37
|
-
]
|
|
38
|
-
|
|
39
30
|
|
|
40
31
|
class Flagsmith:
|
|
41
32
|
"""A Flagsmith client.
|
|
@@ -168,8 +159,10 @@ class Flagsmith:
|
|
|
168
159
|
)
|
|
169
160
|
|
|
170
161
|
def _initialise_local_evaluation(self) -> None:
|
|
162
|
+
# To ensure that the environment is set before allowing subsequent
|
|
163
|
+
# method calls, update the environment manually.
|
|
164
|
+
self.update_environment()
|
|
171
165
|
if self.enable_realtime_updates:
|
|
172
|
-
self.update_environment()
|
|
173
166
|
if not self._environment:
|
|
174
167
|
raise ValueError("Unable to get environment from API key")
|
|
175
168
|
|
|
@@ -184,9 +177,6 @@ class Flagsmith:
|
|
|
184
177
|
self.event_stream_thread.start()
|
|
185
178
|
|
|
186
179
|
else:
|
|
187
|
-
# To ensure that the environment is set before allowing subsequent
|
|
188
|
-
# method calls, update the environment manually.
|
|
189
|
-
self.update_environment()
|
|
190
180
|
self.environment_data_polling_manager_thread = (
|
|
191
181
|
EnvironmentDataPollingManager(
|
|
192
182
|
main=self,
|
|
@@ -198,21 +188,6 @@ class Flagsmith:
|
|
|
198
188
|
self.environment_data_polling_manager_thread.start()
|
|
199
189
|
|
|
200
190
|
def handle_stream_event(self, event: StreamEvent) -> None:
|
|
201
|
-
try:
|
|
202
|
-
event_data = json.loads(event.data)
|
|
203
|
-
except json.JSONDecodeError as e:
|
|
204
|
-
raise FlagsmithAPIError("Unable to get valid json from event data.") from e
|
|
205
|
-
|
|
206
|
-
try:
|
|
207
|
-
stream_updated_at = datetime.fromtimestamp(event_data.get("updated_at"))
|
|
208
|
-
except TypeError as e:
|
|
209
|
-
raise FlagsmithAPIError(
|
|
210
|
-
"Unable to get valid timestamp from event data."
|
|
211
|
-
) from e
|
|
212
|
-
|
|
213
|
-
if stream_updated_at.tzinfo is None:
|
|
214
|
-
stream_updated_at = stream_updated_at.astimezone(timezone.utc)
|
|
215
|
-
|
|
216
191
|
if not self._environment:
|
|
217
192
|
raise ValueError(
|
|
218
193
|
"Unable to access environment. Environment should not be null"
|
|
@@ -221,7 +196,7 @@ class Flagsmith:
|
|
|
221
196
|
if environment_updated_at.tzinfo is None:
|
|
222
197
|
environment_updated_at = environment_updated_at.astimezone(timezone.utc)
|
|
223
198
|
|
|
224
|
-
if
|
|
199
|
+
if event.updated_at > environment_updated_at:
|
|
225
200
|
self.update_environment()
|
|
226
201
|
|
|
227
202
|
def get_environment_flags(self) -> Flags:
|
|
@@ -237,7 +212,9 @@ class Flagsmith:
|
|
|
237
212
|
def get_identity_flags(
|
|
238
213
|
self,
|
|
239
214
|
identifier: str,
|
|
240
|
-
traits: typing.Optional[
|
|
215
|
+
traits: typing.Optional[TraitMapping] = None,
|
|
216
|
+
*,
|
|
217
|
+
transient: bool = False,
|
|
241
218
|
) -> Flags:
|
|
242
219
|
"""
|
|
243
220
|
Get all the flags for the current environment for a given identity. Will also
|
|
@@ -247,13 +224,20 @@ class Flagsmith:
|
|
|
247
224
|
:param identifier: a unique identifier for the identity in the current
|
|
248
225
|
environment, e.g. email address, username, uuid
|
|
249
226
|
:param traits: a dictionary of traits to add / update on the identity in
|
|
250
|
-
Flagsmith, e.g. {"num_orders": 10}
|
|
227
|
+
Flagsmith, e.g. `{"num_orders": 10}`. Envelope traits you don't want persisted
|
|
228
|
+
in a dictionary with `"transient"` and `"value"` keys, e.g.
|
|
229
|
+
`{"num_orders": 10, "color": {"value": "pink", "transient": True}}`.
|
|
230
|
+
:param transient: if `True`, the identity won't get persisted
|
|
251
231
|
:return: Flags object holding all the flags for the given identity.
|
|
252
232
|
"""
|
|
253
233
|
traits = traits or {}
|
|
254
234
|
if (self.offline_mode or self.enable_local_evaluation) and self._environment:
|
|
255
235
|
return self._get_identity_flags_from_document(identifier, traits)
|
|
256
|
-
return self._get_identity_flags_from_api(
|
|
236
|
+
return self._get_identity_flags_from_api(
|
|
237
|
+
identifier,
|
|
238
|
+
traits,
|
|
239
|
+
transient=transient,
|
|
240
|
+
)
|
|
257
241
|
|
|
258
242
|
def get_identity_segments(
|
|
259
243
|
self,
|
|
@@ -281,16 +265,15 @@ class Flagsmith:
|
|
|
281
265
|
return [Segment(id=sm.id, name=sm.name) for sm in segment_models]
|
|
282
266
|
|
|
283
267
|
def update_environment(self) -> None:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
268
|
+
try:
|
|
269
|
+
self._environment = self._get_environment_from_api()
|
|
270
|
+
except (FlagsmithAPIError, pydantic.ValidationError):
|
|
271
|
+
logger.exception("Error updating environment")
|
|
272
|
+
else:
|
|
273
|
+
if overrides := self._environment.identity_overrides:
|
|
274
|
+
self._identity_overrides_by_identifier = {
|
|
275
|
+
identity.identifier: identity for identity in overrides
|
|
276
|
+
}
|
|
294
277
|
|
|
295
278
|
def _get_environment_from_api(self) -> EnvironmentModel:
|
|
296
279
|
environment_data = self._get_json_response(self.environment_url, method="GET")
|
|
@@ -306,7 +289,7 @@ class Flagsmith:
|
|
|
306
289
|
)
|
|
307
290
|
|
|
308
291
|
def _get_identity_flags_from_document(
|
|
309
|
-
self, identifier: str, traits:
|
|
292
|
+
self, identifier: str, traits: TraitMapping
|
|
310
293
|
) -> Flags:
|
|
311
294
|
identity_model = self._get_identity_model(identifier, **traits)
|
|
312
295
|
if self._environment is None:
|
|
@@ -339,13 +322,23 @@ class Flagsmith:
|
|
|
339
322
|
raise
|
|
340
323
|
|
|
341
324
|
def _get_identity_flags_from_api(
|
|
342
|
-
self,
|
|
325
|
+
self,
|
|
326
|
+
identifier: str,
|
|
327
|
+
traits: TraitMapping,
|
|
328
|
+
*,
|
|
329
|
+
transient: bool = False,
|
|
343
330
|
) -> Flags:
|
|
331
|
+
request_body = generate_identity_data(
|
|
332
|
+
identifier,
|
|
333
|
+
traits,
|
|
334
|
+
transient=transient,
|
|
335
|
+
)
|
|
344
336
|
try:
|
|
345
|
-
data = generate_identities_data(identifier, traits)
|
|
346
337
|
json_response: typing.Dict[str, typing.List[typing.Dict[str, JsonType]]] = (
|
|
347
338
|
self._get_json_response(
|
|
348
|
-
url=self.identities_url,
|
|
339
|
+
url=self.identities_url,
|
|
340
|
+
method="POST",
|
|
341
|
+
body=request_body,
|
|
349
342
|
)
|
|
350
343
|
)
|
|
351
344
|
return Flags.from_api_flags(
|
|
@@ -364,22 +357,16 @@ class Flagsmith:
|
|
|
364
357
|
self,
|
|
365
358
|
url: str,
|
|
366
359
|
method: str,
|
|
367
|
-
body: typing.Optional[
|
|
368
|
-
typing.Union[Identity, typing.Dict[str, JsonType]]
|
|
369
|
-
] = None,
|
|
360
|
+
body: typing.Optional[JsonType] = None,
|
|
370
361
|
) -> typing.Any:
|
|
371
362
|
try:
|
|
372
363
|
request_method = getattr(self.session, method.lower())
|
|
373
364
|
response = request_method(
|
|
374
365
|
url, json=body, timeout=self.request_timeout_seconds
|
|
375
366
|
)
|
|
376
|
-
|
|
377
|
-
raise FlagsmithAPIError(
|
|
378
|
-
"Invalid request made to Flagsmith API. Response status code: %d",
|
|
379
|
-
response.status_code,
|
|
380
|
-
)
|
|
367
|
+
response.raise_for_status()
|
|
381
368
|
return response.json()
|
|
382
|
-
except
|
|
369
|
+
except requests.RequestException as e:
|
|
383
370
|
raise FlagsmithAPIError(
|
|
384
371
|
"Unable to get valid response from Flagsmith API."
|
|
385
372
|
) from e
|
|
@@ -387,7 +374,7 @@ class Flagsmith:
|
|
|
387
374
|
def _get_identity_model(
|
|
388
375
|
self,
|
|
389
376
|
identifier: str,
|
|
390
|
-
**traits: TraitValue,
|
|
377
|
+
**traits: typing.Union[TraitValue, TraitConfig],
|
|
391
378
|
) -> IdentityModel:
|
|
392
379
|
if not self._environment:
|
|
393
380
|
raise FlagsmithClientError(
|
|
@@ -395,7 +382,10 @@ class Flagsmith:
|
|
|
395
382
|
)
|
|
396
383
|
|
|
397
384
|
trait_models = [
|
|
398
|
-
TraitModel(
|
|
385
|
+
TraitModel(
|
|
386
|
+
trait_key=key,
|
|
387
|
+
trait_value=value["value"] if isinstance(value, dict) else value,
|
|
388
|
+
)
|
|
399
389
|
for key, value in traits.items()
|
|
400
390
|
]
|
|
401
391
|
|
|
@@ -5,10 +5,6 @@ import threading
|
|
|
5
5
|
import time
|
|
6
6
|
import typing
|
|
7
7
|
|
|
8
|
-
import requests
|
|
9
|
-
|
|
10
|
-
from flagsmith.exceptions import FlagsmithAPIError
|
|
11
|
-
|
|
12
8
|
if typing.TYPE_CHECKING:
|
|
13
9
|
from flagsmith import Flagsmith
|
|
14
10
|
|
|
@@ -30,10 +26,7 @@ class EnvironmentDataPollingManager(threading.Thread):
|
|
|
30
26
|
|
|
31
27
|
def run(self) -> None:
|
|
32
28
|
while not self._stop_event.is_set():
|
|
33
|
-
|
|
34
|
-
self.main.update_environment()
|
|
35
|
-
except (FlagsmithAPIError, requests.RequestException):
|
|
36
|
-
logger.exception("Failed to update environment")
|
|
29
|
+
self.main.update_environment()
|
|
37
30
|
time.sleep(self.refresh_interval_seconds)
|
|
38
31
|
|
|
39
32
|
def stop(self) -> None:
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import threading
|
|
3
3
|
import typing
|
|
4
|
-
from typing import Callable,
|
|
4
|
+
from typing import Callable, Optional
|
|
5
5
|
|
|
6
|
+
import pydantic
|
|
6
7
|
import requests
|
|
7
8
|
import sseclient
|
|
8
9
|
|
|
9
|
-
from flagsmith.exceptions import FlagsmithAPIError
|
|
10
|
-
|
|
11
10
|
logger = logging.getLogger(__name__)
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
class StreamEvent(
|
|
15
|
-
|
|
13
|
+
class StreamEvent(pydantic.BaseModel):
|
|
14
|
+
updated_at: pydantic.AwareDatetime
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class EventStreamManager(threading.Thread):
|
|
@@ -22,7 +21,7 @@ class EventStreamManager(threading.Thread):
|
|
|
22
21
|
stream_url: str,
|
|
23
22
|
on_event: Callable[[StreamEvent], None],
|
|
24
23
|
request_timeout_seconds: Optional[int] = None,
|
|
25
|
-
**kwargs: typing.Any
|
|
24
|
+
**kwargs: typing.Any,
|
|
26
25
|
) -> None:
|
|
27
26
|
super().__init__(*args, **kwargs)
|
|
28
27
|
self._stop_event = threading.Event()
|
|
@@ -39,17 +38,12 @@ class EventStreamManager(threading.Thread):
|
|
|
39
38
|
headers={"Accept": "application/json, text/event-stream"},
|
|
40
39
|
timeout=self.request_timeout_seconds,
|
|
41
40
|
) as response:
|
|
42
|
-
sse_client = sseclient.SSEClient(
|
|
43
|
-
cast(Generator[bytes, None, None], response)
|
|
44
|
-
)
|
|
41
|
+
sse_client = sseclient.SSEClient(chunk for chunk in response)
|
|
45
42
|
for event in sse_client.events():
|
|
46
|
-
self.on_event(event)
|
|
47
|
-
|
|
48
|
-
except requests.exceptions.ReadTimeout:
|
|
49
|
-
pass
|
|
43
|
+
self.on_event(StreamEvent.model_validate_json(event.data))
|
|
50
44
|
|
|
51
|
-
except (
|
|
52
|
-
logger.exception("Error
|
|
45
|
+
except (requests.RequestException, pydantic.ValidationError):
|
|
46
|
+
logger.exception("Error opening or reading from the event stream")
|
|
53
47
|
|
|
54
48
|
def stop(self) -> None:
|
|
55
49
|
self._stop_event.set()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from flag_engine.identities.traits.types import TraitValue
|
|
4
|
+
from typing_extensions import TypeAlias
|
|
5
|
+
|
|
6
|
+
_JsonScalarType: TypeAlias = typing.Union[
|
|
7
|
+
int,
|
|
8
|
+
str,
|
|
9
|
+
float,
|
|
10
|
+
bool,
|
|
11
|
+
None,
|
|
12
|
+
]
|
|
13
|
+
JsonType: TypeAlias = typing.Union[
|
|
14
|
+
_JsonScalarType,
|
|
15
|
+
typing.Dict[str, "JsonType"],
|
|
16
|
+
typing.List["JsonType"],
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TraitConfig(typing.TypedDict):
|
|
21
|
+
value: TraitValue
|
|
22
|
+
transient: bool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TraitMapping: TypeAlias = typing.Mapping[str, typing.Union[TraitValue, TraitConfig]]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from flagsmith.types import JsonType, TraitMapping
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_identity_data(
|
|
7
|
+
identifier: str,
|
|
8
|
+
traits: TraitMapping,
|
|
9
|
+
*,
|
|
10
|
+
transient: bool,
|
|
11
|
+
) -> JsonType:
|
|
12
|
+
identity_data: typing.Dict[str, JsonType] = {"identifier": identifier}
|
|
13
|
+
traits_data: typing.List[JsonType] = []
|
|
14
|
+
for trait_key, trait_value in traits.items():
|
|
15
|
+
trait_data: typing.Dict[str, JsonType] = {"trait_key": trait_key}
|
|
16
|
+
if isinstance(trait_value, dict):
|
|
17
|
+
trait_data["trait_value"] = trait_value["value"]
|
|
18
|
+
if trait_value.get("transient"):
|
|
19
|
+
trait_data["transient"] = True
|
|
20
|
+
else:
|
|
21
|
+
trait_data["trait_value"] = trait_value
|
|
22
|
+
traits_data.append(trait_data)
|
|
23
|
+
identity_data["traits"] = traits_data
|
|
24
|
+
if transient:
|
|
25
|
+
identity_data["transient"] = True
|
|
26
|
+
return identity_data
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_signature(
|
|
7
|
+
request_body: Union[str, bytes],
|
|
8
|
+
shared_secret: str,
|
|
9
|
+
) -> str:
|
|
10
|
+
"""Generates a signature for a webhook request body using HMAC-SHA256.
|
|
11
|
+
|
|
12
|
+
:param request_body: The raw request body, as string or bytes.
|
|
13
|
+
:param shared_secret: The shared secret configured for this specific webhook.
|
|
14
|
+
:return: The hex-encoded signature.
|
|
15
|
+
"""
|
|
16
|
+
if isinstance(request_body, str):
|
|
17
|
+
request_body = request_body.encode()
|
|
18
|
+
|
|
19
|
+
shared_secret_bytes = shared_secret.encode()
|
|
20
|
+
|
|
21
|
+
return hmac.new(
|
|
22
|
+
key=shared_secret_bytes,
|
|
23
|
+
msg=request_body,
|
|
24
|
+
digestmod=hashlib.sha256,
|
|
25
|
+
).hexdigest()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def verify_signature(
|
|
29
|
+
request_body: Union[str, bytes],
|
|
30
|
+
received_signature: str,
|
|
31
|
+
shared_secret: str,
|
|
32
|
+
) -> bool:
|
|
33
|
+
"""Verifies a webhook's signature to determine if the request was sent by Flagsmith.
|
|
34
|
+
|
|
35
|
+
:param request_body: The raw request body, as string or bytes.
|
|
36
|
+
:param received_signature: The signature as received in the X-Flagsmith-Signature request header.
|
|
37
|
+
:param shared_secret: The shared secret configured for this specific webhook.
|
|
38
|
+
:return: True if the signature is valid, False otherwise.
|
|
39
|
+
"""
|
|
40
|
+
expected_signature = generate_signature(request_body, shared_secret)
|
|
41
|
+
return hmac.compare_digest(expected_signature, received_signature)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "flagsmith"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.9.0"
|
|
4
4
|
description = "Flagsmith Python SDK"
|
|
5
5
|
authors = ["Flagsmith <support@flagsmith.com>"]
|
|
6
6
|
license = "BSD3"
|
|
@@ -11,30 +11,29 @@ packages = [{ include = "flagsmith" }]
|
|
|
11
11
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
13
|
python = ">=3.8.1,<4"
|
|
14
|
-
requests = "^2.
|
|
15
|
-
requests-futures = "^1.0.
|
|
14
|
+
requests = "^2.32.3"
|
|
15
|
+
requests-futures = "^1.0.1"
|
|
16
16
|
flagsmith-flag-engine = "^5.1.0"
|
|
17
17
|
sseclient-py = "^1.8.0"
|
|
18
|
+
pydantic = "^2"
|
|
18
19
|
|
|
19
20
|
[tool.poetry.group.dev]
|
|
20
21
|
optional = true
|
|
21
22
|
|
|
22
23
|
[tool.poetry.group.dev.dependencies]
|
|
23
24
|
pytest = "^7.4.0"
|
|
25
|
+
pytest-cov = "^4.1.0"
|
|
24
26
|
pytest-mock = "^3.6.1"
|
|
25
|
-
black = ">=23.3,<25.0"
|
|
26
27
|
pre-commit = "^2.17.0"
|
|
27
28
|
responses = "^0.24.1"
|
|
28
|
-
|
|
29
|
-
isort = "^5.12.0"
|
|
30
|
-
mypy = "^1.7.1"
|
|
31
|
-
types-requests = "^2.31.0.10"
|
|
32
|
-
pytest-cov = "^4.1.0"
|
|
29
|
+
types-requests = "^2.32"
|
|
33
30
|
|
|
34
31
|
[tool.mypy]
|
|
35
32
|
plugins = ["pydantic.mypy"]
|
|
36
33
|
exclude = ["example/*"]
|
|
37
34
|
|
|
35
|
+
[tool.black]
|
|
36
|
+
target-version = ["py38"]
|
|
38
37
|
|
|
39
38
|
[build-system]
|
|
40
39
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
from flag_engine.identities.traits.types import TraitValue
|
|
4
|
-
|
|
5
|
-
Identity = typing.TypedDict(
|
|
6
|
-
"Identity",
|
|
7
|
-
{"identifier": str, "traits": typing.List[typing.Mapping[str, TraitValue]]},
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def generate_identities_data(
|
|
12
|
-
identifier: str, traits: typing.Optional[typing.Mapping[str, TraitValue]] = None
|
|
13
|
-
) -> Identity:
|
|
14
|
-
return {
|
|
15
|
-
"identifier": identifier,
|
|
16
|
-
"traits": (
|
|
17
|
-
[{"trait_key": k, "trait_value": v} for k, v in traits.items()]
|
|
18
|
-
if traits
|
|
19
|
-
else []
|
|
20
|
-
),
|
|
21
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|