33import json
44import urllib .parse
55import asyncio
6- from datetime import datetime
6+ from datetime import datetime
77from typing import Dict , Any , Callable
88from asgiref .sync import sync_to_async
99
1313logger = logging .getLogger (__name__ )
1414logging .getLogger ("httpx" ).setLevel (logging .ERROR )
1515
16+
1617class RemoteFeatureFlagsProvider :
1718 FLAGS_URL_PATH = "/flags"
1819
19- def __init__ (self , token : str , config : RemoteFlagsConfig , version : str , tracker : Callable ) -> None :
20+ def __init__ (
21+ self , token : str , config : RemoteFlagsConfig , version : str , tracker : Callable
22+ ) -> None :
2023 self ._token : str = token
2124 self ._config : RemoteFlagsConfig = config
2225 self ._version : str = version
@@ -29,22 +32,30 @@ def __init__(self, token: str, config: RemoteFlagsConfig, version: str, tracker:
2932 "timeout" : httpx .Timeout (config .request_timeout_in_seconds ),
3033 }
3134
32- self ._async_client : httpx .AsyncClient = httpx .AsyncClient (** httpx_client_parameters )
35+ self ._async_client : httpx .AsyncClient = httpx .AsyncClient (
36+ ** httpx_client_parameters
37+ )
3338 self ._sync_client : httpx .Client = httpx .Client (** httpx_client_parameters )
3439 self ._request_params_base = prepare_common_query_params (self ._token , version )
3540
36- async def aget_variant_value (self , flag_key : str , fallback_value : Any , context : Dict [str , Any ]) -> Any :
41+ async def aget_variant_value (
42+ self , flag_key : str , fallback_value : Any , context : Dict [str , Any ]
43+ ) -> Any :
3744 """
3845 Gets the selected variant value of a feature flag variant for the current user context from remote server.
3946
4047 :param str flag_key: The key of the feature flag to evaluate
4148 :param Any fallback_value: The default value to return if the flag is not found or evaluation fails
4249 :param Dict[str, Any] context: Context dictionary containing user attributes and rollout context
4350 """
44- variant = await self .aget_variant (flag_key , SelectedVariant (variant_value = fallback_value ), context )
51+ variant = await self .aget_variant (
52+ flag_key , SelectedVariant (variant_value = fallback_value ), context
53+ )
4554 return variant .variant_value
4655
47- async def aget_variant (self , flag_key : str , fallback_value : SelectedVariant , context : Dict [str , Any ]) -> SelectedVariant :
56+ async def aget_variant (
57+ self , flag_key : str , fallback_value : SelectedVariant , context : Dict [str , Any ]
58+ ) -> SelectedVariant :
4859 """
4960 Asynchronously gets the selected variant of a feature flag variant for the current user context from remote server.
5061
@@ -58,12 +69,19 @@ async def aget_variant(self, flag_key: str, fallback_value: SelectedVariant, con
5869 response = await self ._async_client .get (self .FLAGS_URL_PATH , params = params )
5970 end_time = datetime .now ()
6071 self ._instrument_call (start_time , end_time )
61- selected_variant , is_fallback = self ._handle_response (flag_key , fallback_value , response )
72+ selected_variant , is_fallback = self ._handle_response (
73+ flag_key , fallback_value , response
74+ )
6275
6376 if not is_fallback and (distinct_id := context .get ("distinct_id" )):
64- properties = self ._build_tracking_properties (flag_key , selected_variant , start_time , end_time )
77+ properties = self ._build_tracking_properties (
78+ flag_key , selected_variant , start_time , end_time
79+ )
6580 asyncio .create_task (
66- sync_to_async (self ._tracker , thread_sensitive = False )(distinct_id , EXPOSURE_EVENT , properties ))
81+ sync_to_async (self ._tracker , thread_sensitive = False )(
82+ distinct_id , EXPOSURE_EVENT , properties
83+ )
84+ )
6785
6886 return selected_variant
6987 except Exception :
@@ -80,20 +98,26 @@ async def ais_enabled(self, flag_key: str, context: Dict[str, Any]) -> bool:
8098 variant_value = await self .aget_variant_value (flag_key , False , context )
8199 return bool (variant_value )
82100
83- def get_variant_value (self , flag_key : str , fallback_value : Any , context : Dict [str , Any ]) -> Any :
101+ def get_variant_value (
102+ self , flag_key : str , fallback_value : Any , context : Dict [str , Any ]
103+ ) -> Any :
84104 """
85105 Synchronously gets the value of a feature flag variant from remote server.
86106
87107 :param str flag_key: The key of the feature flag to evaluate
88108 :param Any fallback_value: The default value to return if the flag is not found or evaluation fails
89109 :param Dict[str, Any] context: Context dictionary containing user attributes and rollout context
90110 """
91- variant = self .get_variant (flag_key , SelectedVariant (variant_value = fallback_value ), context )
111+ variant = self .get_variant (
112+ flag_key , SelectedVariant (variant_value = fallback_value ), context
113+ )
92114 return variant .variant_value
93115
94- def get_variant (self , flag_key : str , fallback_value : SelectedVariant , context : Dict [str , Any ]) -> SelectedVariant :
116+ def get_variant (
117+ self , flag_key : str , fallback_value : SelectedVariant , context : Dict [str , Any ]
118+ ) -> SelectedVariant :
95119 """
96- Synchronously getsthe selected variant for a feature flag from remote server.
120+ Synchronously gets the selected variant for a feature flag from remote server.
97121
98122 :param str flag_key: The key of the feature flag to evaluate
99123 :param SelectedVariant fallback_value: The default variant to return if evaluation fails
@@ -105,10 +129,14 @@ def get_variant(self, flag_key: str, fallback_value: SelectedVariant, context: D
105129 response = self ._sync_client .get (self .FLAGS_URL_PATH , params = params )
106130 end_time = datetime .now ()
107131 self ._instrument_call (start_time , end_time )
108- selected_variant , is_fallback = self ._handle_response (flag_key , fallback_value , response )
132+ selected_variant , is_fallback = self ._handle_response (
133+ flag_key , fallback_value , response
134+ )
109135
110136 if not is_fallback and (distinct_id := context .get ("distinct_id" )):
111- properties = self ._build_tracking_properties (flag_key , selected_variant , start_time , end_time )
137+ properties = self ._build_tracking_properties (
138+ flag_key , selected_variant , start_time , end_time
139+ )
112140 self ._tracker (distinct_id , EXPOSURE_EVENT , properties )
113141
114142 return selected_variant
@@ -126,46 +154,57 @@ def is_enabled(self, flag_key: str, context: Dict[str, Any]) -> bool:
126154 variant_value = self .get_variant_value (flag_key , False , context )
127155 return bool (variant_value )
128156
129- def _prepare_query_params (self , flag_key : str , context : Dict [str , Any ]) -> Dict [str , str ]:
157+ def _prepare_query_params (
158+ self , flag_key : str , context : Dict [str , Any ]
159+ ) -> Dict [str , str ]:
130160 params = self ._request_params_base .copy ()
131- context_json = json .dumps (context ).encode (' utf-8' )
161+ context_json = json .dumps (context ).encode (" utf-8" )
132162 url_encoded_context = urllib .parse .quote (context_json )
133- params .update ({
134- 'flag_key' : flag_key ,
135- 'context' : url_encoded_context
136- })
163+ params .update ({"flag_key" : flag_key , "context" : url_encoded_context })
137164 return params
138165
139166 def _instrument_call (self , start_time : datetime , end_time : datetime ) -> None :
140167 request_duration = end_time - start_time
141168 formatted_start_time = start_time .isoformat ()
142169 formatted_end_time = end_time .isoformat ()
143- logging .info (f"Request started at '{ formatted_start_time } ', completed at '{ formatted_end_time } ', duration: '{ request_duration .total_seconds ():.3f} s'" )
144-
145- def _build_tracking_properties (self , flag_key : str , variant : SelectedVariant , start_time : datetime , end_time : datetime ) -> Dict [str , Any ]:
170+ logging .info (
171+ f"Request started at '{ formatted_start_time } ', completed at '{ formatted_end_time } ', duration: '{ request_duration .total_seconds ():.3f} s'"
172+ )
173+
174+ def _build_tracking_properties (
175+ self ,
176+ flag_key : str ,
177+ variant : SelectedVariant ,
178+ start_time : datetime ,
179+ end_time : datetime ,
180+ ) -> Dict [str , Any ]:
146181 request_duration = end_time - start_time
147182 formatted_start_time = start_time .isoformat ()
148183 formatted_end_time = end_time .isoformat ()
149184
150185 return {
151- ' Experiment name' : flag_key ,
152- ' Variant name' : variant .variant_key ,
153- ' $experiment_type' : ' feature_flag' ,
186+ " Experiment name" : flag_key ,
187+ " Variant name" : variant .variant_key ,
188+ " $experiment_type" : " feature_flag" ,
154189 "Flag evaluation mode" : "remote" ,
155190 "Variant fetch start time" : formatted_start_time ,
156191 "Variant fetch complete time" : formatted_end_time ,
157192 "Variant fetch latency (ms)" : request_duration .total_seconds () * 1000 ,
158193 }
159194
160- def _handle_response (self , flag_key : str , fallback_value : SelectedVariant , response : httpx .Response ) -> tuple [SelectedVariant , bool ]:
195+ def _handle_response (
196+ self , flag_key : str , fallback_value : SelectedVariant , response : httpx .Response
197+ ) -> tuple [SelectedVariant , bool ]:
161198 response .raise_for_status ()
162199
163200 flags_response = RemoteFlagsResponse .model_validate (response .json ())
164201
165202 if flag_key in flags_response .flags :
166203 return flags_response .flags [flag_key ], False
167204 else :
168- logging .warning (f"Flag '{ flag_key } ' not found in remote response. Returning fallback, '{ fallback_value } '" )
205+ logging .warning (
206+ f"Flag '{ flag_key } ' not found in remote response. Returning fallback, '{ fallback_value } '"
207+ )
169208 return fallback_value , True
170209
171210 def __enter__ (self ):
0 commit comments