Skip to content

Commit f8fd387

Browse files
authored
feat: Add indentity overrides to local evaluation mode (#72)
- Bump flag-engine - Add environment overrides parsing - Add pytest-cov - Fix black pre-commit hook - Fix Flag dataclasses
1 parent cc47ae3 commit f8fd387

File tree

8 files changed

+365
-333
lines changed

8 files changed

+365
-333
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ flagsmith.egg-info/
1414

1515
.envrc
1616
.tool-versions
17+
18+
.coverage

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ repos:
88
hooks:
99
- id: isort
1010
- repo: https://github.com/psf/black
11-
rev: stable
11+
rev: 23.3.0
1212
hooks:
1313
- id: black
1414
language_version: python3

flagsmith/flagsmith.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import requests
88
from flag_engine import engine
99
from flag_engine.environments.models import EnvironmentModel
10-
from flag_engine.identities.models import IdentityModel, TraitModel
10+
from flag_engine.identities.models import IdentityModel
11+
from flag_engine.identities.traits.models import TraitModel
12+
from flag_engine.identities.traits.types import TraitValue
1113
from flag_engine.segments.evaluator import get_identity_segments
1214
from requests.adapters import HTTPAdapter
1315
from urllib3 import Retry
@@ -94,6 +96,7 @@ def __init__(
9496
self.enable_realtime_updates = enable_realtime_updates
9597
self._analytics_processor = None
9698
self._environment = None
99+
self._identity_overrides_by_identifier: typing.Dict[str, IdentityModel] = {}
97100

98101
# argument validation
99102
if offline_mode and not offline_handler:
@@ -248,12 +251,21 @@ def get_identity_segments(
248251
)
249252

250253
traits = traits or {}
251-
identity_model = self._build_identity_model(identifier, **traits)
254+
identity_model = self._get_identity_model(identifier, **traits)
252255
segment_models = get_identity_segments(self._environment, identity_model)
253256
return [Segment(id=sm.id, name=sm.name) for sm in segment_models]
254257

255258
def update_environment(self):
256259
self._environment = self._get_environment_from_api()
260+
self._update_overrides()
261+
262+
def _update_overrides(self) -> None:
263+
if not self._environment:
264+
return
265+
if overrides := self._environment.identity_overrides:
266+
self._identity_overrides_by_identifier = {
267+
identity.identifier: identity for identity in overrides
268+
}
257269

258270
def _get_environment_from_api(self) -> EnvironmentModel:
259271
environment_data = self._get_json_response(self.environment_url, method="GET")
@@ -269,7 +281,7 @@ def _get_environment_flags_from_document(self) -> Flags:
269281
def _get_identity_flags_from_document(
270282
self, identifier: str, traits: typing.Dict[str, typing.Any]
271283
) -> Flags:
272-
identity_model = self._build_identity_model(identifier, **traits)
284+
identity_model = self._get_identity_model(identifier, **traits)
273285
feature_states = engine.get_identity_feature_states(
274286
self._environment, identity_model
275287
)
@@ -334,7 +346,11 @@ def _get_json_response(self, url: str, method: str, body: dict = None):
334346
"Unable to get valid response from Flagsmith API."
335347
) from e
336348

337-
def _build_identity_model(self, identifier: str, **traits):
349+
def _get_identity_model(
350+
self,
351+
identifier: str,
352+
**traits: TraitValue,
353+
) -> IdentityModel:
338354
if not self._environment:
339355
raise FlagsmithClientError(
340356
"Unable to build identity model when no local environment present."
@@ -344,6 +360,11 @@ def _build_identity_model(self, identifier: str, **traits):
344360
TraitModel(trait_key=key, trait_value=value)
345361
for key, value in traits.items()
346362
]
363+
364+
if identity := self._identity_overrides_by_identifier.get(identifier):
365+
identity.update_traits(trait_models)
366+
return identity
367+
347368
return IdentityModel(
348369
identifier=identifier,
349370
environment_api_key=self._environment.api_key,

flagsmith/models.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,19 @@
1010
@dataclass
1111
class BaseFlag:
1212
enabled: bool
13-
value: typing.Union[str, int, float, bool, type(None)]
14-
is_default: bool
13+
value: typing.Union[str, int, float, bool, None]
1514

1615

16+
@dataclass
1717
class DefaultFlag(BaseFlag):
18-
def __init__(self, *args, **kwargs):
19-
super().__init__(*args, is_default=True, **kwargs)
18+
is_default: bool = field(default=True)
2019

2120

21+
@dataclass
2222
class Flag(BaseFlag):
23-
def __init__(self, *args, feature_id: int, feature_name: str, **kwargs):
24-
super().__init__(*args, is_default=False, **kwargs)
25-
self.feature_id = feature_id
26-
self.feature_name = feature_name
23+
feature_id: int
24+
feature_name: str
25+
is_default: bool = field(default=False)
2726

2827
@classmethod
2928
def from_feature_state_model(

poetry.lock

Lines changed: 273 additions & 317 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ documentation = "https://docs.flagsmith.com"
1010
packages = [{ include = "flagsmith" }]
1111

1212
[tool.poetry.dependencies]
13-
python = ">=3.7.0,<4"
13+
python = ">=3.8.0,<4"
1414
requests = "^2.27.1"
1515
requests-futures = "^1.0.0"
16-
flagsmith-flag-engine = "^5.0.0"
16+
flagsmith-flag-engine = "^5.1.0"
1717
sseclient-py = "^1.8.0"
1818
pytz = "^2023.4"
1919

@@ -28,6 +28,7 @@ isort = "^5.10.1"
2828

2929
[tool.poetry.group.dev.dependencies]
3030
pytest = "^7.4.0"
31+
pytest-cov = "^4.1.0"
3132

3233
[build-system]
3334
requires = ["poetry-core>=1.0.0"]

tests/data/environment.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,30 @@
5353
"enabled": true
5454
}
5555
],
56-
"updated_at": "2023-07-14 16:12:00.000000"
56+
"updated_at": "2023-07-14 16:12:00.000000",
57+
"identity_overrides": [
58+
{
59+
"identifier": "overridden-id",
60+
"identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
61+
"created_date": "2019-08-27T14:53:45.698555Z",
62+
"updated_at": "2023-07-14 16:12:00.000000",
63+
"environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
64+
"identity_features": [
65+
{
66+
"id": 1,
67+
"feature": {
68+
"id": 1,
69+
"name": "some_feature",
70+
"type": "STANDARD"
71+
},
72+
"featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
73+
"feature_state_value": "some-overridden-value",
74+
"enabled": false,
75+
"environment": 1,
76+
"identity": null,
77+
"feature_segment": null
78+
}
79+
]
80+
}
81+
]
5782
}

tests/test_flagsmith.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import time
23
import typing
34
import uuid
45

@@ -512,3 +513,30 @@ def test_error_raised_when_realtime_updates_is_true_and_local_evaluation_false(
512513
enable_local_evaluation=False,
513514
enable_realtime_updates=True,
514515
)
516+
517+
518+
@responses.activate()
519+
def test_flagsmith_client_get_identity_flags__local_evaluation__returns_expected(
520+
environment_json: str,
521+
server_api_key: str,
522+
) -> None:
523+
# Given
524+
identifier = "overridden-id"
525+
526+
api_url = "https://mocked.flagsmith.com/api/v1/"
527+
environment_document_url = f"{api_url}environment-document/"
528+
responses.add(method="GET", url=environment_document_url, body=environment_json)
529+
530+
flagsmith = Flagsmith(
531+
environment_key=server_api_key,
532+
api_url=api_url,
533+
enable_local_evaluation=True,
534+
)
535+
time.sleep(0.1)
536+
537+
# When
538+
flag = flagsmith.get_identity_flags(identifier).get_flag("some_feature")
539+
540+
# Then
541+
assert flag.enabled is False
542+
assert flag.value == "some-overridden-value"

0 commit comments

Comments
 (0)