11import logging
22import sys
33import typing
4- from datetime import timezone
4+ from datetime import datetime
55
6- import pydantic
76import requests
87from 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
148from requests .adapters import HTTPAdapter
159from requests .utils import default_user_agent
1610from urllib3 import Retry
1711
1812from flagsmith .analytics import AnalyticsProcessor
1913from 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+ )
2018from flagsmith .models import DefaultFlag , Flags , Segment
21- from flagsmith .offline_handlers import BaseOfflineHandler
19+ from flagsmith .offline_handlers import OfflineHandler
2220from flagsmith .polling_manager import EnvironmentDataPollingManager
2321from 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
3023from flagsmith .utils .identities import generate_identity_data
3124from 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