1616This module has required APIs for the clients to use Firebase Remote Config with python.
1717"""
1818
19- import json
19+ import asyncio
2020import logging
21- from typing import Dict , Optional , Literal , Union
21+ from typing import Dict , Optional , Literal , Union , Any
2222from enum import Enum
2323import re
2424import hashlib
25+ import requests
2526from firebase_admin import App , _http_client , _utils
2627import firebase_admin
2728
3233_REMOTE_CONFIG_ATTRIBUTE = '_remoteconfig'
3334MAX_CONDITION_RECURSION_DEPTH = 10
3435ValueSource = Literal ['default' , 'remote' , 'static' ] # Define the ValueSource type
36+
3537class PercentConditionOperator (Enum ):
3638 """Enum representing the available operators for percent conditions.
3739 """
@@ -62,19 +64,40 @@ class CustomSignalOperator(Enum):
6264 UNKNOWN = "UNKNOWN"
6365
6466class ServerTemplateData :
65- """Represents a Server Template Data class ."""
67+ """Parses, validates and encapsulates template data and metadata ."""
6668 def __init__ (self , etag , template_data ):
6769 """Initializes a new ServerTemplateData instance.
6870
6971 Args:
7072 etag: The string to be used for initialize the ETag property.
7173 template_data: The data to be parsed for getting the parameters and conditions.
74+
75+ Raises:
76+ ValueError: If the template data is not valid.
7277 """
73- self ._parameters = template_data ['parameters' ]
74- self ._conditions = template_data ['conditions' ]
75- self ._version = template_data ['version' ]
76- self ._parameter_groups = template_data ['parameterGroups' ]
77- self ._etag = etag
78+ if 'parameters' in template_data :
79+ if template_data ['parameters' ] is not None :
80+ self ._parameters = template_data ['parameters' ]
81+ else :
82+ raise ValueError ('Remote Config parameters must be a non-null object' )
83+ else :
84+ self ._parameters = {}
85+
86+ if 'conditions' in template_data :
87+ if template_data ['conditions' ] is not None :
88+ self ._conditions = template_data ['conditions' ]
89+ else :
90+ raise ValueError ('Remote Config conditions must be a non-null object' )
91+ else :
92+ self ._conditions = []
93+
94+ self ._version = ''
95+ if 'version' in template_data :
96+ self ._version = template_data ['version' ]
97+
98+ self ._etag = ''
99+ if etag is not None and isinstance (etag , str ):
100+ self ._etag = etag
78101
79102 @property
80103 def parameters (self ):
@@ -92,13 +115,9 @@ def version(self):
92115 def conditions (self ):
93116 return self ._conditions
94117
95- @property
96- def parameter_groups (self ):
97- return self ._parameter_groups
98-
99118
100119class ServerTemplate :
101- """Represents a Server Template with implementations for loading and evaluting the tempalte ."""
120+ """Represents a Server Template with implementations for loading and evaluting the template ."""
102121 def __init__ (self , app : App = None , default_config : Optional [Dict [str , str ]] = None ):
103122 """Initializes a ServerTemplate instance.
104123
@@ -112,14 +131,18 @@ def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = N
112131 # This gets set when the template is
113132 # fetched from RC servers via the load API, or via the set API.
114133 self ._cache = None
134+ self ._stringified_default_config : Dict [str , str ] = {}
135+
136+ # RC stores all remote values as string, but it's more intuitive
137+ # to declare default values with specific types, so this converts
138+ # the external declaration to an internal string representation.
115139 if default_config is not None :
116- self ._stringified_default_config = json .dumps (default_config )
117- else :
118- self ._stringified_default_config = None
140+ for key in default_config :
141+ self ._stringified_default_config [key ] = str (default_config [key ])
119142
120143 async def load (self ):
121144 """Fetches the server template and caches the data."""
122- self ._cache = await self ._rc_service .getServerTemplate ()
145+ self ._cache = await self ._rc_service .get_server_template ()
123146
124147 def evaluate (self , context : Optional [Dict [str , Union [str , int ]]] = None ) -> 'ServerConfig' :
125148 """Evaluates the cached server template to produce a ServerConfig.
@@ -140,21 +163,20 @@ def evaluate(self, context: Optional[Dict[str, Union[str, int]]] = None) -> 'Ser
140163 config_values = {}
141164 # Initializes config Value objects with default values.
142165 if self ._stringified_default_config is not None :
143- for key , value in json . loads ( self ._stringified_default_config ) .items ():
166+ for key , value in self ._stringified_default_config .items ():
144167 config_values [key ] = _Value ('default' , value )
145168 self ._evaluator = _ConditionEvaluator (self ._cache .conditions ,
146169 self ._cache .parameters , context ,
147170 config_values )
148171 return ServerConfig (config_values = self ._evaluator .evaluate ())
149172
150- def set (self , template ):
173+ def set (self , template : ServerTemplateData ):
151174 """Updates the cache to store the given template is of type ServerTemplateData.
152175
153176 Args:
154177 template: An object of type ServerTemplateData to be cached.
155178 """
156- if isinstance (template , ServerTemplateData ):
157- self ._cache = template
179+ self ._cache = template
158180
159181
160182class ServerConfig :
@@ -202,20 +224,28 @@ def __init__(self, app):
202224 base_url = remote_config_base_url ,
203225 headers = rc_headers , timeout = timeout )
204226
205-
206- def get_server_template (self ):
227+ async def get_server_template (self ):
207228 """Requests for a server template and converts the response to an instance of
208229 ServerTemplateData for storing the template parameters and conditions."""
209- url_prefix = self ._get_url_prefix ()
210- headers , response_json = self ._client .headers_and_body ('get' ,
211- url = url_prefix + '/namespaces/ \
212- firebase-server/serverRemoteConfig' )
213- return ServerTemplateData (headers .get ('ETag' ), response_json )
230+ try :
231+ loop = asyncio .get_event_loop ()
232+ headers , template_data = await loop .run_in_executor (None ,
233+ self ._client .headers_and_body ,
234+ 'get' , self ._get_url ())
235+ except requests .exceptions .RequestException as error :
236+ raise self ._handle_remote_config_error (error )
237+ else :
238+ return ServerTemplateData (headers .get ('etag' ), template_data )
214239
215- def _get_url_prefix (self ):
216- # Returns project prefix for url, in the format of
217- # /v1/projects/${projectId}
218- return "/v1/projects/{0}" .format (self ._project_id )
240+ def _get_url (self ):
241+ """Returns project prefix for url, in the format of /v1/projects/${projectId}"""
242+ return "/v1/projects/{0}/namespaces/firebase-server/serverRemoteConfig" .format (
243+ self ._project_id )
244+
245+ @classmethod
246+ def _handle_remote_config_error (cls , error : Any ):
247+ """Handles errors received from the Cloud Functions API."""
248+ return _utils .handle_platform_error_from_requests (error )
219249
220250
221251class _ConditionEvaluator :
@@ -587,7 +617,6 @@ def _compare_versions(self, version1, version2, predicate_fn) -> bool:
587617 logger .warning ("Invalid semantic version format for comparison." )
588618 return False
589619
590-
591620async def get_server_template (app : App = None , default_config : Optional [Dict [str , str ]] = None ):
592621 """Initializes a new ServerTemplate instance and fetches the server template.
593622
0 commit comments