11import logging
22import sys
33import typing
4- from datetime import timezone
4+ from datetime import datetime
5+ from urllib .parse import urljoin
56
6- import pydantic
77import requests
88from 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
149from requests .adapters import HTTPAdapter
1510from requests .utils import default_user_agent
1611from urllib3 import Retry
1712
1813from flagsmith .analytics import AnalyticsProcessor
1914from flagsmith .exceptions import FlagsmithAPIError , FlagsmithClientError
15+ from flagsmith .mappers import (
16+ map_context_and_identity_data_to_context ,
17+ map_environment_document_to_context ,
18+ map_environment_document_to_environment_updated_at ,
19+ )
2020from flagsmith .models import DefaultFlag , Flags , Segment
21- from flagsmith .offline_handlers import BaseOfflineHandler
21+ from flagsmith .offline_handlers import OfflineHandler
2222from flagsmith .polling_manager import EnvironmentDataPollingManager
23- from flagsmith .streaming_manager import EventStreamManager , StreamEvent
23+ from flagsmith .streaming_manager import EventStreamManager
2424from flagsmith .types import (
2525 ApplicationMetadata ,
2626 JsonType ,
27- TraitConfig ,
27+ StreamEvent ,
2828 TraitMapping ,
2929)
3030from flagsmith .utils .identities import generate_identity_data
@@ -72,7 +72,7 @@ def __init__(
7272 ] = None ,
7373 proxies : typing .Optional [typing .Dict [str , str ]] = None ,
7474 offline_mode : bool = False ,
75- offline_handler : typing .Optional [BaseOfflineHandler ] = None ,
75+ offline_handler : typing .Optional [OfflineHandler ] = None ,
7676 enable_realtime_updates : bool = False ,
7777 application_metadata : typing .Optional [ApplicationMetadata ] = None ,
7878 ):
@@ -112,8 +112,8 @@ def __init__(
112112 self .default_flag_handler = default_flag_handler
113113 self .enable_realtime_updates = enable_realtime_updates
114114 self ._analytics_processor : typing .Optional [AnalyticsProcessor ] = None
115- self ._environment : typing .Optional [EnvironmentModel ] = None
116- self ._identity_overrides_by_identifier : typing .Dict [ str , IdentityModel ] = {}
115+ self ._evaluation_context : typing .Optional [engine . EvaluationContext ] = None
116+ self ._environment_updated_at : typing .Optional [ datetime ] = None
117117
118118 # argument validation
119119 if offline_mode and not offline_handler :
@@ -129,7 +129,7 @@ def __init__(
129129 )
130130
131131 if self .offline_handler :
132- self ._environment = self .offline_handler .get_environment ()
132+ self ._evaluation_context = self .offline_handler .get_evaluation_context ()
133133
134134 if not self .offline_mode :
135135 if not environment_key :
@@ -159,9 +159,9 @@ def __init__(
159159 self .request_timeout_seconds = request_timeout_seconds
160160 self .session .mount (self .api_url , HTTPAdapter (max_retries = retries ))
161161
162- self .environment_flags_url = f" { self .api_url } flags/"
163- self .identities_url = f" { self .api_url } identities/"
164- self .environment_url = f" { self .api_url } environment-document/"
162+ self .environment_flags_url = urljoin ( self .api_url , " flags/")
163+ self .identities_url = urljoin ( self .api_url , " identities/")
164+ self .environment_url = urljoin ( self .api_url , " environment-document/")
165165
166166 if self .enable_local_evaluation :
167167 if not environment_key .startswith ("ser." ):
@@ -182,10 +182,13 @@ def _initialise_local_evaluation(self) -> None:
182182 # method calls, update the environment manually.
183183 self .update_environment ()
184184 if self .enable_realtime_updates :
185- if not self ._environment :
185+ if not self ._evaluation_context :
186186 raise ValueError ("Unable to get environment from API key" )
187187
188- stream_url = f"{ self .realtime_api_url } sse/environments/{ self ._environment .api_key } /stream"
188+ stream_url = urljoin (
189+ self .realtime_api_url ,
190+ f"sse/environments/{ self ._evaluation_context ['environment' ]['key' ]} /stream" ,
191+ )
189192
190193 self .event_stream_thread = EventStreamManager (
191194 stream_url = stream_url ,
@@ -207,15 +210,11 @@ def _initialise_local_evaluation(self) -> None:
207210 self .environment_data_polling_manager_thread .start ()
208211
209212 def handle_stream_event (self , event : StreamEvent ) -> None :
210- if not self ._environment :
213+ if not ( environment_updated_at := self ._environment_updated_at ) :
211214 raise ValueError (
212- "Unable to access environment. Environment should not be null "
215+ "Cannot handle stream events before retrieving initial environment "
213216 )
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-
218- if event .updated_at > environment_updated_at :
217+ if event ["updated_at" ] > environment_updated_at :
219218 self .update_environment ()
220219
221220 def get_environment_flags (self ) -> Flags :
@@ -224,7 +223,9 @@ def get_environment_flags(self) -> Flags:
224223
225224 :return: Flags object holding all the flags for the current environment.
226225 """
227- if (self .offline_mode or self .enable_local_evaluation ) and self ._environment :
226+ if (
227+ self .offline_mode or self .enable_local_evaluation
228+ ) and self ._evaluation_context :
228229 return self ._get_environment_flags_from_document ()
229230 return self ._get_environment_flags_from_api ()
230231
@@ -250,7 +251,9 @@ def get_identity_flags(
250251 :return: Flags object holding all the flags for the given identity.
251252 """
252253 traits = traits or {}
253- if (self .offline_mode or self .enable_local_evaluation ) and self ._environment :
254+ if (
255+ self .offline_mode or self .enable_local_evaluation
256+ ) and self ._evaluation_context :
254257 return self ._get_identity_flags_from_document (identifier , traits )
255258 return self ._get_identity_flags_from_api (
256259 identifier ,
@@ -261,7 +264,7 @@ def get_identity_flags(
261264 def get_identity_segments (
262265 self ,
263266 identifier : str ,
264- traits : typing .Optional [typing .Mapping [str , TraitValue ]] = None ,
267+ traits : typing .Optional [typing .Mapping [str , engine . ContextValue ]] = None ,
265268 ) -> typing .List [Segment ]:
266269 """
267270 Get a list of segments that the given identity is in.
@@ -272,37 +275,44 @@ def get_identity_segments(
272275 Flagsmith, e.g. {"num_orders": 10}
273276 :return: list of Segment objects that the identity is part of.
274277 """
275-
276- if not self ._environment :
278+ if not self ._evaluation_context :
277279 raise FlagsmithClientError (
278280 "Local evaluation required to obtain identity segments."
279281 )
280282
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 ,
283+ context = map_context_and_identity_data_to_context (
284+ context = self ._evaluation_context ,
285+ identifier = identifier ,
286+ traits = traits ,
287287 )
288+
288289 evaluation_result = engine .get_evaluation_result (
289290 context = context ,
290291 )
291292 return [
292- Segment (id = int (sm ["key" ]), name = sm ["name" ])
293- for sm in evaluation_result . get ( "segments" , [])
293+ Segment (id = int (segment_result ["key" ]), name = segment_result ["name" ])
294+ for segment_result in evaluation_result [ "segments" ]
294295 ]
295296
296297 def update_environment (self ) -> None :
297298 try :
298- self ._environment = self ._get_environment_from_api ()
299- except (FlagsmithAPIError , pydantic .ValidationError ):
300- logger .exception ("Error updating environment" )
299+ environment_data = self ._get_json_response (
300+ self .environment_url , method = "GET"
301+ )
302+ except FlagsmithAPIError :
303+ logger .exception ("Error retrieving environment document from API" )
301304 else :
302- if overrides := self ._environment .identity_overrides :
303- self ._identity_overrides_by_identifier = {
304- identity .identifier : identity for identity in overrides
305- }
305+ try :
306+ self ._evaluation_context = map_environment_document_to_context (
307+ environment_data ,
308+ )
309+ self ._environment_updated_at = (
310+ map_environment_document_to_environment_updated_at (
311+ environment_data ,
312+ )
313+ )
314+ except (KeyError , TypeError , ValueError ):
315+ logger .exception ("Error parsing environment document" )
306316
307317 def _get_headers (
308318 self ,
@@ -322,22 +332,11 @@ def _get_headers(
322332 headers .update (custom_headers or {})
323333 return headers
324334
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-
329335 def _get_environment_flags_from_document (self ) -> Flags :
330- if self ._environment is None :
336+ if self ._evaluation_context is None :
331337 raise TypeError ("No environment present" )
332- identity = self ._get_identity_model (identifier = "" , traits = None )
333-
334- context = map_environment_identity_to_context (
335- environment = self ._environment ,
336- identity = identity ,
337- override_traits = None ,
338- )
339338
340- evaluation_result = engine .get_evaluation_result (context = context )
339+ evaluation_result = engine .get_evaluation_result (self . _evaluation_context )
341340
342341 return Flags .from_evaluation_result (
343342 evaluation_result = evaluation_result ,
@@ -346,18 +345,18 @@ def _get_environment_flags_from_document(self) -> Flags:
346345 )
347346
348347 def _get_identity_flags_from_document (
349- self , identifier : str , traits : TraitMapping
348+ self ,
349+ identifier : str ,
350+ traits : TraitMapping ,
350351 ) -> Flags :
351- identity_model = self ._get_identity_model (identifier , ** traits )
352- if self ._environment is None :
352+ if self ._evaluation_context is None :
353353 raise TypeError ("No environment present" )
354354
355- context = map_environment_identity_to_context (
356- environment = self ._environment ,
357- identity = identity_model ,
358- override_traits = None ,
355+ context = map_context_and_identity_data_to_context (
356+ context = self ._evaluation_context ,
357+ identifier = identifier ,
358+ traits = traits ,
359359 )
360-
361360 evaluation_result = engine .get_evaluation_result (
362361 context = context ,
363362 )
@@ -435,34 +434,6 @@ def _get_json_response(
435434 "Unable to get valid response from Flagsmith API."
436435 ) from e
437436
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-
466437 def __del__ (self ) -> None :
467438 if hasattr (self , "environment_data_polling_manager_thread" ):
468439 self .environment_data_polling_manager_thread .stop ()
0 commit comments