16
16
This module has required APIs for the clients to use Firebase Remote Config with python.
17
17
"""
18
18
19
- import json
19
+ import asyncio
20
20
import logging
21
- from typing import Dict , Optional , Literal , Union
21
+ from typing import Dict , Optional , Literal , Union , Any
22
22
from enum import Enum
23
23
import re
24
24
import hashlib
25
+ import requests
25
26
from firebase_admin import App , _http_client , _utils
26
27
import firebase_admin
27
28
32
33
_REMOTE_CONFIG_ATTRIBUTE = '_remoteconfig'
33
34
MAX_CONDITION_RECURSION_DEPTH = 10
34
35
ValueSource = Literal ['default' , 'remote' , 'static' ] # Define the ValueSource type
36
+
35
37
class PercentConditionOperator (Enum ):
36
38
"""Enum representing the available operators for percent conditions.
37
39
"""
@@ -62,19 +64,40 @@ class CustomSignalOperator(Enum):
62
64
UNKNOWN = "UNKNOWN"
63
65
64
66
class ServerTemplateData :
65
- """Represents a Server Template Data class ."""
67
+ """Parses, validates and encapsulates template data and metadata ."""
66
68
def __init__ (self , etag , template_data ):
67
69
"""Initializes a new ServerTemplateData instance.
68
70
69
71
Args:
70
72
etag: The string to be used for initialize the ETag property.
71
73
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.
72
77
"""
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
78
101
79
102
@property
80
103
def parameters (self ):
@@ -92,13 +115,9 @@ def version(self):
92
115
def conditions (self ):
93
116
return self ._conditions
94
117
95
- @property
96
- def parameter_groups (self ):
97
- return self ._parameter_groups
98
-
99
118
100
119
class 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 ."""
102
121
def __init__ (self , app : App = None , default_config : Optional [Dict [str , str ]] = None ):
103
122
"""Initializes a ServerTemplate instance.
104
123
@@ -112,14 +131,18 @@ def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = N
112
131
# This gets set when the template is
113
132
# fetched from RC servers via the load API, or via the set API.
114
133
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.
115
139
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 ])
119
142
120
143
async def load (self ):
121
144
"""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 ()
123
146
124
147
def evaluate (self , context : Optional [Dict [str , Union [str , int ]]] = None ) -> 'ServerConfig' :
125
148
"""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
140
163
config_values = {}
141
164
# Initializes config Value objects with default values.
142
165
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 ():
144
167
config_values [key ] = _Value ('default' , value )
145
168
self ._evaluator = _ConditionEvaluator (self ._cache .conditions ,
146
169
self ._cache .parameters , context ,
147
170
config_values )
148
171
return ServerConfig (config_values = self ._evaluator .evaluate ())
149
172
150
- def set (self , template ):
173
+ def set (self , template : ServerTemplateData ):
151
174
"""Updates the cache to store the given template is of type ServerTemplateData.
152
175
153
176
Args:
154
177
template: An object of type ServerTemplateData to be cached.
155
178
"""
156
- if isinstance (template , ServerTemplateData ):
157
- self ._cache = template
179
+ self ._cache = template
158
180
159
181
160
182
class ServerConfig :
@@ -202,20 +224,28 @@ def __init__(self, app):
202
224
base_url = remote_config_base_url ,
203
225
headers = rc_headers , timeout = timeout )
204
226
205
-
206
- def get_server_template (self ):
227
+ async def get_server_template (self ):
207
228
"""Requests for a server template and converts the response to an instance of
208
229
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 )
214
239
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 )
219
249
220
250
221
251
class _ConditionEvaluator :
@@ -587,7 +617,6 @@ def _compare_versions(self, version1, version2, predicate_fn) -> bool:
587
617
logger .warning ("Invalid semantic version format for comparison." )
588
618
return False
589
619
590
-
591
620
async def get_server_template (app : App = None , default_config : Optional [Dict [str , str ]] = None ):
592
621
"""Initializes a new ServerTemplate instance and fetches the server template.
593
622
0 commit comments