Skip to content

Commit 0a11db5

Browse files
authored
feat: Support transient identities and traits (#93)
* feat: Support transient identities and traits - Support transient identities and traits - Bump requests - Bump mypy - Remove linting from CI in favour of pre-commit.ci
1 parent 297c382 commit 0a11db5

File tree

10 files changed

+166
-264
lines changed

10 files changed

+166
-264
lines changed

.github/workflows/pytest.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Linting and Tests
1+
name: Run Tests
22

33
on:
44
pull_request:
@@ -9,7 +9,6 @@ on:
99
jobs:
1010
test:
1111
runs-on: ubuntu-latest
12-
name: Linting and Tests
1312

1413
strategy:
1514
max-parallel: 4
@@ -33,14 +32,5 @@ jobs:
3332
pip install poetry
3433
poetry install --with dev
3534
36-
- name: Check Formatting
37-
run: |
38-
poetry run black --check .
39-
poetry run flake8 .
40-
poetry run isort --check .
41-
42-
- name: Check Typing
43-
run: poetry run mypy --strict .
44-
4535
- name: Run Tests
4636
run: poetry run pytest

.pre-commit-config.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/pre-commit/mirrors-mypy
3-
rev: v1.5.1
3+
rev: v1.10.1
44
hooks:
55
- id: mypy
66
args: [--strict]
@@ -14,7 +14,6 @@ repos:
1414
rev: 24.3.0
1515
hooks:
1616
- id: black
17-
language_version: python3
1817
- repo: https://github.com/pycqa/flake8
1918
rev: 6.1.0
2019
hooks:

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ For full documentation visit
1414

1515
## Contributing
1616

17-
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code
18-
of conduct, and the process for submitting pull requests
17+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull
18+
requests
1919

2020
## Getting Help
2121

flagsmith/flagsmith.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,14 @@
1919
from flagsmith.offline_handlers import BaseOfflineHandler
2020
from flagsmith.polling_manager import EnvironmentDataPollingManager
2121
from flagsmith.streaming_manager import EventStreamManager, StreamEvent
22-
from flagsmith.utils.identities import Identity, generate_identities_data
22+
from flagsmith.types import JsonType, TraitConfig, TraitMapping
23+
from flagsmith.utils.identities import generate_identity_data
2324

2425
logger = logging.getLogger(__name__)
2526

2627
DEFAULT_API_URL = "https://edge.api.flagsmith.com/api/v1/"
2728
DEFAULT_REALTIME_API_URL = "https://realtime.flagsmith.com/"
2829

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-
3930

4031
class Flagsmith:
4132
"""A Flagsmith client.
@@ -237,7 +228,9 @@ def get_environment_flags(self) -> Flags:
237228
def get_identity_flags(
238229
self,
239230
identifier: str,
240-
traits: typing.Optional[typing.Mapping[str, TraitValue]] = None,
231+
traits: typing.Optional[TraitMapping] = None,
232+
*,
233+
transient: bool = False,
241234
) -> Flags:
242235
"""
243236
Get all the flags for the current environment for a given identity. Will also
@@ -247,13 +240,20 @@ def get_identity_flags(
247240
:param identifier: a unique identifier for the identity in the current
248241
environment, e.g. email address, username, uuid
249242
:param traits: a dictionary of traits to add / update on the identity in
250-
Flagsmith, e.g. {"num_orders": 10}
243+
Flagsmith, e.g. `{"num_orders": 10}`. Envelope traits you don't want persisted
244+
in a dictionary with `"transient"` and `"value"` keys, e.g.
245+
`{"num_orders": 10, "color": {"value": "pink", "transient": True}}`.
246+
:param transient: if `True`, the identity won't get persisted
251247
:return: Flags object holding all the flags for the given identity.
252248
"""
253249
traits = traits or {}
254250
if (self.offline_mode or self.enable_local_evaluation) and self._environment:
255251
return self._get_identity_flags_from_document(identifier, traits)
256-
return self._get_identity_flags_from_api(identifier, traits)
252+
return self._get_identity_flags_from_api(
253+
identifier,
254+
traits,
255+
transient=transient,
256+
)
257257

258258
def get_identity_segments(
259259
self,
@@ -306,7 +306,7 @@ def _get_environment_flags_from_document(self) -> Flags:
306306
)
307307

308308
def _get_identity_flags_from_document(
309-
self, identifier: str, traits: typing.Mapping[str, TraitValue]
309+
self, identifier: str, traits: TraitMapping
310310
) -> Flags:
311311
identity_model = self._get_identity_model(identifier, **traits)
312312
if self._environment is None:
@@ -339,13 +339,23 @@ def _get_environment_flags_from_api(self) -> Flags:
339339
raise
340340

341341
def _get_identity_flags_from_api(
342-
self, identifier: str, traits: typing.Mapping[str, typing.Any]
342+
self,
343+
identifier: str,
344+
traits: TraitMapping,
345+
*,
346+
transient: bool = False,
343347
) -> Flags:
348+
request_body = generate_identity_data(
349+
identifier,
350+
traits,
351+
transient=transient,
352+
)
344353
try:
345-
data = generate_identities_data(identifier, traits)
346354
json_response: typing.Dict[str, typing.List[typing.Dict[str, JsonType]]] = (
347355
self._get_json_response(
348-
url=self.identities_url, method="POST", body=data
356+
url=self.identities_url,
357+
method="POST",
358+
body=request_body,
349359
)
350360
)
351361
return Flags.from_api_flags(
@@ -364,9 +374,7 @@ def _get_json_response(
364374
self,
365375
url: str,
366376
method: str,
367-
body: typing.Optional[
368-
typing.Union[Identity, typing.Dict[str, JsonType]]
369-
] = None,
377+
body: typing.Optional[JsonType] = None,
370378
) -> typing.Any:
371379
try:
372380
request_method = getattr(self.session, method.lower())
@@ -387,15 +395,18 @@ def _get_json_response(
387395
def _get_identity_model(
388396
self,
389397
identifier: str,
390-
**traits: TraitValue,
398+
**traits: typing.Union[TraitValue, TraitConfig],
391399
) -> IdentityModel:
392400
if not self._environment:
393401
raise FlagsmithClientError(
394402
"Unable to build identity model when no local environment present."
395403
)
396404

397405
trait_models = [
398-
TraitModel(trait_key=key, trait_value=value)
406+
TraitModel(
407+
trait_key=key,
408+
trait_value=value["value"] if isinstance(value, dict) else value,
409+
)
399410
for key, value in traits.items()
400411
]
401412

flagsmith/streaming_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def __init__(
2222
stream_url: str,
2323
on_event: Callable[[StreamEvent], None],
2424
request_timeout_seconds: Optional[int] = None,
25-
**kwargs: typing.Any
25+
**kwargs: typing.Any,
2626
) -> None:
2727
super().__init__(*args, **kwargs)
2828
self._stop_event = threading.Event()

flagsmith/types.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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]]

flagsmith/utils/identities.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import typing
22

3-
from flag_engine.identities.traits.types import TraitValue
3+
from flagsmith.types import JsonType, TraitMapping
44

5-
Identity = typing.TypedDict(
6-
"Identity",
7-
{"identifier": str, "traits": typing.List[typing.Mapping[str, TraitValue]]},
8-
)
95

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-
}
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 commit comments

Comments
 (0)