Skip to content

Commit 1811927

Browse files
committed
feat: Engine V7 compatibility
1 parent a55a921 commit 1811927

File tree

10 files changed

+548
-231
lines changed

10 files changed

+548
-231
lines changed

flagsmith/flagsmith.py

Lines changed: 76 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
11
import logging
22
import sys
33
import typing
4-
from datetime import timezone
4+
from datetime import datetime
55

6-
import pydantic
76
import requests
87
from flag_engine import engine
9-
from flag_engine.context.mappers import map_environment_identity_to_context
10-
from flag_engine.environments.models import EnvironmentModel
11-
from flag_engine.identities.models import IdentityModel
12-
from flag_engine.identities.traits.models import TraitModel
13-
from flag_engine.identities.traits.types import TraitValue
148
from requests.adapters import HTTPAdapter
159
from requests.utils import default_user_agent
1610
from urllib3 import Retry
1711

1812
from flagsmith.analytics import AnalyticsProcessor
1913
from flagsmith.exceptions import FlagsmithAPIError, FlagsmithClientError
14+
from flagsmith.mappers import (
15+
map_environment_document_to_context,
16+
map_environment_document_to_environment_updated_at,
17+
)
2018
from flagsmith.models import DefaultFlag, Flags, Segment
21-
from flagsmith.offline_handlers import BaseOfflineHandler
19+
from flagsmith.offline_handlers import OfflineHandler
2220
from flagsmith.polling_manager import EnvironmentDataPollingManager
2321
from flagsmith.streaming_manager import EventStreamManager, StreamEvent
24-
from flagsmith.types import (
25-
ApplicationMetadata,
26-
JsonType,
27-
TraitConfig,
28-
TraitMapping,
29-
)
22+
from flagsmith.types import ApplicationMetadata, JsonType, TraitMapping
3023
from flagsmith.utils.identities import generate_identity_data
3124
from flagsmith.version import __version__
3225

