From 43b6920ed6dd83643bb90308fc998d646ae149e5 Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Wed, 23 Oct 2024 01:23:28 +0530 Subject: [PATCH 01/11] Initial Skeleton for SSRC Implementation --- firebase_admin/_http_client.py | 10 ++++ firebase_admin/remote_config.py | 94 +++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 firebase_admin/remote_config.py diff --git a/firebase_admin/_http_client.py b/firebase_admin/_http_client.py index d259faddf..17e739fd5 100644 --- a/firebase_admin/_http_client.py +++ b/firebase_admin/_http_client.py @@ -148,3 +148,13 @@ def __init__(self, **kwargs): def parse_body(self, resp): return resp.json() + + +class RemoteConfigApiClient(HttpClient): + """An HTTP client that parses response messages as JSON.""" + + def __init__(self, **kwargs): + HttpClient.__init__(self, **kwargs) + + def parse_body(self, resp): + return resp.json() diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py new file mode 100644 index 000000000..766b95bf9 --- /dev/null +++ b/firebase_admin/remote_config.py @@ -0,0 +1,94 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Firebase Remote Config Module. +This module has required APIs for the clients to use Firebase Remote Config with python. +""" + +from typing import Dict, Optional +from firebase_admin import _http_client + +class RemoteConfig: + """Represents a Server Side Remote Config Class. + + The users can use this for initializing and loading a server template. + """ + + def __init__(self, app=None): + timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS) + self._credential = app.credential.get_credential() + self._api_client = _http_client.RemoteConfigApiClient( + credential=self._credential, timeout=timeout) + + async def get_server_template(self, default_config: Optional[Dict[str, str]] = None): + template = self.init_server_template(default_config) + await template.load() + return template + + def init_server_template(self, default_config: Optional[Dict[str, str]] = None): + template = ServerTemplate(self._api_client, default_config=default_config) + # Logic to handle setting template_data here + return template + + +class ServerTemplateData: + """Represents a Server Template Data class. + """ + def __init__(self, template): + self._template = template + + +class ServerTemplate: + """Represents a Server Template with implementations for loading and evaluting the tempalte. + """ + def __init__(self, client, default_config: Optional[Dict[str, str]] = None): + # Private API client used to make network requests + self._client = client + # Field to represent the cached template. This gets set when the template is + # fetched from RC servers via the load API, or via the set API. + self._cache = None + self._stringified_default_config = default_config.values + # Logic to set default_config here + + async def load(self): + self._cache = await self._client.getServerTemplate() + + def evaluate(self, context: Optional[Dict[str, str | int]]): + # Logic to process the cached template into a ServerConfig here + return ServerConfig(context.values) + + def set(self, template): + if isinstance(template, str): + self._cache = ServerTemplateData(template) + elif isinstance(template, ServerTemplateData): + self._cache = template + + +class ServerConfig: + """Represents a Remote Config Server Side Config. + """ + def __init__(self, config_values): + self._config_values = config_values # dictionary of param key to values + + def get_boolean(self, key): + return self._config_values[key] + + def get_string(self, key): + return self._config_values[key] + + def get_int(self, key): + return self._config_values[key] + + def get_value(self, key): + return self._config_values[key] From 7dbed2c526b8c0554b9fce053f0cfbf96062d694 Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Fri, 25 Oct 2024 02:50:55 +0530 Subject: [PATCH 02/11] Adding Implementation for RemoteConfigApiClient and ServerTemplate APIs --- firebase_admin/_http_client.py | 10 ---- firebase_admin/remote_config.py | 94 ++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 17 deletions(-) diff --git a/firebase_admin/_http_client.py b/firebase_admin/_http_client.py index 17e739fd5..d259faddf 100644 --- a/firebase_admin/_http_client.py +++ b/firebase_admin/_http_client.py @@ -148,13 +148,3 @@ def __init__(self, **kwargs): def parse_body(self, resp): return resp.json() - - -class RemoteConfigApiClient(HttpClient): - """An HTTP client that parses response messages as JSON.""" - - def __init__(self, **kwargs): - HttpClient.__init__(self, **kwargs) - - def parse_body(self, resp): - return resp.json() diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index 766b95bf9..f1562da9c 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -18,6 +18,7 @@ from typing import Dict, Optional from firebase_admin import _http_client +import firebase_admin class RemoteConfig: """Represents a Server Side Remote Config Class. @@ -26,10 +27,8 @@ class RemoteConfig: """ def __init__(self, app=None): - timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS) self._credential = app.credential.get_credential() - self._api_client = _http_client.RemoteConfigApiClient( - credential=self._credential, timeout=timeout) + self._api_client = _RemoteConfigApiClient(app=app) async def get_server_template(self, default_config: Optional[Dict[str, str]] = None): template = self.init_server_template(default_config) @@ -37,7 +36,7 @@ async def get_server_template(self, default_config: Optional[Dict[str, str]] = N return template def init_server_template(self, default_config: Optional[Dict[str, str]] = None): - template = ServerTemplate(self._api_client, default_config=default_config) + template = ServerTemplate(client=self._api_client, default_config=default_config) # Logic to handle setting template_data here return template @@ -45,8 +44,56 @@ def init_server_template(self, default_config: Optional[Dict[str, str]] = None): class ServerTemplateData: """Represents a Server Template Data class. """ - def __init__(self, template): - self._template = template + def __init__(self, resp): + self._parameters = ... + # Convert response['parameters'] to {string : Parameter} + self._version = resp.body.version + self._etag = resp.headers.get('ETag') + + @property + def parameters(self): + return self._parameters + + @property + def etag(self): + return self._etag + + @property + def version(self): + return self._version + +class Parameter: + """ Representation of a remote config parameter.""" + + def __init__(self, default_value): + self._default_value = default_value # ParameterValue + + @property + def default_value(self): + return self._default_value + + +class ParameterValue: + """ Base class to represent remote parameter values. A + ParameterValue could be either an ExplicitParameterValue or an + InAppDefaultValue. """ + + +class ExplicitParameterValue(ParameterValue): + def __init__(self, value): + self._value = value + + @property + def value(self): + return self._value + +class InAppDefaultValue(ParameterValue): + def __init__(self, use_in_app_default): + self._use_in_app_default = use_in_app_default + + @property + def use_in_app_default(self): + return self._use_in_app_default class ServerTemplate: @@ -66,7 +113,7 @@ async def load(self): def evaluate(self, context: Optional[Dict[str, str | int]]): # Logic to process the cached template into a ServerConfig here - return ServerConfig(context.values) + return ServerConfig(config_values=context.values) def set(self, template): if isinstance(template, str): @@ -92,3 +139,36 @@ def get_int(self, key): def get_value(self, key): return self._config_values[key] + +class _RemoteConfigApiClient: + """ Internal class that facilitates sending requests to the Firebase Remote + Config backend API. """ + + def __init__(self, app): + # Initialize a JsonHttpClient with basic inputs. Referenced other + # products' code in the Python SDK for what basic inputs to use. + remote_config_base_url = 'https://firebaseremoteconfig.googleapis.com' + self._project_id = app.project_id + app_credential = app.credential.get_credential() + rc_headers = { + 'X-FIREBASE-CLIENT': 'fire-admin-python/{0}'.format(firebase_admin.__version__), } + timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS) + + self._client = _http_client.JsonHttpClient(credential=app_credential, + base_url=remote_config_base_url, + headers=rc_headers, timeout=timeout) + + + def get_server_template(self): + # Requests for server template and converts the response to + # ServerTemplateData + url_prefix = self._get_url_prefix() + response_json = self._client.body('get', + url=url_prefix+'/namespaces/ \ + firebase-server/serverRemoteConfig') + return ServerTemplateData(response_json) + + def _get_url_prefix(self): + # Returns project prefix for url, in the format of + # /v1/projects/${projectId} + return "/v1/projects/{0}".format(self._project_id) From dc1d17b9bebd72fc83b8289e159ac1537972003a Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Mon, 28 Oct 2024 14:48:50 +0530 Subject: [PATCH 03/11] Updating API signature --- firebase_admin/remote_config.py | 58 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index f1562da9c..308e9ddaf 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -17,36 +17,17 @@ """ from typing import Dict, Optional -from firebase_admin import _http_client +from firebase_admin import App, _http_client, _utils import firebase_admin -class RemoteConfig: - """Represents a Server Side Remote Config Class. - - The users can use this for initializing and loading a server template. - """ - - def __init__(self, app=None): - self._credential = app.credential.get_credential() - self._api_client = _RemoteConfigApiClient(app=app) - - async def get_server_template(self, default_config: Optional[Dict[str, str]] = None): - template = self.init_server_template(default_config) - await template.load() - return template - - def init_server_template(self, default_config: Optional[Dict[str, str]] = None): - template = ServerTemplate(client=self._api_client, default_config=default_config) - # Logic to handle setting template_data here - return template - +_REMOTE_CONFIG_ATTRIBUTE = '_remoteconfig' class ServerTemplateData: """Represents a Server Template Data class. """ def __init__(self, resp): - self._parameters = ... - # Convert response['parameters'] to {string : Parameter} + self._parameters = resp.body.parameters + self._conditions = resp.body.conditions self._version = resp.body.version self._etag = resp.headers.get('ETag') @@ -62,6 +43,10 @@ def etag(self): def version(self): return self._version + @property + def conditions(self): + return self._conditions + class Parameter: """ Representation of a remote config parameter.""" @@ -99,20 +84,21 @@ def use_in_app_default(self): class ServerTemplate: """Represents a Server Template with implementations for loading and evaluting the tempalte. """ - def __init__(self, client, default_config: Optional[Dict[str, str]] = None): - # Private API client used to make network requests - self._client = client + def __init__(self, app: App, default_config: Optional[Dict[str, str]] = None): + self._rc_service = _utils.get_app_service(app, _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) + # Field to represent the cached template. This gets set when the template is # fetched from RC servers via the load API, or via the set API. self._cache = None - self._stringified_default_config = default_config.values - # Logic to set default_config here + for key in default_config: + self._stringified_default_config[key] = default_config[key] async def load(self): - self._cache = await self._client.getServerTemplate() + self._cache = await self._rc_service.getServerTemplate() def evaluate(self, context: Optional[Dict[str, str | int]]): # Logic to process the cached template into a ServerConfig here + # TODO: add Condition evaluator return ServerConfig(config_values=context.values) def set(self, template): @@ -140,7 +126,7 @@ def get_int(self, key): def get_value(self, key): return self._config_values[key] -class _RemoteConfigApiClient: +class _RemoteConfigService: """ Internal class that facilitates sending requests to the Firebase Remote Config backend API. """ @@ -172,3 +158,15 @@ def _get_url_prefix(self): # Returns project prefix for url, in the format of # /v1/projects/${projectId} return "/v1/projects/{0}".format(self._project_id) + + +async def get_server_template(app: App, default_config: Optional[Dict[str, str]] = None): + template = init_server_template(app, default_config) + await template.load() + return template + +def init_server_template(app: App, default_config: Optional[Dict[str, str]] = None, + template_data: Optional[ServerTemplateData] = None): + template = ServerTemplate(app, default_config=default_config) + template.set(template_data) + return template From 7dc118e8b26590c4b09bba7da1ea76e9633353a0 Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Tue, 29 Oct 2024 02:38:36 +0530 Subject: [PATCH 04/11] Minor update to API signature --- firebase_admin/remote_config.py | 47 +++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index 308e9ddaf..a777c574f 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -26,13 +26,16 @@ class ServerTemplateData: """Represents a Server Template Data class. """ def __init__(self, resp): + self._parameters = resp.body.parameters self._conditions = resp.body.conditions self._version = resp.body.version - self._etag = resp.headers.get('ETag') + self._parameterGroups = resp.body.parameterGroups + self._etag = resp.headers.get('etag') @property def parameters(self): + # TODO: convert to Parameters return self._parameters @property @@ -46,6 +49,10 @@ def version(self): @property def conditions(self): return self._conditions + + @property + def conditions(self): + return self._parameterGroups class Parameter: """ Representation of a remote config parameter.""" @@ -84,14 +91,17 @@ def use_in_app_default(self): class ServerTemplate: """Represents a Server Template with implementations for loading and evaluting the tempalte. """ - def __init__(self, app: App, default_config: Optional[Dict[str, str]] = None): + def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = None): self._rc_service = _utils.get_app_service(app, _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) # Field to represent the cached template. This gets set when the template is # fetched from RC servers via the load API, or via the set API. self._cache = None - for key in default_config: - self._stringified_default_config[key] = default_config[key] + if default_config is not None: + for key in default_config: + self._stringified_default_config[key] = default_config[key] + else: + self._stringified_default_config[key] = None async def load(self): self._cache = await self._rc_service.getServerTemplate() @@ -99,7 +109,8 @@ async def load(self): def evaluate(self, context: Optional[Dict[str, str | int]]): # Logic to process the cached template into a ServerConfig here # TODO: add Condition evaluator - return ServerConfig(config_values=context.values) + self._evaluator = ConditionEvaluator(self._cache.conditions, context) + return ServerConfig(config_values=self._evaluator.evaluate()) def set(self, template): if isinstance(template, str): @@ -115,13 +126,13 @@ def __init__(self, config_values): self._config_values = config_values # dictionary of param key to values def get_boolean(self, key): - return self._config_values[key] + return bool(self.get_value(key)) def get_string(self, key): - return self._config_values[key] + return str(self.get_value(key)) def get_int(self, key): - return self._config_values[key] + return int(self.get_value(key)) def get_value(self, key): return self._config_values[key] @@ -160,13 +171,27 @@ def _get_url_prefix(self): return "/v1/projects/{0}".format(self._project_id) -async def get_server_template(app: App, default_config: Optional[Dict[str, str]] = None): +class _ConditionEvaluator: + """ Internal class that facilitates sending requests to the Firebase Remote + Config backend API. """ + + def __init__(self, context, conditions): + self._context = context + self._conditions = conditions + + def evaluate(self): + # TODO: Write evaluator + return {} + + +async def get_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None): template = init_server_template(app, default_config) await template.load() return template -def init_server_template(app: App, default_config: Optional[Dict[str, str]] = None, +def init_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None, template_data: Optional[ServerTemplateData] = None): template = ServerTemplate(app, default_config=default_config) - template.set(template_data) + if template_data is not None: + template.set(template_data) return template From ffe47d401f3a68c504da0dd1b7cdbb57dfc7643c Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Wed, 30 Oct 2024 04:32:16 +0530 Subject: [PATCH 05/11] Adding comments and unit tests --- firebase_admin/remote_config.py | 158 ++++++++++++++++---------------- tests/test_functions.py | 2 +- tests/test_remote_config.py | 60 ++++++++++++ 3 files changed, 140 insertions(+), 80 deletions(-) create mode 100644 tests/test_remote_config.py diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index a777c574f..63f1c7c9d 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -16,6 +16,7 @@ This module has required APIs for the clients to use Firebase Remote Config with python. """ +import json from typing import Dict, Optional from firebase_admin import App, _http_client, _utils import firebase_admin @@ -23,19 +24,16 @@ _REMOTE_CONFIG_ATTRIBUTE = '_remoteconfig' class ServerTemplateData: - """Represents a Server Template Data class. - """ - def __init__(self, resp): - - self._parameters = resp.body.parameters - self._conditions = resp.body.conditions - self._version = resp.body.version - self._parameterGroups = resp.body.parameterGroups - self._etag = resp.headers.get('etag') + """Represents a Server Template Data class.""" + def __init__(self, headers, response_json): + self._parameters = response_json['parameters'] + self._conditions = response_json['conditions'] + self._version = response_json['version'] + self._parameter_groups = response_json['parameterGroups'] + self._etag = headers.get('ETag') @property def parameters(self): - # TODO: convert to Parameters return self._parameters @property @@ -49,79 +47,55 @@ def version(self): @property def conditions(self): return self._conditions - - @property - def conditions(self): - return self._parameterGroups - -class Parameter: - """ Representation of a remote config parameter.""" - - def __init__(self, default_value): - self._default_value = default_value # ParameterValue - - @property - def default_value(self): - return self._default_value - - -class ParameterValue: - """ Base class to represent remote parameter values. A - ParameterValue could be either an ExplicitParameterValue or an - InAppDefaultValue. """ - - -class ExplicitParameterValue(ParameterValue): - def __init__(self, value): - self._value = value @property - def value(self): - return self._value - -class InAppDefaultValue(ParameterValue): - def __init__(self, use_in_app_default): - self._use_in_app_default = use_in_app_default - - @property - def use_in_app_default(self): - return self._use_in_app_default + def parameter_groups(self): + return self._parameter_groups class ServerTemplate: - """Represents a Server Template with implementations for loading and evaluting the tempalte. - """ + """Represents a Server Template with implementations for loading and evaluting the tempalte.""" def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = None): - self._rc_service = _utils.get_app_service(app, _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) + """Initializes a ServerTemplate instance. - # Field to represent the cached template. This gets set when the template is + Args: + app: App instance to be used. This is optional and the default app instance will + be used if not present. + default_config: The default config to be used in the evaluated config. + """ + self._rc_service = _utils.get_app_service(app, + _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) + + # This gets set when the template is # fetched from RC servers via the load API, or via the set API. self._cache = None if default_config is not None: - for key in default_config: - self._stringified_default_config[key] = default_config[key] + self._stringified_default_config = json.dumps(default_config) else: - self._stringified_default_config[key] = None + self._stringified_default_config = None async def load(self): + """Fetches the server template and caches the data.""" self._cache = await self._rc_service.getServerTemplate() - def evaluate(self, context: Optional[Dict[str, str | int]]): - # Logic to process the cached template into a ServerConfig here - # TODO: add Condition evaluator - self._evaluator = ConditionEvaluator(self._cache.conditions, context) + def evaluate(self, context): + # Logic to process the cached template into a ServerConfig here. + # TODO: Add Condition evaluator. + self._evaluator = _ConditionEvaluator(self._cache.conditions, context) return ServerConfig(config_values=self._evaluator.evaluate()) def set(self, template): - if isinstance(template, str): - self._cache = ServerTemplateData(template) - elif isinstance(template, ServerTemplateData): + """Updates the cache to store the given template is of type ServerTemplateData. + + Args: + template: An object of type ServerTemplateData to be cached. + """ + if isinstance(template, ServerTemplateData): self._cache = template class ServerConfig: - """Represents a Remote Config Server Side Config. - """ + """Represents a Remote Config Server Side Config.""" def __init__(self, config_values): self._config_values = config_values # dictionary of param key to values @@ -137,13 +111,18 @@ def get_int(self, key): def get_value(self, key): return self._config_values[key] -class _RemoteConfigService: - """ Internal class that facilitates sending requests to the Firebase Remote - Config backend API. """ +class _RemoteConfigService: + """Internal class that facilitates sending requests to the Firebase Remote + Config backend API. + """ def __init__(self, app): - # Initialize a JsonHttpClient with basic inputs. Referenced other - # products' code in the Python SDK for what basic inputs to use. + """Initialize a JsonHttpClient with necessary inputs. + + Args: + app: App instance to be used for fetching app specific details required + for initializing the http client. + """ remote_config_base_url = 'https://firebaseremoteconfig.googleapis.com' self._project_id = app.project_id app_credential = app.credential.get_credential() @@ -157,24 +136,23 @@ def __init__(self, app): def get_server_template(self): - # Requests for server template and converts the response to - # ServerTemplateData + """Requests for a server template and converts the response to an instance of + ServerTemplateData for storing the template parameters and conditions.""" url_prefix = self._get_url_prefix() - response_json = self._client.body('get', - url=url_prefix+'/namespaces/ \ - firebase-server/serverRemoteConfig') - return ServerTemplateData(response_json) + headers, response_json = self._client.headers_and_body('get', + url=url_prefix+'/namespaces/ \ + firebase-server/serverRemoteConfig') + return ServerTemplateData(headers, response_json) def _get_url_prefix(self): # Returns project prefix for url, in the format of # /v1/projects/${projectId} return "/v1/projects/{0}".format(self._project_id) - -class _ConditionEvaluator: - """ Internal class that facilitates sending requests to the Firebase Remote - Config backend API. """ +class _ConditionEvaluator: + """Internal class that facilitates sending requests to the Firebase Remote + Config backend API.""" def __init__(self, context, conditions): self._context = context self._conditions = conditions @@ -185,13 +163,35 @@ def evaluate(self): async def get_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None): - template = init_server_template(app, default_config) + """Initializes a new ServerTemplate instance and fetches the server template. + + Args: + app: App instance to be used. This is optional and the default app instance will + be used if not present. + default_config: The default config to be used in the evaluated config. + + Returns: + ServerTemplate: An object having the cached server template to be used for evaluation. + """ + template = init_server_template(app=app, default_config=default_config) await template.load() return template -def init_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None, +def init_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None, template_data: Optional[ServerTemplateData] = None): - template = ServerTemplate(app, default_config=default_config) + """Initializes a new ServerTemplate instance. + + Args: + app: App instance to be used. This is optional and the default app instance will + be used if not present. + default_config: The default config to be used in the evaluated config. + template_data: An optional template data to be set on initialization. + + Returns: + ServerTemplate: A new ServerTemplate instance initialized with an optional + template and config. + """ + template = ServerTemplate(app=app, default_config=default_config) if template_data is not None: template.set(template_data) return template diff --git a/tests/test_functions.py b/tests/test_functions.py index 75809c1ad..75603d17a 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -209,7 +209,7 @@ def test_task_options(self, task_opts_params): schedule_time = datetime.fromisoformat(task['schedule_time'][:-1]) delta = abs(schedule_time - _SCHEDULE_TIME) - assert delta <= timedelta(seconds=15) + assert delta <= timedelta(seconds=30) assert task['dispatch_deadline'] == '200s' assert task['http_request']['headers']['x-test-header'] == 'test-header-value' diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py new file mode 100644 index 000000000..db35f3528 --- /dev/null +++ b/tests/test_remote_config.py @@ -0,0 +1,60 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for firebase_admin.remote_config.""" +import json +import firebase_admin +from firebase_admin.remote_config import _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService + +from firebase_admin import _utils +from tests import testutils + +class MockAdapter(testutils.MockAdapter): + """A Mock HTTP Adapter that Firebase Remote Config with ETag in header.""" + + ETAG = '0' + + def __init__(self, data, status, recorder, etag=ETAG): + testutils.MockAdapter.__init__(self, data, status, recorder) + self._etag = etag + + def send(self, request, **kwargs): + resp = super(MockAdapter, self).send(request, **kwargs) + resp.headers = {'ETag': self._etag} + return resp + + +class TestGetServerTemplate: + _DEFAULT_APP = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') + _RC_INSTANCE = _utils.get_app_service(_DEFAULT_APP, + _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) + _DEFAULT_RESPONSE = json.dumps({ + 'parameters': { + 'test_key': 'test_value' + }, + 'conditions': {}, + 'parameterGroups': {}, + 'version': 'test' + }) + + def test_rc_instance_get_server_template(self): + recorder = [] + self._RC_INSTANCE._client.session.mount( + 'https://firebaseremoteconfig.googleapis.com', + MockAdapter(self._DEFAULT_RESPONSE, 200, recorder)) + + template = self._RC_INSTANCE.get_server_template() + + assert template.parameters == dict(test_key="test_value") + assert str(template.version) == 'test' From b6acf10ab500cbe2be7a97e0f1cdda4374d56ea5 Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Wed, 30 Oct 2024 04:48:32 +0530 Subject: [PATCH 06/11] Updating init params for ServerTemplateData --- firebase_admin/remote_config.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index 63f1c7c9d..edd0bee70 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -25,12 +25,18 @@ class ServerTemplateData: """Represents a Server Template Data class.""" - def __init__(self, headers, response_json): - self._parameters = response_json['parameters'] - self._conditions = response_json['conditions'] - self._version = response_json['version'] - self._parameter_groups = response_json['parameterGroups'] - self._etag = headers.get('ETag') + def __init__(self, etag, template_data): + """Initializes a new ServerTemplateData instance. + + Args: + etag: The string to be used for initialize the ETag property. + template_data: The data to be parsed for getting the parameters and conditions. + """ + self._parameters = template_data['parameters'] + self._conditions = template_data['conditions'] + self._version = template_data['version'] + self._parameter_groups = template_data['parameterGroups'] + self._etag = etag @property def parameters(self): @@ -142,7 +148,7 @@ def get_server_template(self): headers, response_json = self._client.headers_and_body('get', url=url_prefix+'/namespaces/ \ firebase-server/serverRemoteConfig') - return ServerTemplateData(headers, response_json) + return ServerTemplateData(headers.get('ETag'), response_json) def _get_url_prefix(self): # Returns project prefix for url, in the format of From f5db7dba886835cb5dacf09ec0f28f23a7b4f3a5 Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Thu, 7 Nov 2024 04:33:32 +0530 Subject: [PATCH 07/11] Adding validation errors and test --- firebase_admin/remote_config.py | 67 +++++++++++++++++++++++++-------- tests/test_remote_config.py | 39 ++++++++++++++----- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index edd0bee70..af94607d8 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -17,7 +17,8 @@ """ import json -from typing import Dict, Optional +from typing import Any, Dict, Optional +import requests from firebase_admin import App, _http_client, _utils import firebase_admin @@ -31,12 +32,41 @@ def __init__(self, etag, template_data): Args: etag: The string to be used for initialize the ETag property. template_data: The data to be parsed for getting the parameters and conditions. + + Raises: + ValueError: If the template data is not valid. """ - self._parameters = template_data['parameters'] - self._conditions = template_data['conditions'] - self._version = template_data['version'] - self._parameter_groups = template_data['parameterGroups'] - self._etag = etag + if 'parameters' in template_data: + if template_data['parameters'] is not None: + self._parameters = template_data['parameters'] + else: + raise ValueError('Remote Config parameters must be a non-null object') + else: + self._parameters = {} + + if 'conditions' in template_data: + if template_data['conditions'] is not None: + self._conditions = template_data['conditions'] + else: + raise ValueError('Remote Config conditions must be a non-null object') + else: + self._conditions = [] + + self._version = '' + if 'version' in template_data: + self._version = template_data['version'] + + if 'parameterGroups' in template_data: + if template_data['parameterGroups'] is not None: + self._parameter_groups = template_data['parameterGroups'] + else: + raise ValueError('Remote Config parameterGroups must be a non-null object') + else: + self.parameter_groups = {} + + self._etag = '' + if etag is not None and isinstance(etag, str): + self._etag = etag @property def parameters(self): @@ -90,14 +120,13 @@ def evaluate(self, context): self._evaluator = _ConditionEvaluator(self._cache.conditions, context) return ServerConfig(config_values=self._evaluator.evaluate()) - def set(self, template): + def set(self, template: ServerTemplateData): """Updates the cache to store the given template is of type ServerTemplateData. Args: template: An object of type ServerTemplateData to be cached. """ - if isinstance(template, ServerTemplateData): - self._cache = template + self._cache = template class ServerConfig: @@ -140,21 +169,27 @@ def __init__(self, app): base_url=remote_config_base_url, headers=rc_headers, timeout=timeout) - def get_server_template(self): """Requests for a server template and converts the response to an instance of ServerTemplateData for storing the template parameters and conditions.""" url_prefix = self._get_url_prefix() - headers, response_json = self._client.headers_and_body('get', - url=url_prefix+'/namespaces/ \ - firebase-server/serverRemoteConfig') - return ServerTemplateData(headers.get('ETag'), response_json) + try: + headers, response_json = self._client.headers_and_body( + 'get', url=url_prefix+'/namespaces/firebase-server/serverRemoteConfig') + except requests.exceptions.RequestException as error: + raise self._handle_remote_config_error(error) + else: + return ServerTemplateData(headers.get('etag'), response_json) def _get_url_prefix(self): - # Returns project prefix for url, in the format of - # /v1/projects/${projectId} + """Returns project prefix for url, in the format of /v1/projects/${projectId}""" return "/v1/projects/{0}".format(self._project_id) + @classmethod + def _handle_remote_config_error(cls, error: Any): + """Handles errors received from the Cloud Functions API.""" + return _utils.handle_platform_error_from_requests(error) + class _ConditionEvaluator: """Internal class that facilitates sending requests to the Firebase Remote diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py index db35f3528..20bac3b6c 100644 --- a/tests/test_remote_config.py +++ b/tests/test_remote_config.py @@ -31,7 +31,7 @@ def __init__(self, data, status, recorder, etag=ETAG): def send(self, request, **kwargs): resp = super(MockAdapter, self).send(request, **kwargs) - resp.headers = {'ETag': self._etag} + resp.headers = {'etag': self._etag} return resp @@ -39,22 +39,41 @@ class TestGetServerTemplate: _DEFAULT_APP = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') _RC_INSTANCE = _utils.get_app_service(_DEFAULT_APP, _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) - _DEFAULT_RESPONSE = json.dumps({ - 'parameters': { - 'test_key': 'test_value' - }, - 'conditions': {}, - 'parameterGroups': {}, - 'version': 'test' - }) def test_rc_instance_get_server_template(self): recorder = [] + response = json.dumps({ + 'parameters': { + 'test_key': 'test_value' + }, + 'conditions': [], + 'parameterGroups': {}, + 'version': 'test' + }) self._RC_INSTANCE._client.session.mount( 'https://firebaseremoteconfig.googleapis.com', - MockAdapter(self._DEFAULT_RESPONSE, 200, recorder)) + MockAdapter(response, 200, recorder)) template = self._RC_INSTANCE.get_server_template() assert template.parameters == dict(test_key="test_value") assert str(template.version) == 'test' + assert str(template.etag) == '0' + + def test_rc_instance_get_server_template_empty_params(self): + recorder = [] + response = json.dumps({ + 'conditions': [], + 'parameterGroups': {}, + 'version': 'test' + }) + + self._RC_INSTANCE._client.session.mount( + 'https://firebaseremoteconfig.googleapis.com', + MockAdapter(response, 200, recorder)) + + template = self._RC_INSTANCE.get_server_template() + + assert template.parameters == {} + assert str(template.version) == 'test' + assert str(template.etag) == '0' From 20749d73d36a3097c88c0858fe1130420a8b9f29 Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Thu, 7 Nov 2024 16:54:12 +0530 Subject: [PATCH 08/11] Adding unit tests for init_server_template and get_server_template --- firebase_admin/remote_config.py | 17 +++--- tests/test_remote_config.py | 92 ++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index af94607d8..ea2e89aed 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -112,12 +112,12 @@ def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = N async def load(self): """Fetches the server template and caches the data.""" - self._cache = await self._rc_service.getServerTemplate() + self._cache = self._rc_service.get_server_template() - def evaluate(self, context): + def evaluate(self): # Logic to process the cached template into a ServerConfig here. - # TODO: Add Condition evaluator. - self._evaluator = _ConditionEvaluator(self._cache.conditions, context) + # TODO: Add and validate Condition evaluator. + self._evaluator = _ConditionEvaluator(self._cache.parameters) return ServerConfig(config_values=self._evaluator.evaluate()) def set(self, template: ServerTemplateData): @@ -194,13 +194,12 @@ def _handle_remote_config_error(cls, error: Any): class _ConditionEvaluator: """Internal class that facilitates sending requests to the Firebase Remote Config backend API.""" - def __init__(self, context, conditions): - self._context = context - self._conditions = conditions + def __init__(self, parameters): + self._parameters = parameters def evaluate(self): - # TODO: Write evaluator - return {} + # TODO: Write logic for evaluator + return self._parameters async def get_server_template(app: App = None, default_config: Optional[Dict[str, str]] = None): diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py index 20bac3b6c..d412396c1 100644 --- a/tests/test_remote_config.py +++ b/tests/test_remote_config.py @@ -14,8 +14,11 @@ """Tests for firebase_admin.remote_config.""" import json +import pytest import firebase_admin -from firebase_admin.remote_config import _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService +from firebase_admin import remote_config +from firebase_admin.remote_config import _REMOTE_CONFIG_ATTRIBUTE +from firebase_admin.remote_config import _RemoteConfigService, ServerTemplateData from firebase_admin import _utils from tests import testutils @@ -23,7 +26,7 @@ class MockAdapter(testutils.MockAdapter): """A Mock HTTP Adapter that Firebase Remote Config with ETag in header.""" - ETAG = '0' + ETAG = 'etag' def __init__(self, data, status, recorder, etag=ETAG): testutils.MockAdapter.__init__(self, data, status, recorder) @@ -35,10 +38,15 @@ def send(self, request, **kwargs): return resp -class TestGetServerTemplate: - _DEFAULT_APP = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') - _RC_INSTANCE = _utils.get_app_service(_DEFAULT_APP, - _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) +class TestRemoteConfigServiceClient: + @classmethod + def setup_class(cls): + cred = testutils.MockCredential() + firebase_admin.initialize_app(cred, {'projectId': 'project-id'}) + + @classmethod + def teardown_class(cls): + testutils.cleanup_apps() def test_rc_instance_get_server_template(self): recorder = [] @@ -50,15 +58,18 @@ def test_rc_instance_get_server_template(self): 'parameterGroups': {}, 'version': 'test' }) - self._RC_INSTANCE._client.session.mount( + + rc_instance = _utils.get_app_service(firebase_admin.get_app(), + _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) + rc_instance._client.session.mount( 'https://firebaseremoteconfig.googleapis.com', MockAdapter(response, 200, recorder)) - template = self._RC_INSTANCE.get_server_template() + template = rc_instance.get_server_template() assert template.parameters == dict(test_key="test_value") assert str(template.version) == 'test' - assert str(template.etag) == '0' + assert str(template.etag) == 'etag' def test_rc_instance_get_server_template_empty_params(self): recorder = [] @@ -68,12 +79,69 @@ def test_rc_instance_get_server_template_empty_params(self): 'version': 'test' }) - self._RC_INSTANCE._client.session.mount( + rc_instance = _utils.get_app_service(firebase_admin.get_app(), + _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) + rc_instance._client.session.mount( 'https://firebaseremoteconfig.googleapis.com', MockAdapter(response, 200, recorder)) - template = self._RC_INSTANCE.get_server_template() + template = rc_instance.get_server_template() assert template.parameters == {} assert str(template.version) == 'test' - assert str(template.etag) == '0' + assert str(template.etag) == 'etag' + + +class TestRemoteConfigService: + @classmethod + def setup_class(cls): + cred = testutils.MockCredential() + firebase_admin.initialize_app(cred, {'projectId': 'project-id'}) + + @classmethod + def teardown_class(cls): + testutils.cleanup_apps() + + def test_init_server_template(self): + app = firebase_admin.get_app() + template_data = { + 'conditions': [], + 'parameters': { + 'test_key': 'test_value' + }, + 'parameterGroups': '', + 'version': '', + } + + template = remote_config.init_server_template( + app=app, + template_data=ServerTemplateData('etag', template_data) # Use ServerTemplateData here + ) + + config = template.evaluate() + assert config.get_string('test_key') == 'test_value' + + @pytest.mark.asyncio + async def test_get_server_template(self): + app = firebase_admin.get_app() + rc_instance = _utils.get_app_service(app, + _REMOTE_CONFIG_ATTRIBUTE, _RemoteConfigService) + + recorder = [] + response = json.dumps({ + 'parameters': { + 'test_key': 'test_value' + }, + 'conditions': [], + 'parameterGroups': {}, + 'version': 'test' + }) + + rc_instance._client.session.mount( + 'https://firebaseremoteconfig.googleapis.com', + MockAdapter(response, 200, recorder)) + + template = await remote_config.get_server_template(app=app) + + config = template.evaluate() + assert config.get_string('test_key') == 'test_value' From 681d353cafd51e16a7187d74df92a3e81d21af55 Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Tue, 12 Nov 2024 23:47:09 +0530 Subject: [PATCH 09/11] Removing parameter groups --- firebase_admin/remote_config.py | 12 ------------ tests/test_remote_config.py | 6 +----- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index ea2e89aed..f1da0f683 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -56,14 +56,6 @@ def __init__(self, etag, template_data): if 'version' in template_data: self._version = template_data['version'] - if 'parameterGroups' in template_data: - if template_data['parameterGroups'] is not None: - self._parameter_groups = template_data['parameterGroups'] - else: - raise ValueError('Remote Config parameterGroups must be a non-null object') - else: - self.parameter_groups = {} - self._etag = '' if etag is not None and isinstance(etag, str): self._etag = etag @@ -84,10 +76,6 @@ def version(self): def conditions(self): return self._conditions - @property - def parameter_groups(self): - return self._parameter_groups - class ServerTemplate: """Represents a Server Template with implementations for loading and evaluting the tempalte.""" diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py index d412396c1..202638f21 100644 --- a/tests/test_remote_config.py +++ b/tests/test_remote_config.py @@ -55,7 +55,6 @@ def test_rc_instance_get_server_template(self): 'test_key': 'test_value' }, 'conditions': [], - 'parameterGroups': {}, 'version': 'test' }) @@ -75,7 +74,6 @@ def test_rc_instance_get_server_template_empty_params(self): recorder = [] response = json.dumps({ 'conditions': [], - 'parameterGroups': {}, 'version': 'test' }) @@ -109,13 +107,12 @@ def test_init_server_template(self): 'parameters': { 'test_key': 'test_value' }, - 'parameterGroups': '', 'version': '', } template = remote_config.init_server_template( app=app, - template_data=ServerTemplateData('etag', template_data) # Use ServerTemplateData here + template_data=ServerTemplateData('etag', template_data) ) config = template.evaluate() @@ -133,7 +130,6 @@ async def test_get_server_template(self): 'test_key': 'test_value' }, 'conditions': [], - 'parameterGroups': {}, 'version': 'test' }) From cde8b09f67bad2197596ae85ff05c5f0867baa99 Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Fri, 15 Nov 2024 21:34:28 +0530 Subject: [PATCH 10/11] Addressing PR comments and fixing async flow during fetch call --- firebase_admin/remote_config.py | 33 ++++++++++++++++++++------------- tests/test_functions.py | 2 +- tests/test_remote_config.py | 17 +++++++++++------ 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index f1da0f683..461198f38 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -16,6 +16,7 @@ This module has required APIs for the clients to use Firebase Remote Config with python. """ +import asyncio import json from typing import Any, Dict, Optional import requests @@ -25,7 +26,7 @@ _REMOTE_CONFIG_ATTRIBUTE = '_remoteconfig' class ServerTemplateData: - """Represents a Server Template Data class.""" + """Parses, validates and encapsulates template data and metadata.""" def __init__(self, etag, template_data): """Initializes a new ServerTemplateData instance. @@ -78,7 +79,7 @@ def conditions(self): class ServerTemplate: - """Represents a Server Template with implementations for loading and evaluting the tempalte.""" + """Represents a Server Template with implementations for loading and evaluting the template.""" def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = None): """Initializes a ServerTemplate instance. @@ -93,14 +94,18 @@ def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = N # This gets set when the template is # fetched from RC servers via the load API, or via the set API. self._cache = None + self._stringified_default_config: Dict[str,str] = {} + + # RC stores all remote values as string, but it's more intuitive + # to declare default values with specific types, so this converts + # the external declaration to an internal string representation. if default_config is not None: - self._stringified_default_config = json.dumps(default_config) - else: - self._stringified_default_config = None + for key in default_config: + self._stringified_default_config[key] = str(default_config[key]) async def load(self): """Fetches the server template and caches the data.""" - self._cache = self._rc_service.get_server_template() + self._cache = await self._rc_service.get_server_template() def evaluate(self): # Logic to process the cached template into a ServerConfig here. @@ -157,21 +162,23 @@ def __init__(self, app): base_url=remote_config_base_url, headers=rc_headers, timeout=timeout) - def get_server_template(self): + async def get_server_template(self): """Requests for a server template and converts the response to an instance of ServerTemplateData for storing the template parameters and conditions.""" - url_prefix = self._get_url_prefix() try: - headers, response_json = self._client.headers_and_body( - 'get', url=url_prefix+'/namespaces/firebase-server/serverRemoteConfig') + loop = asyncio.get_event_loop() + headers, template_data = await loop.run_in_executor(None, + self._client.headers_and_body, + 'get', self._get_url()) except requests.exceptions.RequestException as error: raise self._handle_remote_config_error(error) else: - return ServerTemplateData(headers.get('etag'), response_json) + return ServerTemplateData(headers.get('etag'), template_data) - def _get_url_prefix(self): + def _get_url(self): """Returns project prefix for url, in the format of /v1/projects/${projectId}""" - return "/v1/projects/{0}".format(self._project_id) + return "/v1/projects/{0}/namespaces/firebase-server/serverRemoteConfig".format( + self._project_id) @classmethod def _handle_remote_config_error(cls, error: Any): diff --git a/tests/test_functions.py b/tests/test_functions.py index 75603d17a..75809c1ad 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -209,7 +209,7 @@ def test_task_options(self, task_opts_params): schedule_time = datetime.fromisoformat(task['schedule_time'][:-1]) delta = abs(schedule_time - _SCHEDULE_TIME) - assert delta <= timedelta(seconds=30) + assert delta <= timedelta(seconds=15) assert task['dispatch_deadline'] == '200s' assert task['http_request']['headers']['x-test-header'] == 'test-header-value' diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py index 202638f21..6b0b171cd 100644 --- a/tests/test_remote_config.py +++ b/tests/test_remote_config.py @@ -38,7 +38,8 @@ def send(self, request, **kwargs): return resp -class TestRemoteConfigServiceClient: +class TestRemoteConfigService: + """Tests methods on _RemoteConfigService""" @classmethod def setup_class(cls): cred = testutils.MockCredential() @@ -48,7 +49,8 @@ def setup_class(cls): def teardown_class(cls): testutils.cleanup_apps() - def test_rc_instance_get_server_template(self): + @pytest.mark.asyncio + async def test_rc_instance_get_server_template(self): recorder = [] response = json.dumps({ 'parameters': { @@ -64,13 +66,14 @@ def test_rc_instance_get_server_template(self): 'https://firebaseremoteconfig.googleapis.com', MockAdapter(response, 200, recorder)) - template = rc_instance.get_server_template() + template = await rc_instance.get_server_template() assert template.parameters == dict(test_key="test_value") assert str(template.version) == 'test' assert str(template.etag) == 'etag' - def test_rc_instance_get_server_template_empty_params(self): + @pytest.mark.asyncio + async def test_rc_instance_get_server_template_empty_params(self): recorder = [] response = json.dumps({ 'conditions': [], @@ -83,14 +86,15 @@ def test_rc_instance_get_server_template_empty_params(self): 'https://firebaseremoteconfig.googleapis.com', MockAdapter(response, 200, recorder)) - template = rc_instance.get_server_template() + template = await rc_instance.get_server_template() assert template.parameters == {} assert str(template.version) == 'test' assert str(template.etag) == 'etag' -class TestRemoteConfigService: +class TestRemoteConfigModule: + """Tests methods on firebase_admin.remote_config""" @classmethod def setup_class(cls): cred = testutils.MockCredential() @@ -112,6 +116,7 @@ def test_init_server_template(self): template = remote_config.init_server_template( app=app, + default_config={'default_test': 'default_value'}, template_data=ServerTemplateData('etag', template_data) ) From b4b0aabd9600359bf1e8eb3d87e6389423b19c2b Mon Sep 17 00:00:00 2001 From: Pijush Chakraborty Date: Fri, 15 Nov 2024 21:45:57 +0530 Subject: [PATCH 11/11] Fixing lint issues --- firebase_admin/remote_config.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index 461198f38..cd7d17f6f 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -17,7 +17,6 @@ """ import asyncio -import json from typing import Any, Dict, Optional import requests from firebase_admin import App, _http_client, _utils @@ -94,7 +93,7 @@ def __init__(self, app: App = None, default_config: Optional[Dict[str, str]] = N # This gets set when the template is # fetched from RC servers via the load API, or via the set API. self._cache = None - self._stringified_default_config: Dict[str,str] = {} + self._stringified_default_config: Dict[str, str] = {} # RC stores all remote values as string, but it's more intuitive # to declare default values with specific types, so this converts @@ -167,8 +166,8 @@ async def get_server_template(self): ServerTemplateData for storing the template parameters and conditions.""" try: loop = asyncio.get_event_loop() - headers, template_data = await loop.run_in_executor(None, - self._client.headers_and_body, + headers, template_data = await loop.run_in_executor(None, + self._client.headers_and_body, 'get', self._get_url()) except requests.exceptions.RequestException as error: raise self._handle_remote_config_error(error)