Skip to content

Commit

Permalink
feat: Add indentity overrides to local evaluation mode
Browse files Browse the repository at this point in the history
- Bump flag-engine
 - Add environment overrides parsing
 - Add pytet-cove
 - Fix black pre-commit hook
 - Fix Flag dataclasses
  • Loading branch information
khvn26 committed Feb 7, 2024
1 parent 5db6d24 commit 2577da7
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 333 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ flagsmith.egg-info/

.envrc
.tool-versions

.coverage
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: stable
rev: 23.3.0
hooks:
- id: black
language_version: python3
Expand Down
29 changes: 25 additions & 4 deletions flagsmith/flagsmith.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import requests
from flag_engine import engine
from flag_engine.environments.models import EnvironmentModel
from flag_engine.identities.models import IdentityModel, TraitModel
from flag_engine.identities.models import IdentityModel
from flag_engine.identities.traits.models import TraitModel
from flag_engine.identities.traits.types import TraitValue
from flag_engine.segments.evaluator import get_identity_segments
from requests.adapters import HTTPAdapter
from urllib3 import Retry
Expand Down Expand Up @@ -94,6 +96,7 @@ def __init__(
self.enable_realtime_updates = enable_realtime_updates
self._analytics_processor = None
self._environment = None
self._identity_overrides_by_identifier: typing.Dict[str, IdentityModel] = {}

# argument validation
if offline_mode and not offline_handler:
Expand Down Expand Up @@ -248,12 +251,21 @@ def get_identity_segments(
)

traits = traits or {}
identity_model = self._build_identity_model(identifier, **traits)
identity_model = self._get_identity_model(identifier, **traits)
segment_models = get_identity_segments(self._environment, identity_model)
return [Segment(id=sm.id, name=sm.name) for sm in segment_models]

def update_environment(self):
self._environment = self._get_environment_from_api()
self._update_overrides()

def _update_overrides(self) -> None:
if not self._environment:
return
if overrides := self._environment.identity_overrides:
self._identity_overrides_by_identifier = {
identity.identifier: identity for identity in overrides
}

def _get_environment_from_api(self) -> EnvironmentModel:
environment_data = self._get_json_response(self.environment_url, method="GET")
Expand All @@ -269,7 +281,7 @@ def _get_environment_flags_from_document(self) -> Flags:
def _get_identity_flags_from_document(
self, identifier: str, traits: typing.Dict[str, typing.Any]
) -> Flags:
identity_model = self._build_identity_model(identifier, **traits)
identity_model = self._get_identity_model(identifier, **traits)
feature_states = engine.get_identity_feature_states(
self._environment, identity_model
)
Expand Down Expand Up @@ -334,7 +346,11 @@ def _get_json_response(self, url: str, method: str, body: dict = None):
"Unable to get valid response from Flagsmith API."
) from e

def _build_identity_model(self, identifier: str, **traits):
def _get_identity_model(
self,
identifier: str,
**traits: TraitValue,
) -> IdentityModel:
if not self._environment:
raise FlagsmithClientError(
"Unable to build identity model when no local environment present."
Expand All @@ -344,6 +360,11 @@ def _build_identity_model(self, identifier: str, **traits):
TraitModel(trait_key=key, trait_value=value)
for key, value in traits.items()
]

if identity := self._identity_overrides_by_identifier.get(identifier):
identity.update_traits(trait_models)
return identity

return IdentityModel(
identifier=identifier,
environment_api_key=self._environment.api_key,
Expand Down
15 changes: 7 additions & 8 deletions flagsmith/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,19 @@
@dataclass
class BaseFlag:
enabled: bool
value: typing.Union[str, int, float, bool, type(None)]
is_default: bool
value: typing.Union[str, int, float, bool, None]


@dataclass
class DefaultFlag(BaseFlag):
def __init__(self, *args, **kwargs):
super().__init__(*args, is_default=True, **kwargs)
is_default: bool = field(default=True)


@dataclass
class Flag(BaseFlag):
def __init__(self, *args, feature_id: int, feature_name: str, **kwargs):
super().__init__(*args, is_default=False, **kwargs)
self.feature_id = feature_id
self.feature_name = feature_name
feature_id: int
feature_name: str
is_default: bool = field(default=False)

@classmethod
def from_feature_state_model(
Expand Down
590 changes: 273 additions & 317 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ documentation = "https://docs.flagsmith.com"
packages = [{ include = "flagsmith" }]

[tool.poetry.dependencies]
python = ">=3.7.0,<4"
python = ">=3.8.0,<4"
requests = "^2.27.1"
requests-futures = "^1.0.0"
flagsmith-flag-engine = "^5.0.0"
flagsmith-flag-engine = "^5.1.0"
sseclient-py = "^1.8.0"
pytz = "^2023.4"

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

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-cov = "^4.1.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
27 changes: 26 additions & 1 deletion tests/data/environment.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,30 @@
"enabled": true
}
],
"updated_at": "2023-07-14 16:12:00.000000"
"updated_at": "2023-07-14 16:12:00.000000",
"identity_overrides": [
{
"identifier": "overridden-id",
"identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
"created_date": "2019-08-27T14:53:45.698555Z",
"updated_at": "2023-07-14 16:12:00.000000",
"environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
"identity_features": [
{
"id": 1,
"feature": {
"id": 1,
"name": "some_feature",
"type": "STANDARD"
},
"featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
"feature_state_value": "some-overridden-value",
"enabled": false,
"environment": 1,
"identity": null,
"feature_segment": null
}
]
}
]
}
28 changes: 28 additions & 0 deletions tests/test_flagsmith.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import time
import typing
import uuid

Expand Down Expand Up @@ -512,3 +513,30 @@ def test_error_raised_when_realtime_updates_is_true_and_local_evaluation_false(
enable_local_evaluation=False,
enable_realtime_updates=True,
)


@responses.activate()
def test_flagsmith_client_get_identity_flags__local_evaluation__returns_expected(
environment_json: str,
server_api_key: str,
) -> None:
# Given
identifier = "overridden-id"

api_url = "https://mocked.flagsmith.com/api/v1/"
environment_document_url = f"{api_url}environment-document/"
responses.add(method="GET", url=environment_document_url, body=environment_json)

flagsmith = Flagsmith(
environment_key=server_api_key,
api_url=api_url,
enable_local_evaluation=True,
)
time.sleep(0.1)

# When
flag = flagsmith.get_identity_flags(identifier).get_flag("some_feature")

# Then
assert flag.enabled is False
assert flag.value == "some-overridden-value"

0 comments on commit 2577da7

Please sign in to comment.