@@ -72,7 +65,7 @@ def __init__(
7265
] = None,
7366
proxies: typing.Optional[typing.Dict[str, str]] = None,
7467
offline_mode: bool = False,
75-
offline_handler: typing.Optional[BaseOfflineHandler] = None,
68+
offline_handler: typing.Optional[OfflineHandler] = None,
7669
enable_realtime_updates: bool = False,
7770
application_metadata: typing.Optional[ApplicationMetadata] = None,
7871
):
@@ -112,8 +105,8 @@ def __init__(
112105
self.default_flag_handler = default_flag_handler
113106
self.enable_realtime_updates = enable_realtime_updates
114107
self._analytics_processor: typing.Optional[AnalyticsProcessor] = None
115-
self._environment: typing.Optional[EnvironmentModel] = None
116-
self._identity_overrides_by_identifier: typing.Dict[str, IdentityModel] = {}
108+
self._evaluation_context: typing.Optional[engine.EvaluationContext] = None
109+
self._environment_updated_at: typing.Optional[datetime] = None
117110

118111
# argument validation
119112
if offline_mode and not offline_handler:
@@ -129,7 +122,7 @@ def __init__(
129122
)
130123

131124
if self.offline_handler:
132-
self._environment = self.offline_handler.get_environment()
125+
self._evaluation_context = self.offline_handler.get_evaluation_context()
133126

134127
if not self.offline_mode:
135128
if not environment_key:
@@ -182,10 +175,13 @@ def _initialise_local_evaluation(self) -> None:
182175
# method calls, update the environment manually.
183176
self.update_environment()
184177
if self.enable_realtime_updates:
185-
if not self._environment:
178+
if not self._evaluation_context:
186179
raise ValueError("Unable to get environment from API key")
187180

188-
stream_url = f"{self.realtime_api_url}sse/environments/{self._environment.api_key}/stream"
181+
stream_url = (
182+
f"{self.realtime_api_url}sse/environments/"
183+
f"{self._evaluation_context['environment']['key']}/stream"
184+
)
189185

190186
self.event_stream_thread = EventStreamManager(
191187
stream_url=stream_url,
@@ -207,14 +203,10 @@ def _initialise_local_evaluation(self) -> None:
207203
self.environment_data_polling_manager_thread.start()
208204

209205
def handle_stream_event(self, event: StreamEvent) -> None:
210-
if not self._environment:
206+
if not (environment_updated_at := self._environment_updated_at):
211207
raise ValueError(
212-
"Unable to access environment. Environment should not be null"
208+
"Cannot handle stream events before retrieving initial environment"
213209
)
214-
environment_updated_at = self._environment.updated_at
215-
if environment_updated_at.tzinfo is None:
216-
environment_updated_at = environment_updated_at.astimezone(timezone.utc)
217-
218210
if event.updated_at > environment_updated_at:
219211
self.update_environment()
220212

@@ -224,7 +216,9 @@ def get_environment_flags(self) -> Flags:
224216
225217
:return: Flags object holding all the flags for the current environment.
226218
"""
227-
if (self.offline_mode or self.enable_local_evaluation) and self._environment:
219+
if (
220+
self.offline_mode or self.enable_local_evaluation
221+
) and self._evaluation_context:
228222
return self._get_environment_flags_from_document()
229223
return self._get_environment_flags_from_api()
230224

@@ -250,7 +244,9 @@ def get_identity_flags(
250244
:return: Flags object holding all the flags for the given identity.
251245
"""
252246
traits = traits or {}
253-
if (self.offline_mode or self.enable_local_evaluation) and self._environment:
247+
if (
248+
self.offline_mode or self.enable_local_evaluation
249+
) and self._evaluation_context:
254250
return self._get_identity_flags_from_document(identifier, traits)
255251
return self._get_identity_flags_from_api(
256252
identifier,
@@ -261,7 +257,7 @@ def get_identity_flags(
261257
def get_identity_segments(
262258
self,
263259
identifier: str,
264-
traits: typing.Optional[typing.Mapping[str, TraitValue]] = None,
260+
traits: typing.Optional[typing.Mapping[str, engine.ContextValue]] = None,
265261
) -> typing.List[Segment]:
266262
"""
267263
Get a list of segments that the given identity is in.
@@ -272,37 +268,48 @@ def get_identity_segments(
272268
Flagsmith, e.g. {"num_orders": 10}
273269
:return: list of Segment objects that the identity is part of.
274270
"""
275-
276-
if not self._environment:
271+
if not self._evaluation_context:
277272
raise FlagsmithClientError(
278273
"Local evaluation required to obtain identity segments."
279274
)
280275

281-
traits = traits or {}
282-
identity_model = self._get_identity_model(identifier, **traits)
283-
context = map_environment_identity_to_context(
284-
environment=self._environment,
285-
identity=identity_model,
286-
override_traits=None,
287-
)
276+
identity_key = f"{self._evaluation_context['environment']['key']}_{identifier}"
277+
context: engine.EvaluationContext = {
278+
**self._evaluation_context,
279+
"identity": {
280+
"identifier": identifier,
281+
"key": identity_key,
282+
"traits": dict(traits or {}),
283+
},
284+
}
285+
288286
evaluation_result = engine.get_evaluation_result(
289287
context=context,
290288
)
291289
return [
292-
Segment(id=int(sm["key"]), name=sm["name"])
293-
for sm in evaluation_result.get("segments", [])
290+
Segment(id=int(segment_result["key"]), name=segment_result["name"])
291+
for segment_result in evaluation_result["segments"]
294292
]
295293

296294
def update_environment(self) -> None:
297295
try:
298-
self._environment = self._get_environment_from_api()
299-
except (FlagsmithAPIError, pydantic.ValidationError):
300-
logger.exception("Error updating environment")
296+
environment_data = self._get_json_response(
297+
self.environment_url, method="GET"
298+
)
299+
except FlagsmithAPIError:
300+
logger.exception("Error retrieving environment document from API")
301301
else:
302-
if overrides := self._environment.identity_overrides:
303-
self._identity_overrides_by_identifier = {
304-
identity.identifier: identity for identity in overrides
305-
}
302+
try:
303+
self._evaluation_context, self._environment_updated_at = (
304+
map_environment_document_to_context(
305+
environment_data,
306+
),
307+
map_environment_document_to_environment_updated_at(
308+
environment_data,
309+
),
310+
)
311+
except (KeyError, TypeError, ValueError):
312+
logger.exception("Error parsing environment document")
306313

307314
def _get_headers(
308315
self,
@@ -322,22 +329,11 @@ def _get_headers(
322329
headers.update(custom_headers or {})
323330
return headers
324331

325-
def _get_environment_from_api(self) -> EnvironmentModel:
326-
environment_data = self._get_json_response(self.environment_url, method="GET")
327-
return EnvironmentModel.model_validate(environment_data)
328-
329332
def _get_environment_flags_from_document(self) -> Flags:
330-
if self._environment is None:
333+
if self._evaluation_context is None:
331334
raise TypeError("No environment present")
332-
identity = self._get_identity_model(identifier="", traits=None)
333335

334-
context = map_environment_identity_to_context(
335-
environment=self._environment,
336-
identity=identity,
337-
override_traits=None,
338-
)
339-
340-
evaluation_result = engine.get_evaluation_result(context=context)
336+
evaluation_result = engine.get_evaluation_result(self._evaluation_context)
341337

342338
return Flags.from_evaluation_result(
343339
evaluation_result=evaluation_result,
@@ -346,18 +342,29 @@ def _get_environment_flags_from_document(self) -> Flags:
346342
)
347343

348344
def _get_identity_flags_from_document(
349-
self, identifier: str, traits: TraitMapping
345+
self,
346+
identifier: str,
347+
traits: TraitMapping,
350348
) -> Flags:
351-
identity_model = self._get_identity_model(identifier, **traits)
352-
if self._environment is None:
349+
if self._evaluation_context is None:
353350
raise TypeError("No environment present")
354351

355-
context = map_environment_identity_to_context(
356-
environment=self._environment,
357-
identity=identity_model,
358-
override_traits=None,
359-
)
360-
352+
identity_key = f"{self._evaluation_context['environment']['key']}_{identifier}"
353+
context: engine.EvaluationContext = {
354+
**self._evaluation_context,
355+
"identity": {
356+
"identifier": identifier,
357+
"key": identity_key,
358+
"traits": {
359+
trait_key: (
360+
trait_value_or_config["value"]
361+
if isinstance(trait_value_or_config, dict)
362+
else trait_value_or_config
363+
)
364+
for trait_key, trait_value_or_config in traits.items()
365+
},
366+
},
367+
}
361368
evaluation_result = engine.get_evaluation_result(
362369
context=context,
363370
)
@@ -435,34 +442,6 @@ def _get_json_response(
435442
"Unable to get valid response from Flagsmith API."
436443
) from e
437444

438-
def _get_identity_model(
439-
self,
440-
identifier: str,
441-
**traits: typing.Union[TraitValue, TraitConfig],
442-
) -> IdentityModel:
443-
if not self._environment:
444-
raise FlagsmithClientError(
445-
"Unable to build identity model when no local environment present."
446-
)
447-
448-
trait_models = [
449-
TraitModel(
450-
trait_key=key,
451-
trait_value=value["value"] if isinstance(value, dict) else value,
452-
)
453-
for key, value in traits.items()
454-
]
455-
456-
if identity := self._identity_overrides_by_identifier.get(identifier):
457-
identity.update_traits(trait_models)
458-
return identity
459-
460-
return IdentityModel(
461-
identifier=identifier,
462-
environment_api_key=self._environment.api_key,
463-
identity_traits=trait_models,
464-
)
465-
466445
def __del__(self) -> None:
467446
if hasattr(self, "environment_data_polling_manager_thread"):
468447
self.environment_data_polling_manager_thread.stop()

0 commit comments

Comments
 (0)