From b57af183fa89013417653790b7693c2628a03723 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:16:19 -0400 Subject: [PATCH 1/6] change(fcm): Remove deprecated FCM APIs (#890) --- firebase_admin/_gapic_utils.py | 122 ------- firebase_admin/messaging.py | 118 ------- integration/test_messaging.py | 65 ---- requirements.txt | 1 - setup.py | 1 - snippets/messaging/cloud_messaging.py | 24 +- tests/test_exceptions.py | 161 --------- tests/test_messaging.py | 486 +------------------------- 8 files changed, 16 insertions(+), 962 deletions(-) delete mode 100644 firebase_admin/_gapic_utils.py diff --git a/firebase_admin/_gapic_utils.py b/firebase_admin/_gapic_utils.py deleted file mode 100644 index 3c975808c..000000000 --- a/firebase_admin/_gapic_utils.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2021 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. - -"""Internal utilities for interacting with Google API client.""" - -import io -import socket - -import googleapiclient -import httplib2 -import requests - -from firebase_admin import exceptions -from firebase_admin import _utils - - -def handle_platform_error_from_googleapiclient(error, handle_func=None): - """Constructs a ``FirebaseError`` from the given googleapiclient error. - - This can be used to handle errors returned by Google Cloud Platform (GCP) APIs. - - Args: - error: An error raised by the googleapiclient while making an HTTP call to a GCP API. - handle_func: A function that can be used to handle platform errors in a custom way. When - specified, this function will be called with three arguments. It has the same - signature as ```_handle_func_googleapiclient``, but may return ``None``. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code. - """ - if not isinstance(error, googleapiclient.errors.HttpError): - return handle_googleapiclient_error(error) - - content = error.content.decode() - status_code = error.resp.status - error_dict, message = _utils._parse_platform_error(content, status_code) # pylint: disable=protected-access - http_response = _http_response_from_googleapiclient_error(error) - exc = None - if handle_func: - exc = handle_func(error, message, error_dict, http_response) - - return exc if exc else _handle_func_googleapiclient(error, message, error_dict, http_response) - - -def _handle_func_googleapiclient(error, message, error_dict, http_response): - """Constructs a ``FirebaseError`` from the given GCP error. - - Args: - error: An error raised by the googleapiclient module while making an HTTP call. - message: A message to be included in the resulting ``FirebaseError``. - error_dict: Parsed GCP error response. - http_response: A requests HTTP response object to associate with the exception. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code or None. - """ - code = error_dict.get('status') - return handle_googleapiclient_error(error, message, code, http_response) - - -def handle_googleapiclient_error(error, message=None, code=None, http_response=None): - """Constructs a ``FirebaseError`` from the given googleapiclient error. - - This method is agnostic of the remote service that produced the error, whether it is a GCP - service or otherwise. Therefore, this method does not attempt to parse the error response in - any way. - - Args: - error: An error raised by the googleapiclient module while making an HTTP call. - message: A message to be included in the resulting ``FirebaseError`` (optional). If not - specified the string representation of the ``error`` argument is used as the message. - code: A GCP error code that will be used to determine the resulting error type (optional). - If not specified the HTTP status code on the error response is used to determine a - suitable error code. - http_response: A requests HTTP response object to associate with the exception (optional). - If not specified, one will be created from the ``error``. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code. - """ - if isinstance(error, socket.timeout) or ( - isinstance(error, socket.error) and 'timed out' in str(error)): - return exceptions.DeadlineExceededError( - message='Timed out while making an API call: {0}'.format(error), - cause=error) - if isinstance(error, httplib2.ServerNotFoundError): - return exceptions.UnavailableError( - message='Failed to establish a connection: {0}'.format(error), - cause=error) - if not isinstance(error, googleapiclient.errors.HttpError): - return exceptions.UnknownError( - message='Unknown error while making a remote service call: {0}'.format(error), - cause=error) - - if not code: - code = _utils._http_status_to_error_code(error.resp.status) # pylint: disable=protected-access - if not message: - message = str(error) - if not http_response: - http_response = _http_response_from_googleapiclient_error(error) - - err_type = _utils._error_code_to_exception_type(code) # pylint: disable=protected-access - return err_type(message=message, cause=error, http_response=http_response) - - -def _http_response_from_googleapiclient_error(error): - """Creates a requests HTTP Response object from the given googleapiclient error.""" - resp = requests.models.Response() - resp.raw = io.BytesIO(error.content) - resp.status_code = error.resp.status - return resp diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 99dc93a67..0e3a55f49 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -18,21 +18,16 @@ from typing import Any, Callable, Dict, List, Optional, cast import concurrent.futures import json -import warnings import asyncio import logging import requests import httpx -from googleapiclient import http -from googleapiclient import _auth - import firebase_admin from firebase_admin import ( _http_client, _messaging_encoder, _messaging_utils, - _gapic_utils, _utils, exceptions, App @@ -72,8 +67,6 @@ 'WebpushNotificationAction', 'send', - 'send_all', - 'send_multicast', 'send_each', 'send_each_async', 'send_each_for_multicast', @@ -246,64 +239,6 @@ def send_each_for_multicast(multicast_message, dry_run=False, app=None): ) for token in multicast_message.tokens] return _get_messaging_service(app).send_each(messages, dry_run) -def send_all(messages, dry_run=False, app=None): - """Sends the given list of messages via Firebase Cloud Messaging as a single batch. - - If the ``dry_run`` mode is enabled, the message will not be actually delivered to the - recipients. Instead, FCM performs all the usual validations and emulates the send operation. - - Args: - messages: A list of ``messaging.Message`` instances. - dry_run: A boolean indicating whether to run the operation in dry run mode (optional). - app: An App instance (optional). - - Returns: - BatchResponse: A ``messaging.BatchResponse`` instance. - - Raises: - FirebaseError: If an error occurs while sending the message to the FCM service. - ValueError: If the input arguments are invalid. - - send_all() is deprecated. Use send_each() instead. - """ - warnings.warn('send_all() is deprecated. Use send_each() instead.', DeprecationWarning) - return _get_messaging_service(app).send_all(messages, dry_run) - -def send_multicast(multicast_message, dry_run=False, app=None): - """Sends the given mutlicast message to all tokens via Firebase Cloud Messaging (FCM). - - If the ``dry_run`` mode is enabled, the message will not be actually delivered to the - recipients. Instead, FCM performs all the usual validations and emulates the send operation. - - Args: - multicast_message: An instance of ``messaging.MulticastMessage``. - dry_run: A boolean indicating whether to run the operation in dry run mode (optional). - app: An App instance (optional). - - Returns: - BatchResponse: A ``messaging.BatchResponse`` instance. - - Raises: - FirebaseError: If an error occurs while sending the message to the FCM service. - ValueError: If the input arguments are invalid. - - send_multicast() is deprecated. Use send_each_for_multicast() instead. - """ - warnings.warn('send_multicast() is deprecated. Use send_each_for_multicast() instead.', - DeprecationWarning) - if not isinstance(multicast_message, MulticastMessage): - raise ValueError('Message must be an instance of messaging.MulticastMessage class.') - messages = [Message( - data=multicast_message.data, - notification=multicast_message.notification, - android=multicast_message.android, - webpush=multicast_message.webpush, - apns=multicast_message.apns, - fcm_options=multicast_message.fcm_options, - token=token - ) for token in multicast_message.tokens] - return _get_messaging_service(app).send_all(messages, dry_run) - def subscribe_to_topic(tokens, topic, app=None): """Subscribes a list of registration tokens to an FCM topic. @@ -472,7 +407,6 @@ def __init__(self, app: App) -> None: self._client = _http_client.JsonHttpClient(credential=self._credential, timeout=timeout) self._async_client = _http_client.HttpxAsyncClient( credential=self._credential, timeout=timeout) - self._build_transport = _auth.authorized_http @classmethod def encode_message(cls, message): @@ -555,45 +489,6 @@ async def send_data(data): message='Unknown error while making remote service calls: {0}'.format(error), cause=error) - - def send_all(self, messages, dry_run=False): - """Sends the given messages to FCM via the batch API.""" - if not isinstance(messages, list): - raise ValueError('messages must be a list of messaging.Message instances.') - if len(messages) > 500: - raise ValueError('messages must not contain more than 500 elements.') - - responses = [] - - def batch_callback(_, response, error): - exception = None - if error: - exception = self._handle_batch_error(error) - send_response = SendResponse(response, exception) - responses.append(send_response) - - batch = http.BatchHttpRequest( - callback=batch_callback, batch_uri=_MessagingService.FCM_BATCH_URL) - transport = self._build_transport(self._credential) - for message in messages: - body = json.dumps(self._message_data(message, dry_run)) - req = http.HttpRequest( - http=transport, - postproc=self._postproc, - uri=self._fcm_url, - method='POST', - body=body, - headers=self._fcm_headers - ) - batch.add(req) - - try: - batch.execute() - except Exception as error: - raise self._handle_batch_error(error) - else: - return BatchResponse(responses) - def make_topic_management_request(self, tokens, topic, operation): """Invokes the IID service for topic management functionality.""" if isinstance(tokens, str): @@ -670,11 +565,6 @@ def _handle_iid_error(self, error): return _utils.handle_requests_error(error, msg) - def _handle_batch_error(self, error): - """Handles errors received from the googleapiclient while making batch requests.""" - return _gapic_utils.handle_platform_error_from_googleapiclient( - error, _MessagingService._build_fcm_error_googleapiclient) - def close(self) -> None: asyncio.run(self._async_client.aclose()) @@ -700,14 +590,6 @@ def _build_fcm_error_httpx( message, cause=error, http_response=error.response) if exc_type else None return exc_type(message, cause=error) if exc_type else None - - @classmethod - def _build_fcm_error_googleapiclient(cls, error, message, error_dict, http_response): - """Parses an error response from the FCM API and creates a FCM-specific exception if - appropriate.""" - exc_type = cls._build_fcm_error(error_dict) - return exc_type(message, cause=error, http_response=http_response) if exc_type else None - @classmethod def _build_fcm_error( cls, diff --git a/integration/test_messaging.py b/integration/test_messaging.py index 296a4d338..804691962 100644 --- a/integration/test_messaging.py +++ b/integration/test_messaging.py @@ -149,71 +149,6 @@ def test_send_each_for_multicast(): assert response.exception is not None assert response.message_id is None -@pytest.mark.skip(reason="Replaced with test_send_each") -def test_send_all(): - messages = [ - messaging.Message( - topic='foo-bar', notification=messaging.Notification('Title', 'Body')), - messaging.Message( - topic='foo-bar', notification=messaging.Notification('Title', 'Body')), - messaging.Message( - token='not-a-token', notification=messaging.Notification('Title', 'Body')), - ] - - batch_response = messaging.send_all(messages, dry_run=True) - - assert batch_response.success_count == 2 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 3 - - response = batch_response.responses[0] - assert response.success is True - assert response.exception is None - assert re.match('^projects/.*/messages/.*$', response.message_id) - - response = batch_response.responses[1] - assert response.success is True - assert response.exception is None - assert re.match('^projects/.*/messages/.*$', response.message_id) - - response = batch_response.responses[2] - assert response.success is False - assert isinstance(response.exception, exceptions.InvalidArgumentError) - assert response.message_id is None - -@pytest.mark.skip(reason="Replaced with test_send_each_500") -def test_send_all_500(): - messages = [] - for msg_number in range(500): - topic = 'foo-bar-{0}'.format(msg_number % 10) - messages.append(messaging.Message(topic=topic)) - - batch_response = messaging.send_all(messages, dry_run=True) - - assert batch_response.success_count == 500 - assert batch_response.failure_count == 0 - assert len(batch_response.responses) == 500 - for response in batch_response.responses: - assert response.success is True - assert response.exception is None - assert re.match('^projects/.*/messages/.*$', response.message_id) - -@pytest.mark.skip(reason="Replaced with test_send_each_for_multicast") -def test_send_multicast(): - multicast = messaging.MulticastMessage( - notification=messaging.Notification('Title', 'Body'), - tokens=['not-a-token', 'also-not-a-token']) - - batch_response = messaging.send_multicast(multicast) - - assert batch_response.success_count == 0 - assert batch_response.failure_count == 2 - assert len(batch_response.responses) == 2 - for response in batch_response.responses: - assert response.success is False - assert response.exception is not None - assert response.message_id is None - def test_subscribe(): resp = messaging.subscribe_to_topic(_REGISTRATION_TOKEN, 'mock-topic') assert resp.success_count + resp.failure_count == 1 diff --git a/requirements.txt b/requirements.txt index ba6f2f947..b5642b549 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ respx == 0.22.0 cachecontrol >= 0.12.14 google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != 'PyPy' -google-api-python-client >= 1.7.8 google-cloud-firestore >= 2.19.0; platform.python_implementation != 'PyPy' google-cloud-storage >= 1.37.1 pyjwt[crypto] >= 2.5.0 diff --git a/setup.py b/setup.py index e92d207aa..b9eb11806 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,6 @@ install_requires = [ 'cachecontrol>=0.12.14', 'google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != "PyPy"', - 'google-api-python-client >= 1.7.8', 'google-cloud-firestore>=2.19.0; platform.python_implementation != "PyPy"', 'google-cloud-storage>=1.37.1', 'pyjwt[crypto] >= 2.5.0', diff --git a/snippets/messaging/cloud_messaging.py b/snippets/messaging/cloud_messaging.py index bb63db065..18a992dcc 100644 --- a/snippets/messaging/cloud_messaging.py +++ b/snippets/messaging/cloud_messaging.py @@ -222,9 +222,9 @@ def unsubscribe_from_topic(): # [END unsubscribe] -def send_all(): +def send_each(): registration_token = 'YOUR_REGISTRATION_TOKEN' - # [START send_all] + # [START send_each] # Create a list containing up to 500 messages. messages = [ messaging.Message( @@ -238,15 +238,15 @@ def send_all(): ), ] - response = messaging.send_all(messages) + response = messaging.send_each(messages) # See the BatchResponse reference documentation # for the contents of response. print('{0} messages were sent successfully'.format(response.success_count)) - # [END send_all] + # [END send_each] -def send_multicast(): - # [START send_multicast] +def send_each_for_multicast(): + # [START send_each_for_multicast] # Create a list containing up to 500 registration tokens. # These registration tokens come from the client FCM SDKs. registration_tokens = [ @@ -259,15 +259,15 @@ def send_multicast(): data={'score': '850', 'time': '2:45'}, tokens=registration_tokens, ) - response = messaging.send_multicast(message) + response = messaging.send_each_for_multicast(message) # See the BatchResponse reference documentation # for the contents of response. print('{0} messages were sent successfully'.format(response.success_count)) - # [END send_multicast] + # [END send_each_for_multicast] -def send_multicast_and_handle_errors(): - # [START send_multicast_error] +def send_each_for_multicast_and_handle_errors(): + # [START send_each_for_multicast_error] # These registration tokens come from the client FCM SDKs. registration_tokens = [ 'YOUR_REGISTRATION_TOKEN_1', @@ -279,7 +279,7 @@ def send_multicast_and_handle_errors(): data={'score': '850', 'time': '2:45'}, tokens=registration_tokens, ) - response = messaging.send_multicast(message) + response = messaging.send_each_for_multicast(message) if response.failure_count > 0: responses = response.responses failed_tokens = [] @@ -288,4 +288,4 @@ def send_multicast_and_handle_errors(): # The order of responses corresponds to the order of the registration tokens. failed_tokens.append(registration_tokens[idx]) print('List of tokens that caused failures: {0}'.format(failed_tokens)) - # [END send_multicast_error] + # [END send_each_for_multicast_error] diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4347c838a..fa1276feb 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -14,17 +14,12 @@ import io import json -import socket -import httplib2 -import pytest import requests from requests import models -from googleapiclient import errors from firebase_admin import exceptions from firebase_admin import _utils -from firebase_admin import _gapic_utils _NOT_FOUND_ERROR_DICT = { @@ -178,159 +173,3 @@ def _create_response(self, status=500, payload=None): resp.raw = io.BytesIO(payload.encode()) exc = requests.exceptions.RequestException('Test error', response=resp) return resp, exc - - -class TestGoogleApiClient: - - @pytest.mark.parametrize('error', [ - socket.timeout('Test error'), - socket.error('Read timed out') - ]) - def test_googleapicleint_timeout_error(self, error): - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.DeadlineExceededError) - assert str(firebase_error) == 'Timed out while making an API call: {0}'.format(error) - assert firebase_error.cause is error - assert firebase_error.http_response is None - - def test_googleapiclient_connection_error(self): - error = httplib2.ServerNotFoundError('Test error') - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.UnavailableError) - assert str(firebase_error) == 'Failed to establish a connection: Test error' - assert firebase_error.cause is error - assert firebase_error.http_response is None - - def test_unknown_transport_error(self): - error = socket.error('Test error') - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.UnknownError) - assert str(firebase_error) == 'Unknown error while making a remote service call: Test error' - assert firebase_error.cause is error - assert firebase_error.http_response is None - - def test_http_response(self): - error = self._create_http_error() - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.InternalError) - assert str(firebase_error) == str(error) - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_http_response_with_unknown_status(self): - error = self._create_http_error(status=501) - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.UnknownError) - assert str(firebase_error) == str(error) - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 501 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_http_response_with_message(self): - error = self._create_http_error() - firebase_error = _gapic_utils.handle_googleapiclient_error( - error, message='Explicit error message') - assert isinstance(firebase_error, exceptions.InternalError) - assert str(firebase_error) == 'Explicit error message' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_http_response_with_code(self): - error = self._create_http_error() - firebase_error = _gapic_utils.handle_googleapiclient_error( - error, code=exceptions.UNAVAILABLE) - assert isinstance(firebase_error, exceptions.UnavailableError) - assert str(firebase_error) == str(error) - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_http_response_with_message_and_code(self): - error = self._create_http_error() - firebase_error = _gapic_utils.handle_googleapiclient_error( - error, message='Explicit error message', code=exceptions.UNAVAILABLE) - assert isinstance(firebase_error, exceptions.UnavailableError) - assert str(firebase_error) == 'Explicit error message' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_handle_platform_error(self): - error = self._create_http_error(payload=_NOT_FOUND_PAYLOAD) - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) - assert isinstance(firebase_error, exceptions.NotFoundError) - assert str(firebase_error) == 'test error' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == _NOT_FOUND_PAYLOAD - - def test_handle_platform_error_with_no_response(self): - error = socket.error('Test error') - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) - assert isinstance(firebase_error, exceptions.UnknownError) - assert str(firebase_error) == 'Unknown error while making a remote service call: Test error' - assert firebase_error.cause is error - assert firebase_error.http_response is None - - def test_handle_platform_error_with_no_error_code(self): - error = self._create_http_error(payload='no error code') - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) - assert isinstance(firebase_error, exceptions.InternalError) - message = 'Unexpected HTTP response with status: 500; body: no error code' - assert str(firebase_error) == message - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'no error code' - - def test_handle_platform_error_with_custom_handler(self): - error = self._create_http_error(payload=_NOT_FOUND_PAYLOAD) - invocations = [] - - def _custom_handler(cause, message, error_dict, http_response): - invocations.append((cause, message, error_dict, http_response)) - return exceptions.InvalidArgumentError('Custom message', cause, http_response) - - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient( - error, _custom_handler) - - assert isinstance(firebase_error, exceptions.InvalidArgumentError) - assert str(firebase_error) == 'Custom message' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == _NOT_FOUND_PAYLOAD - assert len(invocations) == 1 - args = invocations[0] - assert len(args) == 4 - assert args[0] is error - assert args[1] == 'test error' - assert args[2] == _NOT_FOUND_ERROR_DICT - assert args[3] is not None - - def test_handle_platform_error_with_custom_handler_ignore(self): - error = self._create_http_error(payload=_NOT_FOUND_PAYLOAD) - invocations = [] - - def _custom_handler(cause, message, error_dict, http_response): - invocations.append((cause, message, error_dict, http_response)) - - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient( - error, _custom_handler) - - assert isinstance(firebase_error, exceptions.NotFoundError) - assert str(firebase_error) == 'test error' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == _NOT_FOUND_PAYLOAD - assert len(invocations) == 1 - args = invocations[0] - assert len(args) == 4 - assert args[0] is error - assert args[1] == 'test error' - assert args[2] == _NOT_FOUND_ERROR_DICT - assert args[3] is not None - - def _create_http_error(self, status=500, payload='Body'): - resp = httplib2.Response({'status': status}) - return errors.HttpError(resp, payload.encode()) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 76cee2a33..341fd9e07 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -20,8 +20,6 @@ import httpx import respx -from googleapiclient import http -from googleapiclient import _helpers import pytest import firebase_admin @@ -1826,17 +1824,7 @@ def test_send_unknown_fcm_error_code(self, status): self._assert_request(recorder[0], 'POST', self._get_url('explicit-project-id'), body) -class _HttpMockException: - - def __init__(self, exc): - self._exc = exc - - def request(self, url, **kwargs): - raise self._exc - - -class TestBatch: - +class TestSendEach(): @classmethod def setup_class(cls): cred = testutils.MockCredential() @@ -1856,40 +1844,6 @@ def _instrument_messaging_service(self, response_dict, app=None): testutils.MockRequestBasedMultiRequestAdapter(response_dict, recorder)) return fcm_service, recorder - def _instrument_batch_messaging_service(self, app=None, status=200, payload='', exc=None): - def build_mock_transport(_): - if exc: - return _HttpMockException(exc) - - if status == 200: - content_type = 'multipart/mixed; boundary=boundary' - else: - content_type = 'application/json' - return http.HttpMockSequence([ - ({'status': str(status), 'content-type': content_type}, payload), - ]) - - if not app: - app = firebase_admin.get_app() - - fcm_service = messaging._get_messaging_service(app) - fcm_service._build_transport = build_mock_transport - return fcm_service - - def _batch_payload(self, payloads): - # payloads should be a list of (status_code, content) tuples - payload = '' - _playload_format = """--boundary\r\nContent-Type: application/http\r\n\ -Content-ID: \r\n\r\nHTTP/1.1 {} Success\r\n\ -Content-Type: application/json; charset=UTF-8\r\n\r\n{}\r\n\r\n""" - for (index, (status_code, content)) in enumerate(payloads): - payload += _playload_format.format(str(index + 1), str(status_code), content) - payload += '--boundary--' - return payload - - -class TestSendEach(TestBatch): - def test_no_project_id(self): def evaluate(): app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') @@ -1948,12 +1902,6 @@ async def test_send_each_async(self): batch_response = await messaging.send_each_async([msg1, msg2, msg3], dry_run=True) - # try: - # batch_response = await messaging.send_each_async([msg1, msg2], dry_run=True) - # except Exception as error: - # if isinstance(error.cause.__cause__, StopIteration): - # raise Exception('Received more requests than mocks') - assert batch_response.success_count == 3 assert batch_response.failure_count == 0 assert len(batch_response.responses) == 3 @@ -2217,19 +2165,19 @@ def test_send_each_fcm_error_code(self, status, fcm_error_code, exc_type): check_exception(exception, 'test error', status) -class TestSendEachForMulticast(TestBatch): +class TestSendEachForMulticast(TestSendEach): def test_no_project_id(self): def evaluate(): app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') with pytest.raises(ValueError): - messaging.send_all([messaging.Message(topic='foo')], app=app) + messaging.send_each([messaging.Message(topic='foo')], app=app) testutils.run_without_project_id(evaluate) @pytest.mark.parametrize('msg', NON_LIST_ARGS) def test_invalid_send_each_for_multicast(self, msg): with pytest.raises(ValueError) as excinfo: - messaging.send_multicast(msg) + messaging.send_each_for_multicast(msg) expected = 'Message must be an instance of messaging.MulticastMessage class.' assert str(excinfo.value) == expected @@ -2338,432 +2286,6 @@ def test_send_each_for_multicast_fcm_error_code(self, status): check_exception(exception, 'test error', status) -class TestSendAll(TestBatch): - - def test_no_project_id(self): - def evaluate(): - app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') - with pytest.raises(ValueError): - messaging.send_all([messaging.Message(topic='foo')], app=app) - testutils.run_without_project_id(evaluate) - - @pytest.mark.parametrize('msg', NON_LIST_ARGS) - def test_invalid_send_all(self, msg): - with pytest.raises(ValueError) as excinfo: - messaging.send_all(msg) - if isinstance(msg, list): - expected = 'Message must be an instance of messaging.Message class.' - assert str(excinfo.value) == expected - else: - expected = 'messages must be a list of messaging.Message instances.' - assert str(excinfo.value) == expected - - def test_invalid_over_500(self): - msg = messaging.Message(topic='foo') - with pytest.raises(ValueError) as excinfo: - messaging.send_all([msg for _ in range(0, 501)]) - expected = 'messages must not contain more than 500 elements.' - assert str(excinfo.value) == expected - - def test_send_all(self): - payload = json.dumps({'name': 'message-id'}) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, payload), (200, payload)])) - msg = messaging.Message(topic='foo') - batch_response = messaging.send_all([msg, msg], dry_run=True) - assert batch_response.success_count == 2 - assert batch_response.failure_count == 0 - assert len(batch_response.responses) == 2 - assert [r.message_id for r in batch_response.responses] == ['message-id', 'message-id'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) - - def test_send_all_with_positional_param_enforcement(self): - payload = json.dumps({'name': 'message-id'}) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, payload), (200, payload)])) - msg = messaging.Message(topic='foo') - - enforcement = _helpers.positional_parameters_enforcement - _helpers.positional_parameters_enforcement = _helpers.POSITIONAL_EXCEPTION - try: - batch_response = messaging.send_all([msg, msg], dry_run=True) - assert batch_response.success_count == 2 - finally: - _helpers.positional_parameters_enforcement = enforcement - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_detailed_error(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.Message(topic='foo') - batch_response = messaging.send_all([msg, msg]) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - exception = error_response.exception - assert isinstance(exception, exceptions.InvalidArgumentError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_canonical_error_code(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'NOT_FOUND', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.Message(topic='foo') - batch_response = messaging.send_all([msg, msg]) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - exception = error_response.exception - assert isinstance(exception, exceptions.NotFoundError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - @pytest.mark.parametrize('fcm_error_code, exc_type', FCM_ERROR_CODES.items()) - def test_send_all_fcm_error_code(self, status, fcm_error_code, exc_type): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error', - 'details': [ - { - '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', - 'errorCode': fcm_error_code, - }, - ], - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.Message(topic='foo') - batch_response = messaging.send_all([msg, msg]) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - exception = error_response.exception - assert isinstance(exception, exc_type) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status, exc_type', HTTP_ERROR_CODES.items()) - def test_send_all_batch_error(self, status, exc_type): - _ = self._instrument_batch_messaging_service(status=status, payload='{}') - msg = messaging.Message(topic='foo') - with pytest.raises(exc_type) as excinfo: - messaging.send_all([msg]) - expected = 'Unexpected HTTP response with status: {0}; body: {{}}'.format(status) - check_exception(excinfo.value, expected, status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_batch_detailed_error(self, status): - payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.Message(topic='foo') - with pytest.raises(exceptions.InvalidArgumentError) as excinfo: - messaging.send_all([msg]) - check_exception(excinfo.value, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_batch_canonical_error_code(self, status): - payload = json.dumps({ - 'error': { - 'status': 'NOT_FOUND', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.Message(topic='foo') - with pytest.raises(exceptions.NotFoundError) as excinfo: - messaging.send_all([msg]) - check_exception(excinfo.value, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_batch_fcm_error_code(self, status): - payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error', - 'details': [ - { - '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', - 'errorCode': 'UNREGISTERED', - }, - ], - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.Message(topic='foo') - with pytest.raises(messaging.UnregisteredError) as excinfo: - messaging.send_all([msg]) - check_exception(excinfo.value, 'test error', status) - - def test_send_all_runtime_exception(self): - exc = BrokenPipeError('Test error') - _ = self._instrument_batch_messaging_service(exc=exc) - msg = messaging.Message(topic='foo') - - with pytest.raises(exceptions.UnknownError) as excinfo: - messaging.send_all([msg]) - - expected = 'Unknown error while making a remote service call: Test error' - assert str(excinfo.value) == expected - assert excinfo.value.cause is exc - assert excinfo.value.http_response is None - - def test_send_transport_init(self): - def track_call_count(build_transport): - def wrapper(credential): - wrapper.calls += 1 - return build_transport(credential) - wrapper.calls = 0 - return wrapper - - payload = json.dumps({'name': 'message-id'}) - fcm_service = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, payload), (200, payload)])) - build_mock_transport = fcm_service._build_transport - fcm_service._build_transport = track_call_count(build_mock_transport) - msg = messaging.Message(topic='foo') - - batch_response = messaging.send_all([msg, msg], dry_run=True) - assert batch_response.success_count == 2 - assert fcm_service._build_transport.calls == 1 - - batch_response = messaging.send_all([msg, msg], dry_run=True) - assert batch_response.success_count == 2 - assert fcm_service._build_transport.calls == 2 - - -class TestSendMulticast(TestBatch): - - def test_no_project_id(self): - def evaluate(): - app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') - with pytest.raises(ValueError): - messaging.send_all([messaging.Message(topic='foo')], app=app) - testutils.run_without_project_id(evaluate) - - @pytest.mark.parametrize('msg', NON_LIST_ARGS) - def test_invalid_send_multicast(self, msg): - with pytest.raises(ValueError) as excinfo: - messaging.send_multicast(msg) - expected = 'Message must be an instance of messaging.MulticastMessage class.' - assert str(excinfo.value) == expected - - def test_send_multicast(self): - payload = json.dumps({'name': 'message-id'}) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, payload), (200, payload)])) - msg = messaging.MulticastMessage(tokens=['foo', 'foo']) - batch_response = messaging.send_multicast(msg, dry_run=True) - assert batch_response.success_count == 2 - assert batch_response.failure_count == 0 - assert len(batch_response.responses) == 2 - assert [r.message_id for r in batch_response.responses] == ['message-id', 'message-id'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_detailed_error(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.MulticastMessage(tokens=['foo', 'foo']) - batch_response = messaging.send_multicast(msg) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - assert error_response.exception is not None - exception = error_response.exception - assert isinstance(exception, exceptions.InvalidArgumentError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_canonical_error_code(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'NOT_FOUND', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.MulticastMessage(tokens=['foo', 'foo']) - batch_response = messaging.send_multicast(msg) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - assert error_response.exception is not None - exception = error_response.exception - assert isinstance(exception, exceptions.NotFoundError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_fcm_error_code(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error', - 'details': [ - { - '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', - 'errorCode': 'UNREGISTERED', - }, - ], - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.MulticastMessage(tokens=['foo', 'foo']) - batch_response = messaging.send_multicast(msg) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - assert error_response.exception is not None - exception = error_response.exception - assert isinstance(exception, messaging.UnregisteredError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status, exc_type', HTTP_ERROR_CODES.items()) - def test_send_multicast_batch_error(self, status, exc_type): - _ = self._instrument_batch_messaging_service(status=status, payload='{}') - msg = messaging.MulticastMessage(tokens=['foo']) - with pytest.raises(exc_type) as excinfo: - messaging.send_multicast(msg) - expected = 'Unexpected HTTP response with status: {0}; body: {{}}'.format(status) - check_exception(excinfo.value, expected, status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_batch_detailed_error(self, status): - payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.MulticastMessage(tokens=['foo']) - with pytest.raises(exceptions.InvalidArgumentError) as excinfo: - messaging.send_multicast(msg) - check_exception(excinfo.value, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_batch_canonical_error_code(self, status): - payload = json.dumps({ - 'error': { - 'status': 'NOT_FOUND', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.MulticastMessage(tokens=['foo']) - with pytest.raises(exceptions.NotFoundError) as excinfo: - messaging.send_multicast(msg) - check_exception(excinfo.value, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_batch_fcm_error_code(self, status): - payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error', - 'details': [ - { - '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', - 'errorCode': 'UNREGISTERED', - }, - ], - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.MulticastMessage(tokens=['foo']) - with pytest.raises(messaging.UnregisteredError) as excinfo: - messaging.send_multicast(msg) - check_exception(excinfo.value, 'test error', status) - - def test_send_multicast_runtime_exception(self): - exc = BrokenPipeError('Test error') - _ = self._instrument_batch_messaging_service(exc=exc) - msg = messaging.MulticastMessage(tokens=['foo']) - - with pytest.raises(exceptions.UnknownError) as excinfo: - messaging.send_multicast(msg) - - expected = 'Unknown error while making a remote service call: Test error' - assert str(excinfo.value) == expected - assert excinfo.value.cause is exc - assert excinfo.value.http_response is None - - class TestTopicManagement: _DEFAULT_RESPONSE = json.dumps({'results': [{}, {'error': 'error_reason'}]}) From dae267c1f93450852de904627f364706718f8356 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:59:53 -0400 Subject: [PATCH 2/6] chore(deps): Bump minimum supported Python version to 3.9 and add 3.13 to CIs (#892) * chore(deps): Bump minimum supported Python version to 3.9 and add 3.13 to CIs * fix deprecation warnings * fix GHA build status svg * fix: Correctly scope async eventloop * fix: Bump pylint to v2.7.4 and astroid to v2.5.8 to fix lint issues * fix ml tests * fix lint * fix: remove commented code --- .github/workflows/ci.yml | 6 +++--- .github/workflows/nightly.yml | 5 +++-- .github/workflows/release.yml | 5 +++-- CONTRIBUTING.md | 2 +- README.md | 6 +++--- firebase_admin/__init__.py | 5 +++-- firebase_admin/_auth_providers.py | 6 +++--- firebase_admin/_auth_utils.py | 16 ++++++++-------- firebase_admin/_sseclient.py | 2 +- firebase_admin/_token_gen.py | 6 +++--- firebase_admin/_user_import.py | 6 +++--- firebase_admin/_user_mgt.py | 16 ++++++++-------- firebase_admin/app_check.py | 20 ++++++++++---------- firebase_admin/credentials.py | 10 +++++----- firebase_admin/db.py | 2 +- firebase_admin/messaging.py | 5 ++++- firebase_admin/ml.py | 5 +---- firebase_admin/project_management.py | 8 ++++---- firebase_admin/storage.py | 4 ++-- firebase_admin/tenant_mgt.py | 5 +---- integration/conftest.py | 7 ------- integration/test_firestore_async.py | 8 ++++---- integration/test_messaging.py | 6 +++--- integration/test_ml.py | 14 +++++++++----- integration/test_storage.py | 2 +- requirements.txt | 8 ++++---- setup.cfg | 2 ++ setup.py | 7 +++---- tests/test_db.py | 2 +- tests/test_messaging.py | 23 ++++++++++++----------- tests/test_ml.py | 6 +++--- tests/test_remote_config.py | 2 +- tests/test_sseclient.py | 4 ++-- tests/test_tenant_mgt.py | 6 +++--- tests/testutils.py | 2 +- 35 files changed, 119 insertions(+), 120 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cc8ec481..bfd29e2cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.9'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.9'] steps: - uses: actions/checkout@v4 @@ -35,10 +35,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 282cb1b91..3d5420537 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -36,7 +36,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | @@ -45,6 +45,7 @@ jobs: pip install setuptools wheel pip install tensorflow pip install keras + pip install build - name: Run unit tests run: pytest @@ -57,7 +58,7 @@ jobs: # Build the Python Wheel and the source distribution. - name: Package release artifacts - run: python setup.py bdist_wheel sdist + run: python -m build # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a7986a5a..6cd1d3f07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | @@ -56,6 +56,7 @@ jobs: pip install setuptools wheel pip install tensorflow pip install keras + pip install build - name: Run unit tests run: pytest @@ -68,7 +69,7 @@ jobs: # Build the Python Wheel and the source distribution. - name: Package release artifacts - run: python setup.py bdist_wheel sdist + run: python -m build # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de5934866..72933a24f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ information on using pull requests. ### Initial Setup -You need Python 3.8+ to build and test the code in this repo. +You need Python 3.9+ to build and test the code in this repo. We recommend using [pip](https://pypi.python.org/pypi/pip) for installing the necessary tools and project dependencies. Most recent versions of Python ship with pip. If your development environment diff --git a/README.md b/README.md index 6e3ed6805..29303fd4f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/firebase/firebase-admin-python.svg?branch=master)](https://travis-ci.org/firebase/firebase-admin-python) +[![Nightly Builds](https://github.com/firebase/firebase-admin-python/actions/workflows/nightly.yml/badge.svg)](https://github.com/firebase/firebase-admin-python/actions/workflows/nightly.yml) [![Python](https://img.shields.io/pypi/pyversions/firebase-admin.svg)](https://pypi.org/project/firebase-admin/) [![Version](https://img.shields.io/pypi/v/firebase-admin.svg)](https://pypi.org/project/firebase-admin/) @@ -43,8 +43,8 @@ requests, code review feedback, and also pull requests. ## Supported Python Versions -We currently support Python 3.7+. However, Python 3.7 and Python 3.8 support is deprecated, -and developers are strongly advised to use Python 3.9 or higher. Firebase +We currently support Python 3.9+. However, Python 3.9 support is deprecated, +and developers are strongly advised to use Python 3.10 or higher. Firebase Admin Python SDK is also tested on PyPy and [Google App Engine](https://cloud.google.com/appengine/) environments. diff --git a/firebase_admin/__init__.py b/firebase_admin/__init__.py index 7bb9c59c2..597aaa6b6 100644 --- a/firebase_admin/__init__.py +++ b/firebase_admin/__init__.py @@ -178,11 +178,12 @@ def _load_from_environment(self): with open(config_file, 'r') as json_file: json_str = json_file.read() except Exception as err: - raise ValueError('Unable to read file {}. {}'.format(config_file, err)) + raise ValueError('Unable to read file {}. {}'.format(config_file, err)) from err try: json_data = json.loads(json_str) except Exception as err: - raise ValueError('JSON string "{0}" is not valid json. {1}'.format(json_str, err)) + raise ValueError( + 'JSON string "{0}" is not valid json. {1}'.format(json_str, err)) from err return {k: v for k, v in json_data.items() if k in _CONFIG_VALID_KEYS} diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 31894a4dc..6512a4f7b 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -422,13 +422,13 @@ def _validate_url(url, label): if not parsed.netloc: raise ValueError('Malformed {0}: "{1}".'.format(label, url)) return url - except Exception: - raise ValueError('Malformed {0}: "{1}".'.format(label, url)) + except Exception as exception: + raise ValueError('Malformed {0}: "{1}".'.format(label, url)) from exception def _validate_x509_certificates(x509_certificates): if not isinstance(x509_certificates, list) or not x509_certificates: raise ValueError('x509_certificates must be a non-empty list.') - if not all([isinstance(cert, str) and cert for cert in x509_certificates]): + if not all(isinstance(cert, str) and cert for cert in x509_certificates): raise ValueError('x509_certificates must only contain non-empty strings.') return [{'x509Certificate': cert} for cert in x509_certificates] diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index ac7b322ff..0d56ca7fa 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -175,8 +175,8 @@ def validate_photo_url(photo_url, required=False): if not parsed.netloc: raise ValueError('Malformed photo URL: "{0}".'.format(photo_url)) return photo_url - except Exception: - raise ValueError('Malformed photo URL: "{0}".'.format(photo_url)) + except Exception as err: + raise ValueError('Malformed photo URL: "{0}".'.format(photo_url)) from err def validate_timestamp(timestamp, label, required=False): """Validates the given timestamp value. Timestamps must be positive integers.""" @@ -186,8 +186,8 @@ def validate_timestamp(timestamp, label, required=False): raise ValueError('Boolean value specified as timestamp.') try: timestamp_int = int(timestamp) - except TypeError: - raise ValueError('Invalid type for timestamp value: {0}.'.format(timestamp)) + except TypeError as err: + raise ValueError('Invalid type for timestamp value: {0}.'.format(timestamp)) from err else: if timestamp_int != timestamp: raise ValueError('{0} must be a numeric value and a whole number.'.format(label)) @@ -207,8 +207,8 @@ def validate_int(value, label, low=None, high=None): raise ValueError('Invalid type for integer value: {0}.'.format(value)) try: val_int = int(value) - except TypeError: - raise ValueError('Invalid type for integer value: {0}.'.format(value)) + except TypeError as err: + raise ValueError('Invalid type for integer value: {0}.'.format(value)) from err else: if val_int != value: # This will be True for non-numeric values like '2' and non-whole numbers like 2.5. @@ -246,8 +246,8 @@ def validate_custom_claims(custom_claims, required=False): MAX_CLAIMS_PAYLOAD_SIZE)) try: parsed = json.loads(claims_str) - except Exception: - raise ValueError('Failed to parse custom claims string as JSON.') + except Exception as err: + raise ValueError('Failed to parse custom claims string as JSON.') from err if not isinstance(parsed, dict): raise ValueError('Custom claims must be parseable as a JSON object.') diff --git a/firebase_admin/_sseclient.py b/firebase_admin/_sseclient.py index 6585dfc80..ec20cb45c 100644 --- a/firebase_admin/_sseclient.py +++ b/firebase_admin/_sseclient.py @@ -34,7 +34,7 @@ class KeepAuthSession(transport.requests.AuthorizedSession): """A session that does not drop authentication on redirects between domains.""" def __init__(self, credential): - super(KeepAuthSession, self).__init__(credential) + super().__init__(credential) def rebuild_auth(self, prepared_request, response): pass diff --git a/firebase_admin/_token_gen.py b/firebase_admin/_token_gen.py index a2fc725e8..6d82bf7a6 100644 --- a/firebase_admin/_token_gen.py +++ b/firebase_admin/_token_gen.py @@ -158,7 +158,7 @@ def signing_provider(self): 'Failed to determine service account: {0}. Make sure to initialize the SDK ' 'with service account credentials or specify a service account ID with ' 'iam.serviceAccounts.signBlob permission. Please refer to {1} for more ' - 'details on creating custom tokens.'.format(error, url)) + 'details on creating custom tokens.'.format(error, url)) from error return self._signing_provider def create_custom_token(self, uid, developer_claims=None, tenant_id=None): @@ -203,7 +203,7 @@ def create_custom_token(self, uid, developer_claims=None, tenant_id=None): return jwt.encode(signing_provider.signer, payload, header=header) except google.auth.exceptions.TransportError as error: msg = 'Failed to sign custom token. {0}'.format(error) - raise TokenSignError(msg, error) + raise TokenSignError(msg, error) from error def create_session_cookie(self, id_token, expires_in): @@ -403,7 +403,7 @@ def verify(self, token, request, clock_skew_seconds=0): verified_claims['uid'] = verified_claims['sub'] return verified_claims except google.auth.exceptions.TransportError as error: - raise CertificateFetchError(str(error), cause=error) + raise CertificateFetchError(str(error), cause=error) from error except ValueError as error: if 'Token expired' in str(error): raise self._expired_token_error(str(error), cause=error) diff --git a/firebase_admin/_user_import.py b/firebase_admin/_user_import.py index 659a68701..7c7a9e70b 100644 --- a/firebase_admin/_user_import.py +++ b/firebase_admin/_user_import.py @@ -216,10 +216,10 @@ def provider_data(self): def provider_data(self, provider_data): if provider_data is not None: try: - if any([not isinstance(p, UserProvider) for p in provider_data]): + if any(not isinstance(p, UserProvider) for p in provider_data): raise ValueError('One or more provider data instances are invalid.') - except TypeError: - raise ValueError('provider_data must be iterable.') + except TypeError as err: + raise ValueError('provider_data must be iterable.') from err self._provider_data = provider_data @property diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index aa0dfb0a4..957b749a6 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -128,7 +128,7 @@ class UserRecord(UserInfo): """Contains metadata associated with a Firebase user account.""" def __init__(self, data): - super(UserRecord, self).__init__() + super().__init__() if not isinstance(data, dict): raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data)) if not data.get('localId'): @@ -452,7 +452,7 @@ class ProviderUserInfo(UserInfo): """Contains metadata regarding how a user is known by a particular identity provider.""" def __init__(self, data): - super(ProviderUserInfo, self).__init__() + super().__init__() if not isinstance(data, dict): raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data)) if not data.get('rawId'): @@ -518,8 +518,8 @@ def encode_action_code_settings(settings): if not parsed.netloc: raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) parameters['continueUrl'] = settings.url - except Exception: - raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) + except Exception as err: + raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) from err # handle_code_in_app if settings.handle_code_in_app is not None: @@ -788,13 +788,13 @@ def import_users(self, users, hash_alg=None): raise ValueError( 'Users must be a non-empty list with no more than {0} elements.'.format( MAX_IMPORT_USERS_SIZE)) - if any([not isinstance(u, _user_import.ImportUserRecord) for u in users]): + if any(not isinstance(u, _user_import.ImportUserRecord) for u in users): raise ValueError('One or more user objects are invalid.') - except TypeError: - raise ValueError('users must be iterable') + except TypeError as err: + raise ValueError('users must be iterable') from err payload = {'users': [u.to_dict() for u in users]} - if any(['passwordHash' in u for u in payload['users']]): + if any('passwordHash' in u for u in payload['users']): if not isinstance(hash_alg, _user_import.UserImportHash): raise ValueError('A UserImportHash is required to import users with passwords.') payload.update(hash_alg.to_dict()) diff --git a/firebase_admin/app_check.py b/firebase_admin/app_check.py index 53686db3d..1224f7d80 100644 --- a/firebase_admin/app_check.py +++ b/firebase_admin/app_check.py @@ -84,7 +84,7 @@ def verify_token(self, token: str) -> Dict[str, Any]: except (InvalidTokenError, DecodeError) as exception: raise ValueError( f'Verifying App Check token failed. Error: {exception}' - ) + ) from exception verified_claims['app_id'] = verified_claims.get('sub') return verified_claims @@ -112,28 +112,28 @@ def _decode_and_verify(self, token: str, signing_key: str): algorithms=["RS256"], audience=self._scoped_project_id ) - except InvalidSignatureError: + except InvalidSignatureError as exception: raise ValueError( 'The provided App Check token has an invalid signature.' - ) - except InvalidAudienceError: + ) from exception + except InvalidAudienceError as exception: raise ValueError( 'The provided App Check token has an incorrect "aud" (audience) claim. ' f'Expected payload to include {self._scoped_project_id}.' - ) - except InvalidIssuerError: + ) from exception + except InvalidIssuerError as exception: raise ValueError( 'The provided App Check token has an incorrect "iss" (issuer) claim. ' f'Expected claim to include {self._APP_CHECK_ISSUER}' - ) - except ExpiredSignatureError: + ) from exception + except ExpiredSignatureError as exception: raise ValueError( 'The provided App Check token has expired.' - ) + ) from exception except InvalidTokenError as exception: raise ValueError( f'Decoding App Check token failed. Error: {exception}' - ) + ) from exception audience = payload.get('aud') if not isinstance(audience, list) or self._scoped_project_id not in audience: diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py index 750600280..8259c93b4 100644 --- a/firebase_admin/credentials.py +++ b/firebase_admin/credentials.py @@ -63,7 +63,7 @@ class _ExternalCredentials(Base): """A wrapper for google.auth.credentials.Credentials typed credential instances""" def __init__(self, credential: GoogleAuthCredentials): - super(_ExternalCredentials, self).__init__() + super().__init__() self._g_credential = credential def get_credential(self): @@ -92,7 +92,7 @@ def __init__(self, cert): IOError: If the specified certificate file doesn't exist or cannot be read. ValueError: If the specified certificate is invalid. """ - super(Certificate, self).__init__() + super().__init__() if _is_file_path(cert): with open(cert) as json_file: json_data = json.load(json_file) @@ -111,7 +111,7 @@ def __init__(self, cert): json_data, scopes=_scopes) except ValueError as error: raise ValueError('Failed to initialize a certificate credential. ' - 'Caused by: "{0}"'.format(error)) + 'Caused by: "{0}"'.format(error)) from error @property def project_id(self): @@ -142,7 +142,7 @@ def __init__(self): The credentials will be lazily initialized when get_credential() or project_id() is called. See those methods for possible errors raised. """ - super(ApplicationDefault, self).__init__() + super().__init__() self._g_credential = None # Will be lazily-loaded via _load_credential(). def get_credential(self): @@ -193,7 +193,7 @@ def __init__(self, refresh_token): IOError: If the specified file doesn't exist or cannot be read. ValueError: If the refresh token configuration is invalid. """ - super(RefreshToken, self).__init__() + super().__init__() if _is_file_path(refresh_token): with open(refresh_token) as json_file: json_data = json.load(json_file) diff --git a/firebase_admin/db.py b/firebase_admin/db.py index 1dec98653..fc69cbd83 100644 --- a/firebase_admin/db.py +++ b/firebase_admin/db.py @@ -926,7 +926,7 @@ def request(self, method, url, **kwargs): kwargs['params'] = query try: - return super(_Client, self).request(method, url, **kwargs) + return super().request(method, url, **kwargs) except requests.exceptions.RequestException as error: raise _Client.handle_rtdb_error(error) diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 0e3a55f49..5b2e48e80 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -451,7 +451,7 @@ def send_data(data): message_data = [self._message_data(message, dry_run) for message in messages] try: with concurrent.futures.ThreadPoolExecutor(max_workers=len(message_data)) as executor: - responses = [resp for resp in executor.map(send_data, message_data)] + responses = list(executor.map(send_data, message_data)) return BatchResponse(responses) except Exception as error: raise exceptions.UnknownError( @@ -573,6 +573,7 @@ def _build_fcm_error_requests(cls, error, message, error_dict): """Parses an error response from the FCM API and creates a FCM-specific exception if appropriate.""" exc_type = cls._build_fcm_error(error_dict) + # pylint: disable=not-callable return exc_type(message, cause=error, http_response=error.response) if exc_type else None @classmethod @@ -586,8 +587,10 @@ def _build_fcm_error_httpx( appropriate.""" exc_type = cls._build_fcm_error(error_dict) if isinstance(error, httpx.HTTPStatusError): + # pylint: disable=not-callable return exc_type( message, cause=error, http_response=error.response) if exc_type else None + # pylint: disable=not-callable return exc_type(message, cause=error) if exc_type else None @classmethod diff --git a/firebase_admin/ml.py b/firebase_admin/ml.py index 98bdbb56a..8cedc8482 100644 --- a/firebase_admin/ml.py +++ b/firebase_admin/ml.py @@ -721,7 +721,7 @@ def __init__(self, current_page): self._current_page = current_page self._index = 0 - def next(self): + def __next__(self): if self._index == len(self._current_page.models): if self._current_page.has_next_page: self._current_page = self._current_page.get_next_page() @@ -732,9 +732,6 @@ def next(self): return result raise StopIteration - def __next__(self): - return self.next() - def __iter__(self): return self diff --git a/firebase_admin/project_management.py b/firebase_admin/project_management.py index ed292b80f..9405c8318 100644 --- a/firebase_admin/project_management.py +++ b/firebase_admin/project_management.py @@ -338,7 +338,7 @@ class AndroidAppMetadata(_AppMetadata): def __init__(self, package_name, name, app_id, display_name, project_id): """Clients should not instantiate this class directly.""" - super(AndroidAppMetadata, self).__init__(name, app_id, display_name, project_id) + super().__init__(name, app_id, display_name, project_id) self._package_name = _check_is_nonempty_string(package_name, 'package_name') @property @@ -347,7 +347,7 @@ def package_name(self): return self._package_name def __eq__(self, other): - return (super(AndroidAppMetadata, self).__eq__(other) and + return (super().__eq__(other) and self.package_name == other.package_name) def __ne__(self, other): @@ -363,7 +363,7 @@ class IOSAppMetadata(_AppMetadata): def __init__(self, bundle_id, name, app_id, display_name, project_id): """Clients should not instantiate this class directly.""" - super(IOSAppMetadata, self).__init__(name, app_id, display_name, project_id) + super().__init__(name, app_id, display_name, project_id) self._bundle_id = _check_is_nonempty_string(bundle_id, 'bundle_id') @property @@ -372,7 +372,7 @@ def bundle_id(self): return self._bundle_id def __eq__(self, other): - return super(IOSAppMetadata, self).__eq__(other) and self.bundle_id == other.bundle_id + return super().__eq__(other) and self.bundle_id == other.bundle_id def __ne__(self, other): return not self.__eq__(other) diff --git a/firebase_admin/storage.py b/firebase_admin/storage.py index b6084842a..567a6abad 100644 --- a/firebase_admin/storage.py +++ b/firebase_admin/storage.py @@ -21,9 +21,9 @@ # pylint: disable=import-error,no-name-in-module try: from google.cloud import storage -except ImportError: +except ImportError as exception: raise ImportError('Failed to import the Cloud Storage library for Python. Make sure ' - 'to install the "google-cloud-storage" module.') + 'to install the "google-cloud-storage" module.') from exception from firebase_admin import _utils diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index 8c53e30a1..133e80b45 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -417,7 +417,7 @@ def __init__(self, current_page): self._current_page = current_page self._index = 0 - def next(self): + def __next__(self): if self._index == len(self._current_page.tenants): if self._current_page.has_next_page: self._current_page = self._current_page.get_next_page() @@ -428,9 +428,6 @@ def next(self): return result raise StopIteration - def __next__(self): - return self.next() - def __iter__(self): return self diff --git a/integration/conftest.py b/integration/conftest.py index efa45932d..169e02d5b 100644 --- a/integration/conftest.py +++ b/integration/conftest.py @@ -16,7 +16,6 @@ import json import pytest -from pytest_asyncio import is_async_test import firebase_admin from firebase_admin import credentials @@ -71,9 +70,3 @@ def api_key(request): 'command-line option.') with open(path) as keyfile: return keyfile.read().strip() - -def pytest_collection_modifyitems(items): - pytest_asyncio_tests = (item for item in items if is_async_test(item)) - session_scope_marker = pytest.mark.asyncio(loop_scope="session") - for async_test in pytest_asyncio_tests: - async_test.add_marker(session_scope_marker, append=False) diff --git a/integration/test_firestore_async.py b/integration/test_firestore_async.py index 8b73dda0f..584ef590a 100644 --- a/integration/test_firestore_async.py +++ b/integration/test_firestore_async.py @@ -34,7 +34,7 @@ } -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_firestore_async(): client = firestore_async.client() expected = _CITY @@ -48,7 +48,7 @@ async def test_firestore_async(): data = await doc.get() assert data.exists is False -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_firestore_async_explicit_database_id(): client = firestore_async.client(database_id='testing-database') expected = _CITY @@ -62,7 +62,7 @@ async def test_firestore_async_explicit_database_id(): data = await doc.get() assert data.exists is False -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_firestore_async_multi_db(): city_client = firestore_async.client() movie_client = firestore_async.client(database_id='testing-database') @@ -98,7 +98,7 @@ async def test_firestore_async_multi_db(): assert data[0].exists is False assert data[1].exists is False -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_server_timestamp(): client = firestore_async.client() expected = { diff --git a/integration/test_messaging.py b/integration/test_messaging.py index 804691962..7ab707c82 100644 --- a/integration/test_messaging.py +++ b/integration/test_messaging.py @@ -157,7 +157,7 @@ def test_unsubscribe(): resp = messaging.unsubscribe_from_topic(_REGISTRATION_TOKEN, 'mock-topic') assert resp.success_count + resp.failure_count == 1 -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_send_each_async(): messages = [ messaging.Message( @@ -189,7 +189,7 @@ async def test_send_each_async(): assert isinstance(response.exception, exceptions.InvalidArgumentError) assert response.message_id is None -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_send_each_async_500(): messages = [] for msg_number in range(500): @@ -206,7 +206,7 @@ async def test_send_each_async_500(): assert response.exception is None assert re.match('^projects/.*/messages/.*$', response.message_id) -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_send_each_for_multicast_async(): multicast = messaging.MulticastMessage( notification=messaging.Notification('Title', 'Body'), diff --git a/integration/test_ml.py b/integration/test_ml.py index 52cb1bb7e..f8dd6bb47 100644 --- a/integration/test_ml.py +++ b/integration/test_ml.py @@ -317,12 +317,16 @@ def _clean_up_directory(save_dir): @pytest.fixture def keras_model(): assert _TF_ENABLED - x_array = [-1, 0, 1, 2, 3, 4] - y_array = [-3, -1, 1, 3, 5, 7] - model = tf.keras.models.Sequential( - [tf.keras.layers.Dense(units=1, input_shape=[1])]) + x_list = [-1, 0, 1, 2, 3, 4] + y_list = [-3, -1, 1, 3, 5, 7] + x_tensor = tf.convert_to_tensor(x_list, dtype=tf.float32) + y_tensor = tf.convert_to_tensor(y_list, dtype=tf.float32) + model = tf.keras.models.Sequential([ + tf.keras.Input(shape=(1,)), + tf.keras.layers.Dense(units=1) + ]) model.compile(optimizer='sgd', loss='mean_squared_error') - model.fit(x_array, y_array, epochs=3) + model.fit(x_tensor, y_tensor, epochs=3) return model diff --git a/integration/test_storage.py b/integration/test_storage.py index 729190950..4f0faf76c 100644 --- a/integration/test_storage.py +++ b/integration/test_storage.py @@ -38,7 +38,7 @@ def _verify_bucket(bucket, expected_name): blob.upload_from_string('Hello World') blob = bucket.get_blob(file_name) - assert blob.download_as_string().decode() == 'Hello World' + assert blob.download_as_bytes().decode() == 'Hello World' bucket.delete_blob(file_name) assert not bucket.get_blob(file_name) diff --git a/requirements.txt b/requirements.txt index b5642b549..76eeb7582 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -astroid == 2.3.3 -pylint == 2.3.1 -pytest >= 6.2.0 +astroid == 2.5.8 +pylint == 2.7.4 +pytest >= 8.2.2 pytest-cov >= 2.4.0 pytest-localserver >= 0.4.1 -pytest-asyncio >= 0.16.0 +pytest-asyncio >= 0.26.0 pytest-mock >= 3.6.1 respx == 0.22.0 diff --git a/setup.cfg b/setup.cfg index 25c649748..32e00676b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,4 @@ [tool:pytest] testpaths = tests +asyncio_default_test_loop_scope = class +asyncio_default_fixture_loop_scope = None diff --git a/setup.py b/setup.py index b9eb11806..25cf12672 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ (major, minor) = (sys.version_info.major, sys.version_info.minor) if major != 3 or minor < 7: - print('firebase_admin requires python >= 3.7', file=sys.stderr) + print('firebase_admin requires python >= 3.9', file=sys.stderr) sys.exit(1) # Read in the package metadata per recommendations from: @@ -60,18 +60,17 @@ keywords='firebase cloud development', install_requires=install_requires, packages=['firebase_admin'], - python_requires='>=3.7', + python_requires='>=3.9', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'License :: OSI Approved :: Apache Software License', ], ) diff --git a/tests/test_db.py b/tests/test_db.py index 00a0077cb..93f4672f1 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -45,7 +45,7 @@ def __init__(self, data, status, recorder, etag=ETAG): def send(self, request, **kwargs): if_match = request.headers.get('if-match') if_none_match = request.headers.get('if-none-match') - resp = super(MockAdapter, self).send(request, **kwargs) + resp = super().send(request, **kwargs) resp.headers = {'ETag': self._etag} if if_match and if_match != MockAdapter.ETAG: resp.status_code = 412 diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 341fd9e07..63b649485 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -1881,8 +1881,8 @@ def test_send_each(self): assert batch_response.failure_count == 0 assert len(batch_response.responses) == 2 assert [r.message_id for r in batch_response.responses] == ['message-id1', 'message-id2'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) @respx.mock @pytest.mark.asyncio @@ -1907,8 +1907,8 @@ async def test_send_each_async(self): assert len(batch_response.responses) == 3 assert [r.message_id for r in batch_response.responses] \ == ['message-id1', 'message-id2', 'message-id3'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) assert route.call_count == 3 @@ -1976,8 +1976,8 @@ async def test_send_each_async_error_401_pass_on_auth_retry(self): assert batch_response.failure_count == 0 assert len(batch_response.responses) == 1 assert [r.message_id for r in batch_response.responses] == ['message-id1'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) @respx.mock @pytest.mark.asyncio @@ -2049,11 +2049,12 @@ async def test_send_each_async_error_500_pass_on_retry_config(self): assert batch_response.failure_count == 0 assert len(batch_response.responses) == 1 assert [r.message_id for r in batch_response.responses] == ['message-id1'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) + - @respx.mock @pytest.mark.asyncio + @respx.mock async def test_send_each_async_request_error(self): responses = httpx.ConnectError("Test request error", request=httpx.Request( 'POST', @@ -2192,8 +2193,8 @@ def test_send_each_for_multicast(self): assert batch_response.failure_count == 0 assert len(batch_response.responses) == 2 assert [r.message_id for r in batch_response.responses] == ['message-id1', 'message-id2'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) @pytest.mark.parametrize('status', HTTP_ERROR_CODES) def test_send_each_for_multicast_detailed_error(self, status): diff --git a/tests/test_ml.py b/tests/test_ml.py index 18a9e2754..4aebdcab6 100644 --- a/tests/test_ml.py +++ b/tests/test_ml.py @@ -1094,7 +1094,7 @@ def test_list_single_page(self): assert models_page.next_page_token == '' assert models_page.has_next_page is False assert models_page.get_next_page() is None - models = [model for model in models_page.iterate_all()] + models = list(models_page.iterate_all()) assert len(models) == 1 def test_list_multiple_pages(self): @@ -1140,7 +1140,7 @@ def test_list_models_stop_iteration(self): assert len(recorder) == 1 assert len(page.models) == 3 iterator = page.iterate_all() - models = [model for model in iterator] + models = list(iterator) assert len(page.models) == 3 with pytest.raises(StopIteration): next(iterator) @@ -1151,5 +1151,5 @@ def test_list_models_no_models(self): page = ml.list_models() assert len(recorder) == 1 assert len(page.models) == 0 - models = [model for model in page.iterate_all()] + models = list(page.iterate_all()) assert len(models) == 0 diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py index 8c6248e18..14b54838f 100644 --- a/tests/test_remote_config.py +++ b/tests/test_remote_config.py @@ -830,7 +830,7 @@ def __init__(self, data, status, recorder, etag=ETAG): self._etag = etag def send(self, request, **kwargs): - resp = super(MockAdapter, self).send(request, **kwargs) + resp = super().send(request, **kwargs) resp.headers = {'etag': self._etag} return resp diff --git a/tests/test_sseclient.py b/tests/test_sseclient.py index 70edcf0d0..2c523e36f 100644 --- a/tests/test_sseclient.py +++ b/tests/test_sseclient.py @@ -25,10 +25,10 @@ class MockSSEClientAdapter(testutils.MockAdapter): def __init__(self, payload, recorder): - super(MockSSEClientAdapter, self).__init__(payload, 200, recorder) + super().__init__(payload, 200, recorder) def send(self, request, **kwargs): - resp = super(MockSSEClientAdapter, self).send(request, **kwargs) + resp = super().send(request, **kwargs) resp.url = request.url resp.status_code = self.status resp.raw = io.BytesIO(self.data.encode()) diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 018892e3a..156846343 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -450,7 +450,7 @@ def test_list_single_page(self, tenant_mgt_app): assert page.next_page_token == '' assert page.has_next_page is False assert page.get_next_page() is None - tenants = [tenant for tenant in page.iterate_all()] + tenants = list(page.iterate_all()) assert len(tenants) == 2 self._assert_request(recorder) @@ -514,7 +514,7 @@ def test_list_tenants_stop_iteration(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, LIST_TENANTS_RESPONSE) page = tenant_mgt.list_tenants(app=tenant_mgt_app) iterator = page.iterate_all() - tenants = [tenant for tenant in iterator] + tenants = list(iterator) assert len(tenants) == 2 with pytest.raises(StopIteration): @@ -526,7 +526,7 @@ def test_list_tenants_no_tenants_response(self, tenant_mgt_app): _instrument_tenant_mgt(tenant_mgt_app, 200, json.dumps(response)) page = tenant_mgt.list_tenants(app=tenant_mgt_app) assert len(page.tenants) == 0 - tenants = [tenant for tenant in page.iterate_all()] + tenants = list(page.iterate_all()) assert len(tenants) == 0 def test_list_tenants_with_max_results(self, tenant_mgt_app): diff --git a/tests/testutils.py b/tests/testutils.py index 62f7bd9b5..0505eb6c7 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -183,7 +183,7 @@ def send(self, request, **kwargs): # pylint: disable=arguments-differ class MockAdapter(MockMultiRequestAdapter): """A mock HTTP adapter for the Python requests module.""" def __init__(self, data, status, recorder): - super(MockAdapter, self).__init__([data], [status], recorder) + super().__init__([data], [status], recorder) @property def status(self): From d8e2269d1cbc4e7b80aef9cc677e69ac98bcdd15 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:08:39 -0400 Subject: [PATCH 3/6] change(ml): Drop AutoML model support (#894) --- firebase_admin/ml.py | 54 ++--------------------------------- integration/test_ml.py | 64 +----------------------------------------- tests/test_ml.py | 63 ----------------------------------------- 3 files changed, 3 insertions(+), 178 deletions(-) diff --git a/firebase_admin/ml.py b/firebase_admin/ml.py index 8cedc8482..5fffbd836 100644 --- a/firebase_admin/ml.py +++ b/firebase_admin/ml.py @@ -24,7 +24,6 @@ import time import os from urllib import parse -import warnings import requests @@ -33,14 +32,14 @@ from firebase_admin import _utils from firebase_admin import exceptions -# pylint: disable=import-error,no-name-in-module +# pylint: disable=import-error,no-member try: from firebase_admin import storage _GCS_ENABLED = True except ImportError: _GCS_ENABLED = False -# pylint: disable=import-error,no-name-in-module +# pylint: disable=import-error,no-member try: import tensorflow as tf _TF_ENABLED = True @@ -54,9 +53,6 @@ _TAG_PATTERN = re.compile(r'^[A-Za-z0-9_-]{1,32}$') _GCS_TFLITE_URI_PATTERN = re.compile( r'^gs://(?P[a-z0-9_.-]{3,63})/(?P.+)$') -_AUTO_ML_MODEL_PATTERN = re.compile( - r'^projects/(?P[a-z0-9-]{6,30})/locations/(?P[^/]+)/' + - r'models/(?P[A-Za-z0-9]+)$') _RESOURCE_NAME_PATTERN = re.compile( r'^projects/(?P[a-z0-9-]{6,30})/models/(?P[A-Za-z0-9_-]{1,60})$') _OPERATION_NAME_PATTERN = re.compile( @@ -388,11 +384,6 @@ def _init_model_source(data): gcs_tflite_uri = data.pop('gcsTfliteUri', None) if gcs_tflite_uri: return TFLiteGCSModelSource(gcs_tflite_uri=gcs_tflite_uri) - auto_ml_model = data.pop('automlModel', None) - if auto_ml_model: - warnings.warn('AutoML model support is deprecated and will be removed in the next ' - 'major version.', DeprecationWarning) - return TFLiteAutoMlSource(auto_ml_model=auto_ml_model) return None @property @@ -606,42 +597,6 @@ def as_dict(self, for_upload=False): return {'gcsTfliteUri': self._gcs_tflite_uri} - -class TFLiteAutoMlSource(TFLiteModelSource): - """TFLite model source representing a tflite model created with AutoML. - - AutoML model support is deprecated and will be removed in the next major version. - """ - - def __init__(self, auto_ml_model, app=None): - warnings.warn('AutoML model support is deprecated and will be removed in the next ' - 'major version.', DeprecationWarning) - self._app = app - self.auto_ml_model = auto_ml_model - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.auto_ml_model == other.auto_ml_model - return False - - def __ne__(self, other): - return not self.__eq__(other) - - @property - def auto_ml_model(self): - """Resource name of the model, created by the AutoML API or Cloud console.""" - return self._auto_ml_model - - @auto_ml_model.setter - def auto_ml_model(self, auto_ml_model): - self._auto_ml_model = _validate_auto_ml_model(auto_ml_model) - - def as_dict(self, for_upload=False): - """Returns a serializable representation of the object.""" - # Upload is irrelevant for auto_ml models - return {'automlModel': self._auto_ml_model} - - class ListModelsPage: """Represents a page of models in a Firebase project. @@ -786,11 +741,6 @@ def _validate_gcs_tflite_uri(uri): raise ValueError('GCS TFLite URI format is invalid.') return uri -def _validate_auto_ml_model(model): - if not _AUTO_ML_MODEL_PATTERN.match(model): - raise ValueError('Model resource name format is invalid.') - return model - def _validate_model_format(model_format): if not isinstance(model_format, ModelFormat): diff --git a/integration/test_ml.py b/integration/test_ml.py index f8dd6bb47..6deb22a69 100644 --- a/integration/test_ml.py +++ b/integration/test_ml.py @@ -22,25 +22,18 @@ import pytest -import firebase_admin from firebase_admin import exceptions from firebase_admin import ml from tests import testutils -# pylint: disable=import-error,no-name-in-module +# pylint: disable=import-error, no-member try: import tensorflow as tf _TF_ENABLED = True except ImportError: _TF_ENABLED = False -try: - from google.cloud import automl_v1 - _AUTOML_ENABLED = True -except ImportError: - _AUTOML_ENABLED = False - def _random_identifier(prefix): #pylint: disable=unused-variable suffix = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(8)]) @@ -159,14 +152,6 @@ def check_tflite_gcs_format(model, validation_error=None): assert model.model_hash is not None -def check_tflite_automl_format(model): - assert model.validation_error is None - assert model.published is False - assert model.model_format.model_source.auto_ml_model.startswith('projects/') - # Automl models don't have validation errors since they are references - # to valid automl models. - - @pytest.mark.parametrize('firebase_model', [NAME_AND_TAGS_ARGS], indirect=True) def test_create_simple_model(firebase_model): check_model(firebase_model, NAME_AND_TAGS_ARGS) @@ -392,50 +377,3 @@ def test_from_saved_model(saved_model_dir): assert created_model.validation_error is None finally: _clean_up_model(created_model) - - -# Test AutoML functionality if AutoML is enabled. -#'pip install google-cloud-automl' in the environment if you want _AUTOML_ENABLED = True -# You will also need a predefined AutoML model named 'admin_sdk_integ_test1' to run the -# successful test. (Test is skipped otherwise) - -@pytest.fixture -def automl_model(): - assert _AUTOML_ENABLED - - # It takes > 20 minutes to train a model, so we expect a predefined AutoMl - # model named 'admin_sdk_integ_test1' to exist in the project, or we skip - # the test. - automl_client = automl_v1.AutoMlClient() - project_id = firebase_admin.get_app().project_id - parent = automl_client.location_path(project_id, 'us-central1') - models = automl_client.list_models(parent, filter_="display_name=admin_sdk_integ_test1") - # Expecting exactly one. (Ok to use last one if somehow more than 1) - automl_ref = None - for model in models: - automl_ref = model.name - - # Skip if no pre-defined model. (It takes min > 20 minutes to train a model) - if automl_ref is None: - pytest.skip("No pre-existing AutoML model found. Skipping test") - - source = ml.TFLiteAutoMlSource(automl_ref) - tflite_format = ml.TFLiteFormat(model_source=source) - ml_model = ml.Model( - display_name=_random_identifier('TestModel_automl_'), - tags=['test_automl'], - model_format=tflite_format) - model = ml.create_model(model=ml_model) - yield model - _clean_up_model(model) - -@pytest.mark.skipif(not _AUTOML_ENABLED, reason='AutoML is required for this test.') -def test_automl_model(automl_model): - # This test looks for a predefined automl model with display_name = 'admin_sdk_integ_test1' - automl_model.wait_for_unlocked() - - check_model(automl_model, { - 'display_name': automl_model.display_name, - 'tags': ['test_automl'], - }) - check_tflite_automl_format(automl_model) diff --git a/tests/test_ml.py b/tests/test_ml.py index 4aebdcab6..2af9ae42f 100644 --- a/tests/test_ml.py +++ b/tests/test_ml.py @@ -121,18 +121,6 @@ } TFLITE_FORMAT_2 = ml.TFLiteFormat.from_dict(TFLITE_FORMAT_JSON_2) -AUTOML_MODEL_NAME = 'projects/111111111111/locations/us-central1/models/ICN7683346839371803263' -AUTOML_MODEL_SOURCE = ml.TFLiteAutoMlSource(AUTOML_MODEL_NAME) -TFLITE_FORMAT_JSON_3 = { - 'automlModel': AUTOML_MODEL_NAME, - 'sizeBytes': '3456789' -} -TFLITE_FORMAT_3 = ml.TFLiteFormat.from_dict(TFLITE_FORMAT_JSON_3) - -AUTOML_MODEL_NAME_2 = 'projects/2222222222/locations/us-central1/models/ICN2222222222222222222' -AUTOML_MODEL_NAME_JSON_2 = {'automlModel': AUTOML_MODEL_NAME_2} -AUTOML_MODEL_SOURCE_2 = ml.TFLiteAutoMlSource(AUTOML_MODEL_NAME_2) - CREATED_UPDATED_MODEL_JSON_1 = { 'name': MODEL_NAME_1, 'displayName': DISPLAY_NAME_1, @@ -423,14 +411,6 @@ def test_model_keyword_based_creation_and_setters(self): 'tfliteModel': TFLITE_FORMAT_JSON_2 } - model.model_format = TFLITE_FORMAT_3 - assert model.as_dict() == { - 'displayName': DISPLAY_NAME_2, - 'tags': TAGS_2, - 'tfliteModel': TFLITE_FORMAT_JSON_3 - } - - def test_gcs_tflite_model_format_source_creation(self): model_source = ml.TFLiteGCSModelSource(gcs_tflite_uri=GCS_TFLITE_URI) model_format = ml.TFLiteFormat(model_source=model_source) @@ -442,17 +422,6 @@ def test_gcs_tflite_model_format_source_creation(self): } } - def test_auto_ml_tflite_model_format_source_creation(self): - model_source = ml.TFLiteAutoMlSource(auto_ml_model=AUTOML_MODEL_NAME) - model_format = ml.TFLiteFormat(model_source=model_source) - model = ml.Model(display_name=DISPLAY_NAME_1, model_format=model_format) - assert model.as_dict() == { - 'displayName': DISPLAY_NAME_1, - 'tfliteModel': { - 'automlModel': AUTOML_MODEL_NAME - } - } - def test_source_creation_from_tflite_file(self): model_source = ml.TFLiteGCSModelSource.from_tflite_model_file( "my_model.tflite", "my_bucket") @@ -466,13 +435,6 @@ def test_gcs_tflite_model_source_setters(self): assert model_source.gcs_tflite_uri == GCS_TFLITE_URI_2 assert model_source.as_dict() == GCS_TFLITE_URI_JSON_2 - def test_auto_ml_tflite_model_source_setters(self): - model_source = ml.TFLiteAutoMlSource(AUTOML_MODEL_NAME) - model_source.auto_ml_model = AUTOML_MODEL_NAME_2 - assert model_source.auto_ml_model == AUTOML_MODEL_NAME_2 - assert model_source.as_dict() == AUTOML_MODEL_NAME_JSON_2 - - def test_model_format_setters(self): model_format = ml.TFLiteFormat(model_source=GCS_TFLITE_MODEL_SOURCE) model_format.model_source = GCS_TFLITE_MODEL_SOURCE_2 @@ -483,14 +445,6 @@ def test_model_format_setters(self): } } - model_format.model_source = AUTOML_MODEL_SOURCE - assert model_format.model_source == AUTOML_MODEL_SOURCE - assert model_format.as_dict() == { - 'tfliteModel': { - 'automlModel': AUTOML_MODEL_NAME - } - } - def test_model_as_dict_for_upload(self): model_source = ml.TFLiteGCSModelSource(gcs_tflite_uri=GCS_TFLITE_URI) model_format = ml.TFLiteFormat(model_source=model_source) @@ -576,23 +530,6 @@ def test_gcs_tflite_source_validation_errors(self, uri, exc_type): ml.TFLiteGCSModelSource(gcs_tflite_uri=uri) check_error(excinfo, exc_type) - @pytest.mark.parametrize('auto_ml_model, exc_type', [ - (123, TypeError), - ('abc', ValueError), - ('/projects/123456/locations/us-central1/models/noLeadingSlash', ValueError), - ('projects/123546/models/ICN123456', ValueError), - ('projects//locations/us-central1/models/ICN123456', ValueError), - ('projects/123456/locations//models/ICN123456', ValueError), - ('projects/123456/locations/us-central1/models/', ValueError), - ('projects/ABC/locations/us-central1/models/ICN123456', ValueError), - ('projects/123456/locations/us-central1/models/@#$%^&', ValueError), - ('projects/123456/locations/us-cent/ral1/models/ICN123456', ValueError), - ]) - def test_auto_ml_tflite_source_validation_errors(self, auto_ml_model, exc_type): - with pytest.raises(exc_type) as excinfo: - ml.TFLiteAutoMlSource(auto_ml_model=auto_ml_model) - check_error(excinfo, exc_type) - def test_wait_for_unlocked_not_locked(self): model = ml.Model(display_name="not_locked") model.wait_for_unlocked() From c9dce80a787eb4e16f478c3f52f2c83dde090e9b Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:54:29 -0400 Subject: [PATCH 4/6] chore: Bump `pylint` to v3.3.7 and `astroid` to v3.3.10 (#895) * chore: Bump pylint to v3 * chore: fix src lint * chore: fix unit test lint * chore: fix integration test lint * chore: fix snippets lint * chore: 2nd pass for errors * fix: corrected use of the `bad-functions` config * fix: add EoF newline --- .pylintrc | 105 +++++++----------- firebase_admin/__init__.py | 47 ++++---- firebase_admin/_auth_client.py | 15 +-- firebase_admin/_auth_providers.py | 53 +++++---- firebase_admin/_auth_utils.py | 98 ++++++++--------- firebase_admin/_messaging_encoder.py | 52 ++++----- firebase_admin/_rfc3339.py | 2 +- firebase_admin/_sseclient.py | 9 +- firebase_admin/_token_gen.py | 113 ++++++++++---------- firebase_admin/_user_mgt.py | 57 +++++----- firebase_admin/_utils.py | 21 ++-- firebase_admin/app_check.py | 4 +- firebase_admin/credentials.py | 26 ++--- firebase_admin/db.py | 59 +++++----- firebase_admin/functions.py | 6 +- firebase_admin/instance_id.py | 6 +- firebase_admin/messaging.py | 32 +++--- firebase_admin/ml.py | 20 ++-- firebase_admin/project_management.py | 43 ++++---- firebase_admin/remote_config.py | 10 +- firebase_admin/storage.py | 4 +- firebase_admin/tenant_mgt.py | 31 +++--- integration/conftest.py | 8 +- integration/test_auth.py | 17 +-- integration/test_db.py | 14 +-- integration/test_firestore.py | 14 +-- integration/test_firestore_async.py | 10 +- integration/test_messaging.py | 4 +- integration/test_ml.py | 13 ++- integration/test_project_management.py | 11 +- integration/test_storage.py | 6 +- integration/test_tenant_mgt.py | 9 +- requirements.txt | 4 +- snippets/auth/get_service_account_tokens.py | 2 +- snippets/auth/index.py | 29 ++--- snippets/database/index.py | 12 +-- snippets/messaging/cloud_messaging.py | 6 +- tests/test_app.py | 14 +-- tests/test_app_check.py | 4 +- tests/test_auth_providers.py | 21 ++-- tests/test_credentials.py | 4 +- tests/test_db.py | 65 +++++------ tests/test_instance_id.py | 6 +- tests/test_messaging.py | 40 +++---- tests/test_ml.py | 64 ++++++----- tests/test_project_management.py | 6 +- tests/test_remote_config.py | 2 +- tests/test_storage.py | 2 +- tests/test_tenant_mgt.py | 75 +++++++------ tests/test_token_gen.py | 20 ++-- tests/test_user_mgt.py | 39 ++++--- tests/testutils.py | 2 +- 52 files changed, 648 insertions(+), 688 deletions(-) diff --git a/.pylintrc b/.pylintrc index 2155853c7..ea54e481c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,4 +1,4 @@ -[MASTER] +[MAIN] # Specify a configuration file. #rcfile= @@ -20,7 +20,9 @@ persistent=no # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins=pylint.extensions.docparams,pylint.extensions.docstyle +load-plugins=pylint.extensions.docparams, + pylint.extensions.docstyle, + pylint.extensions.bad_builtin, # Use multiple processes to speed up Pylint. jobs=1 @@ -34,15 +36,6 @@ unsafe-load-any-extension=no # run arbitrary code extension-pkg-whitelist= -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. This option is deprecated -# and it will be removed in Pylint 2.0. -optimize-ast=no - [MESSAGES CONTROL] @@ -65,21 +58,31 @@ enable=indexing-exception,old-raise-syntax # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,missing-type-doc +disable=design, + similarities, + no-self-use, + attribute-defined-outside-init, + locally-disabled, + star-args, + pointless-except, + bad-option-value, + lobal-statement, + fixme, + suppressed-message, + useless-suppression, + locally-enabled, + file-ignored, + missing-type-doc, + c-extension-no-member, [REPORTS] -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". This option is deprecated -# and it will be removed in Pylint 2.0. -files-output=no +# Set the output format. Available formats are: 'text', 'parseable', +# 'colorized', 'json2' (improved json format), 'json' (old json format), msvs +# (visual studio) and 'github' (GitHub actions). You can also give a reporter +# class, e.g. mypackage.mymodule.MyReporterClass. +output-format=colorized # Tells whether to display a full report or only the messages reports=no @@ -176,9 +179,12 @@ logging-modules=logging good-names=main,_ # Bad variable names which should always be refused, separated by a comma -bad-names= - -bad-functions=input,apply,reduce +bad-names=foo, + bar, + baz, + toto, + tutu, + tata # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. @@ -194,64 +200,33 @@ property-classes=abc.abstractproperty # Regular expression matching correct function names function-rgx=[a-z_][a-z0-9_]*$ -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]*$ - # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct constant names const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - # Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - # Regular expression matching correct method names method-rgx=[a-z_][a-z0-9_]*$ -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]*$ - # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=(__.*__|main) @@ -294,12 +269,6 @@ ignore-long-lines=^\s*(# )??$ # else. single-line-if-stmt=no -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - # Maximum number of lines in a module max-module-lines=1000 @@ -405,6 +374,12 @@ exclude-protected=_asdict,_fields,_replace,_source,_make [EXCEPTIONS] -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + +[DEPRECATED_BUILTINS] + +# List of builtins function names that should not be used, separated by a comma +bad-functions=input, + apply, + reduce diff --git a/firebase_admin/__init__.py b/firebase_admin/__init__.py index 597aaa6b6..8c9f628e5 100644 --- a/firebase_admin/__init__.py +++ b/firebase_admin/__init__.py @@ -79,11 +79,11 @@ def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME): 'apps, pass a second argument to initialize_app() to give each app ' 'a unique name.')) - raise ValueError(( - 'Firebase app named "{0}" already exists. This means you called ' + raise ValueError( + f'Firebase app named "{name}" already exists. This means you called ' 'initialize_app() more than once with the same app name as the ' 'second argument. Make sure you provide a unique name every time ' - 'you call initialize_app().').format(name)) + 'you call initialize_app().') def delete_app(app): @@ -96,8 +96,7 @@ def delete_app(app): ValueError: If the app is not initialized. """ if not isinstance(app, App): - raise ValueError('Illegal app argument type: "{}". Argument must be of ' - 'type App.'.format(type(app))) + raise ValueError(f'Illegal app argument type: "{type(app)}". Argument must be of type App.') with _apps_lock: if _apps.get(app.name) is app: del _apps[app.name] @@ -109,9 +108,9 @@ def delete_app(app): 'the default app by calling initialize_app().') raise ValueError( - ('Firebase app named "{0}" is not initialized. Make sure to initialize ' - 'the app by calling initialize_app() with your app name as the ' - 'second argument.').format(app.name)) + f'Firebase app named "{app.name}" is not initialized. Make sure to initialize ' + 'the app by calling initialize_app() with your app name as the ' + 'second argument.') def get_app(name=_DEFAULT_APP_NAME): @@ -128,8 +127,8 @@ def get_app(name=_DEFAULT_APP_NAME): app does not exist. """ if not isinstance(name, str): - raise ValueError('Illegal app name argument type: "{}". App name ' - 'must be a string.'.format(type(name))) + raise ValueError( + f'Illegal app name argument type: "{type(name)}". App name must be a string.') with _apps_lock: if name in _apps: return _apps[name] @@ -140,9 +139,9 @@ def get_app(name=_DEFAULT_APP_NAME): 'the SDK by calling initialize_app().') raise ValueError( - ('Firebase app named "{0}" does not exist. Make sure to initialize ' - 'the SDK by calling initialize_app() with your app name as the ' - 'second argument.').format(name)) + f'Firebase app named "{name}" does not exist. Make sure to initialize ' + 'the SDK by calling initialize_app() with your app name as the ' + 'second argument.') class _AppOptions: @@ -153,8 +152,9 @@ def __init__(self, options): options = self._load_from_environment() if not isinstance(options, dict): - raise ValueError('Illegal Firebase app options type: {0}. Options ' - 'must be a dictionary.'.format(type(options))) + raise ValueError( + f'Illegal Firebase app options type: {type(options)}. ' + 'Options must be a dictionary.') self._options = options def get(self, key, default=None): @@ -175,15 +175,15 @@ def _load_from_environment(self): json_str = config_file else: try: - with open(config_file, 'r') as json_file: + with open(config_file, 'r', encoding='utf-8') as json_file: json_str = json_file.read() except Exception as err: - raise ValueError('Unable to read file {}. {}'.format(config_file, err)) from err + raise ValueError(f'Unable to read file {config_file}. {err}') from err try: json_data = json.loads(json_str) except Exception as err: raise ValueError( - 'JSON string "{0}" is not valid json. {1}'.format(json_str, err)) from err + f'JSON string "{json_str}" is not valid json. {err}') from err return {k: v for k, v in json_data.items() if k in _CONFIG_VALID_KEYS} @@ -206,8 +206,9 @@ def __init__(self, name, credential, options): ValueError: If an argument is None or invalid. """ if not name or not isinstance(name, str): - raise ValueError('Illegal Firebase app name "{0}" provided. App name must be a ' - 'non-empty string.'.format(name)) + raise ValueError( + f'Illegal Firebase app name "{name}" provided. App name must be a ' + 'non-empty string.') self._name = name if isinstance(credential, GoogleAuthCredentials): @@ -228,7 +229,7 @@ def __init__(self, name, credential, options): def _validate_project_id(cls, project_id): if project_id is not None and not isinstance(project_id, str): raise ValueError( - 'Invalid project ID: "{0}". project ID must be a string.'.format(project_id)) + f'Invalid project ID: "{project_id}". project ID must be a string.') @property def name(self): @@ -293,11 +294,11 @@ def _get_service(self, name, initializer): """ if not name or not isinstance(name, str): raise ValueError( - 'Illegal name argument: "{0}". Name must be a non-empty string.'.format(name)) + f'Illegal name argument: "{name}". Name must be a non-empty string.') with self._lock: if self._services is None: raise ValueError( - 'Service requested from deleted Firebase App: "{0}".'.format(self._name)) + f'Service requested from deleted Firebase App: "{self._name}".') if name not in self._services: self._services[name] = initializer(self) return self._services[name] diff --git a/firebase_admin/_auth_client.py b/firebase_admin/_auth_client.py index 38b42993a..74261fa37 100644 --- a/firebase_admin/_auth_client.py +++ b/firebase_admin/_auth_client.py @@ -38,7 +38,7 @@ def __init__(self, app, tenant_id=None): 3. set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.""") credential = None - version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__) + version_header = f'Python/Admin/{firebase_admin.__version__}' timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS) # Non-default endpoint URLs for emulator support are set in this dict later. endpoint_urls = {} @@ -48,7 +48,7 @@ def __init__(self, app, tenant_id=None): # endpoint URLs to use the emulator. Additionally, use a fake credential. emulator_host = _auth_utils.get_emulator_host() if emulator_host: - base_url = 'http://{0}/identitytoolkit.googleapis.com'.format(emulator_host) + base_url = f'http://{emulator_host}/identitytoolkit.googleapis.com' endpoint_urls['v1'] = base_url + '/v1' endpoint_urls['v2'] = base_url + '/v2' credential = _utils.EmulatorAdminCredentials() @@ -123,15 +123,16 @@ def verify_id_token(self, id_token, check_revoked=False, clock_skew_seconds=0): """ if not isinstance(check_revoked, bool): # guard against accidental wrong assignment. - raise ValueError('Illegal check_revoked argument. Argument must be of type ' - ' bool, but given "{0}".'.format(type(check_revoked))) + raise ValueError( + 'Illegal check_revoked argument. Argument must be of type bool, but given ' + f'"{type(check_revoked)}".') verified_claims = self._token_verifier.verify_id_token(id_token, clock_skew_seconds) if self.tenant_id: token_tenant_id = verified_claims.get('firebase', {}).get('tenant') if self.tenant_id != token_tenant_id: raise _auth_utils.TenantIdMismatchError( - 'Invalid tenant ID: {0}'.format(token_tenant_id)) + f'Invalid tenant ID: {token_tenant_id}') if check_revoked: self._check_jwt_revoked_or_disabled( @@ -249,7 +250,7 @@ def _matches(identifier, user_record): if identifier.provider_id == user_info.provider_id and identifier.provider_uid == user_info.uid ), False) - raise TypeError("Unexpected type: {}".format(type(identifier))) + raise TypeError(f"Unexpected type: {type(identifier)}") def _is_user_found(identifier, user_records): return any(_matches(identifier, user_record) for user_record in user_records) @@ -757,4 +758,4 @@ def _check_jwt_revoked_or_disabled(self, verified_claims, exc_type, label): if user.disabled: raise _auth_utils.UserDisabledError('The user record is disabled.') if verified_claims.get('iat') * 1000 < user.tokens_valid_after_timestamp: - raise exc_type('The Firebase {0} has been revoked.'.format(label)) + raise exc_type(f'The Firebase {label} has been revoked.') diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 6512a4f7b..cc7949526 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -181,13 +181,13 @@ class ProviderConfigClient: def __init__(self, http_client, project_id, tenant_id=None, url_override=None): self.http_client = http_client url_prefix = url_override or self.PROVIDER_CONFIG_URL - self.base_url = '{0}/projects/{1}'.format(url_prefix, project_id) + self.base_url = f'{url_prefix}/projects/{project_id}' if tenant_id: - self.base_url += '/tenants/{0}'.format(tenant_id) + self.base_url += f'/tenants/{tenant_id}' def get_oidc_provider_config(self, provider_id): _validate_oidc_provider_id(provider_id) - body = self._make_request('get', '/oauthIdpConfigs/{0}'.format(provider_id)) + body = self._make_request('get', f'/oauthIdpConfigs/{provider_id}') return OIDCProviderConfig(body) def create_oidc_provider_config( @@ -218,7 +218,7 @@ def create_oidc_provider_config( if response_type: req['responseType'] = response_type - params = 'oauthIdpConfigId={0}'.format(provider_id) + params = f'oauthIdpConfigId={provider_id}' body = self._make_request('post', '/oauthIdpConfigs', json=req, params=params) return OIDCProviderConfig(body) @@ -259,14 +259,14 @@ def update_oidc_provider_config( raise ValueError('At least one parameter must be specified for update.') update_mask = _auth_utils.build_update_mask(req) - params = 'updateMask={0}'.format(','.join(update_mask)) - url = '/oauthIdpConfigs/{0}'.format(provider_id) + params = f'updateMask={",".join(update_mask)}' + url = f'/oauthIdpConfigs/{provider_id}' body = self._make_request('patch', url, json=req, params=params) return OIDCProviderConfig(body) def delete_oidc_provider_config(self, provider_id): _validate_oidc_provider_id(provider_id) - self._make_request('delete', '/oauthIdpConfigs/{0}'.format(provider_id)) + self._make_request('delete', f'/oauthIdpConfigs/{provider_id}') def list_oidc_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS): return _ListOIDCProviderConfigsPage( @@ -277,7 +277,7 @@ def _fetch_oidc_provider_configs(self, page_token=None, max_results=MAX_LIST_CON def get_saml_provider_config(self, provider_id): _validate_saml_provider_id(provider_id) - body = self._make_request('get', '/inboundSamlConfigs/{0}'.format(provider_id)) + body = self._make_request('get', f'/inboundSamlConfigs/{provider_id}') return SAMLProviderConfig(body) def create_saml_provider_config( @@ -301,7 +301,7 @@ def create_saml_provider_config( if enabled is not None: req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled') - params = 'inboundSamlConfigId={0}'.format(provider_id) + params = f'inboundSamlConfigId={provider_id}' body = self._make_request('post', '/inboundSamlConfigs', json=req, params=params) return SAMLProviderConfig(body) @@ -341,14 +341,14 @@ def update_saml_provider_config( raise ValueError('At least one parameter must be specified for update.') update_mask = _auth_utils.build_update_mask(req) - params = 'updateMask={0}'.format(','.join(update_mask)) - url = '/inboundSamlConfigs/{0}'.format(provider_id) + params = f'updateMask={",".join(update_mask)}' + url = f'/inboundSamlConfigs/{provider_id}' body = self._make_request('patch', url, json=req, params=params) return SAMLProviderConfig(body) def delete_saml_provider_config(self, provider_id): _validate_saml_provider_id(provider_id) - self._make_request('delete', '/inboundSamlConfigs/{0}'.format(provider_id)) + self._make_request('delete', f'/inboundSamlConfigs/{provider_id}') def list_saml_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS): return _ListSAMLProviderConfigsPage( @@ -367,15 +367,15 @@ def _fetch_provider_configs(self, path, page_token=None, max_results=MAX_LIST_CO if max_results < 1 or max_results > MAX_LIST_CONFIGS_RESULTS: raise ValueError( 'Max results must be a positive integer less than or equal to ' - '{0}.'.format(MAX_LIST_CONFIGS_RESULTS)) + f'{MAX_LIST_CONFIGS_RESULTS}.') - params = 'pageSize={0}'.format(max_results) + params = f'pageSize={max_results}' if page_token: - params += '&pageToken={0}'.format(page_token) + params += f'&pageToken={page_token}' return self._make_request('get', path, params=params) def _make_request(self, method, path, **kwargs): - url = '{0}{1}'.format(self.base_url, path) + url = f'{self.base_url}{path}' try: return self.http_client.body(method, url, **kwargs) except requests.exceptions.RequestException as error: @@ -385,29 +385,27 @@ def _make_request(self, method, path, **kwargs): def _validate_oidc_provider_id(provider_id): if not isinstance(provider_id, str): raise ValueError( - 'Invalid OIDC provider ID: {0}. Provider ID must be a non-empty string.'.format( - provider_id)) + f'Invalid OIDC provider ID: {provider_id}. Provider ID must be a non-empty string.') if not provider_id.startswith('oidc.'): - raise ValueError('Invalid OIDC provider ID: {0}.'.format(provider_id)) + raise ValueError(f'Invalid OIDC provider ID: {provider_id}.') return provider_id def _validate_saml_provider_id(provider_id): if not isinstance(provider_id, str): raise ValueError( - 'Invalid SAML provider ID: {0}. Provider ID must be a non-empty string.'.format( - provider_id)) + f'Invalid SAML provider ID: {provider_id}. Provider ID must be a non-empty string.') if not provider_id.startswith('saml.'): - raise ValueError('Invalid SAML provider ID: {0}.'.format(provider_id)) + raise ValueError(f'Invalid SAML provider ID: {provider_id}.') return provider_id def _validate_non_empty_string(value, label): """Validates that the given value is a non-empty string.""" if not isinstance(value, str): - raise ValueError('Invalid type for {0}: {1}.'.format(label, value)) + raise ValueError(f'Invalid type for {label}: {value}.') if not value: - raise ValueError('{0} must not be empty.'.format(label)) + raise ValueError(f'{label} must not be empty.') return value @@ -415,15 +413,14 @@ def _validate_url(url, label): """Validates that the given value is a well-formed URL string.""" if not isinstance(url, str) or not url: raise ValueError( - 'Invalid photo URL: "{0}". {1} must be a non-empty ' - 'string.'.format(url, label)) + f'Invalid photo URL: "{url}". {label} must be a non-empty string.') try: parsed = parse.urlparse(url) if not parsed.netloc: - raise ValueError('Malformed {0}: "{1}".'.format(label, url)) + raise ValueError(f'Malformed {label}: "{url}".') return url except Exception as exception: - raise ValueError('Malformed {0}: "{1}".'.format(label, url)) from exception + raise ValueError(f'Malformed {label}: "{url}".') from exception def _validate_x509_certificates(x509_certificates): diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index 0d56ca7fa..60d411822 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -74,8 +74,8 @@ def get_emulator_host(): emulator_host = os.getenv(EMULATOR_HOST_ENV_VAR, '') if emulator_host and '//' in emulator_host: raise ValueError( - 'Invalid {0}: "{1}". It must follow format "host:port".'.format( - EMULATOR_HOST_ENV_VAR, emulator_host)) + f'Invalid {EMULATOR_HOST_ENV_VAR}: "{emulator_host}". ' + 'It must follow format "host:port".') return emulator_host @@ -88,8 +88,8 @@ def validate_uid(uid, required=False): return None if not isinstance(uid, str) or not uid or len(uid) > 128: raise ValueError( - 'Invalid uid: "{0}". The uid must be a non-empty string with no more than 128 ' - 'characters.'.format(uid)) + f'Invalid uid: "{uid}". The uid must be a non-empty string with no more than 128 ' + 'characters.') return uid def validate_email(email, required=False): @@ -97,10 +97,10 @@ def validate_email(email, required=False): return None if not isinstance(email, str) or not email: raise ValueError( - 'Invalid email: "{0}". Email must be a non-empty string.'.format(email)) + f'Invalid email: "{email}". Email must be a non-empty string.') parts = email.split('@') if len(parts) != 2 or not parts[0] or not parts[1]: - raise ValueError('Malformed email address string: "{0}".'.format(email)) + raise ValueError(f'Malformed email address string: "{email}".') return email def validate_phone(phone, required=False): @@ -113,11 +113,12 @@ def validate_phone(phone, required=False): if phone is None and not required: return None if not isinstance(phone, str) or not phone: - raise ValueError('Invalid phone number: "{0}". Phone number must be a non-empty ' - 'string.'.format(phone)) + raise ValueError( + f'Invalid phone number: "{phone}". Phone number must be a non-empty string.') if not phone.startswith('+') or not re.search('[a-zA-Z0-9]', phone): - raise ValueError('Invalid phone number: "{0}". Phone number must be a valid, E.164 ' - 'compliant identifier.'.format(phone)) + raise ValueError( + f'Invalid phone number: "{phone}". Phone number must be a valid, E.164 ' + 'compliant identifier.') return phone def validate_password(password, required=False): @@ -132,7 +133,7 @@ def validate_bytes(value, label, required=False): if value is None and not required: return None if not isinstance(value, bytes) or not value: - raise ValueError('{0} must be a non-empty byte sequence.'.format(label)) + raise ValueError(f'{label} must be a non-empty byte sequence.') return value def validate_display_name(display_name, required=False): @@ -140,8 +141,8 @@ def validate_display_name(display_name, required=False): return None if not isinstance(display_name, str) or not display_name: raise ValueError( - 'Invalid display name: "{0}". Display name must be a non-empty ' - 'string.'.format(display_name)) + f'Invalid display name: "{display_name}". Display name must be a non-empty ' + 'string.') return display_name def validate_provider_id(provider_id, required=True): @@ -149,8 +150,7 @@ def validate_provider_id(provider_id, required=True): return None if not isinstance(provider_id, str) or not provider_id: raise ValueError( - 'Invalid provider ID: "{0}". Provider ID must be a non-empty ' - 'string.'.format(provider_id)) + f'Invalid provider ID: "{provider_id}". Provider ID must be a non-empty string.') return provider_id def validate_provider_uid(provider_uid, required=True): @@ -158,8 +158,7 @@ def validate_provider_uid(provider_uid, required=True): return None if not isinstance(provider_uid, str) or not provider_uid: raise ValueError( - 'Invalid provider UID: "{0}". Provider UID must be a non-empty ' - 'string.'.format(provider_uid)) + f'Invalid provider UID: "{provider_uid}". Provider UID must be a non-empty string.') return provider_uid def validate_photo_url(photo_url, required=False): @@ -168,15 +167,14 @@ def validate_photo_url(photo_url, required=False): return None if not isinstance(photo_url, str) or not photo_url: raise ValueError( - 'Invalid photo URL: "{0}". Photo URL must be a non-empty ' - 'string.'.format(photo_url)) + f'Invalid photo URL: "{photo_url}". Photo URL must be a non-empty string.') try: parsed = parse.urlparse(photo_url) if not parsed.netloc: - raise ValueError('Malformed photo URL: "{0}".'.format(photo_url)) + raise ValueError(f'Malformed photo URL: "{photo_url}".') return photo_url except Exception as err: - raise ValueError('Malformed photo URL: "{0}".'.format(photo_url)) from err + raise ValueError(f'Malformed photo URL: "{photo_url}".') from err def validate_timestamp(timestamp, label, required=False): """Validates the given timestamp value. Timestamps must be positive integers.""" @@ -187,13 +185,12 @@ def validate_timestamp(timestamp, label, required=False): try: timestamp_int = int(timestamp) except TypeError as err: - raise ValueError('Invalid type for timestamp value: {0}.'.format(timestamp)) from err - else: - if timestamp_int != timestamp: - raise ValueError('{0} must be a numeric value and a whole number.'.format(label)) - if timestamp_int <= 0: - raise ValueError('{0} timestamp must be a positive interger.'.format(label)) - return timestamp_int + raise ValueError(f'Invalid type for timestamp value: {timestamp}.') from err + if timestamp_int != timestamp: + raise ValueError(f'{label} must be a numeric value and a whole number.') + if timestamp_int <= 0: + raise ValueError(f'{label} timestamp must be a positive interger.') + return timestamp_int def validate_int(value, label, low=None, high=None): """Validates that the given value represents an integer. @@ -204,31 +201,30 @@ def validate_int(value, label, low=None, high=None): a developer error. """ if value is None or isinstance(value, bool): - raise ValueError('Invalid type for integer value: {0}.'.format(value)) + raise ValueError(f'Invalid type for integer value: {value}.') try: val_int = int(value) except TypeError as err: - raise ValueError('Invalid type for integer value: {0}.'.format(value)) from err - else: - if val_int != value: - # This will be True for non-numeric values like '2' and non-whole numbers like 2.5. - raise ValueError('{0} must be a numeric value and a whole number.'.format(label)) - if low is not None and val_int < low: - raise ValueError('{0} must not be smaller than {1}.'.format(label, low)) - if high is not None and val_int > high: - raise ValueError('{0} must not be larger than {1}.'.format(label, high)) - return val_int + raise ValueError(f'Invalid type for integer value: {value}.') from err + if val_int != value: + # This will be True for non-numeric values like '2' and non-whole numbers like 2.5. + raise ValueError(f'{label} must be a numeric value and a whole number.') + if low is not None and val_int < low: + raise ValueError(f'{label} must not be smaller than {low}.') + if high is not None and val_int > high: + raise ValueError(f'{label} must not be larger than {high}.') + return val_int def validate_string(value, label): """Validates that the given value is a string.""" if not isinstance(value, str): - raise ValueError('Invalid type for {0}: {1}.'.format(label, value)) + raise ValueError(f'Invalid type for {label}: {value}.') return value def validate_boolean(value, label): """Validates that the given value is a boolean.""" if not isinstance(value, bool): - raise ValueError('Invalid type for {0}: {1}.'.format(label, value)) + raise ValueError(f'Invalid type for {label}: {value}.') return value def validate_custom_claims(custom_claims, required=False): @@ -242,8 +238,7 @@ def validate_custom_claims(custom_claims, required=False): claims_str = str(custom_claims) if len(claims_str) > MAX_CLAIMS_PAYLOAD_SIZE: raise ValueError( - 'Custom claims payload must not exceed {0} characters.'.format( - MAX_CLAIMS_PAYLOAD_SIZE)) + f'Custom claims payload must not exceed {MAX_CLAIMS_PAYLOAD_SIZE} characters.') try: parsed = json.loads(claims_str) except Exception as err: @@ -254,16 +249,17 @@ def validate_custom_claims(custom_claims, required=False): invalid_claims = RESERVED_CLAIMS.intersection(set(parsed.keys())) if len(invalid_claims) > 1: joined = ', '.join(sorted(invalid_claims)) - raise ValueError('Claims "{0}" are reserved, and must not be set.'.format(joined)) + raise ValueError(f'Claims "{joined}" are reserved, and must not be set.') if len(invalid_claims) == 1: raise ValueError( - 'Claim "{0}" is reserved, and must not be set.'.format(invalid_claims.pop())) + f'Claim "{invalid_claims.pop()}" is reserved, and must not be set.') return claims_str def validate_action_type(action_type): if action_type not in VALID_EMAIL_ACTION_TYPES: - raise ValueError('Invalid action type provided action_type: {0}. \ - Valid values are {1}'.format(action_type, ', '.join(VALID_EMAIL_ACTION_TYPES))) + raise ValueError( + f'Invalid action type provided action_type: {action_type}. Valid values are ' + f'{", ".join(VALID_EMAIL_ACTION_TYPES)}') return action_type def validate_provider_ids(provider_ids, required=False): @@ -282,7 +278,7 @@ def build_update_mask(params): if isinstance(value, dict): child_mask = build_update_mask(value) for child in child_mask: - mask.append('{0}.{1}'.format(key, child)) + mask.append(f'{key}.{child}') else: mask.append(key) @@ -443,7 +439,7 @@ def handle_auth_backend_error(error): code, custom_message = _parse_error_body(error.response) if not code: - msg = 'Unexpected error response: {0}'.format(error.response.content.decode()) + msg = f'Unexpected error response: {error.response.content.decode()}' return _utils.handle_requests_error(error, message=msg) exc_type = _CODE_TO_EXC_TYPE.get(code) @@ -479,5 +475,5 @@ def _parse_error_body(response): def _build_error_message(code, exc_type, custom_message): default_message = exc_type.default_message if ( exc_type and hasattr(exc_type, 'default_message')) else 'Error while calling Auth service' - ext = ' {0}'.format(custom_message) if custom_message else '' - return '{0} ({1}).{2}'.format(default_message, code, ext) + ext = f' {custom_message}' if custom_message else '' + return f'{default_message} ({code}).{ext}' diff --git a/firebase_admin/_messaging_encoder.py b/firebase_admin/_messaging_encoder.py index 32f97875e..960a6d742 100644 --- a/firebase_admin/_messaging_encoder.py +++ b/firebase_admin/_messaging_encoder.py @@ -20,7 +20,7 @@ import numbers import re -import firebase_admin._messaging_utils as _messaging_utils +from firebase_admin import _messaging_utils class Message: @@ -99,10 +99,10 @@ def check_string(cls, label, value, non_empty=False): return None if not isinstance(value, str): if non_empty: - raise ValueError('{0} must be a non-empty string.'.format(label)) - raise ValueError('{0} must be a string.'.format(label)) + raise ValueError(f'{label} must be a non-empty string.') + raise ValueError(f'{label} must be a string.') if non_empty and not value: - raise ValueError('{0} must be a non-empty string.'.format(label)) + raise ValueError(f'{label} must be a non-empty string.') return value @classmethod @@ -110,7 +110,7 @@ def check_number(cls, label, value): if value is None: return None if not isinstance(value, numbers.Number): - raise ValueError('{0} must be a number.'.format(label)) + raise ValueError(f'{label} must be a number.') return value @classmethod @@ -119,13 +119,13 @@ def check_string_dict(cls, label, value): if value is None or value == {}: return None if not isinstance(value, dict): - raise ValueError('{0} must be a dictionary.'.format(label)) + raise ValueError(f'{label} must be a dictionary.') non_str = [k for k in value if not isinstance(k, str)] if non_str: - raise ValueError('{0} must not contain non-string keys.'.format(label)) + raise ValueError(f'{label} must not contain non-string keys.') non_str = [v for v in value.values() if not isinstance(v, str)] if non_str: - raise ValueError('{0} must not contain non-string values.'.format(label)) + raise ValueError(f'{label} must not contain non-string values.') return value @classmethod @@ -134,10 +134,10 @@ def check_string_list(cls, label, value): if value is None or value == []: return None if not isinstance(value, list): - raise ValueError('{0} must be a list of strings.'.format(label)) + raise ValueError(f'{label} must be a list of strings.') non_str = [k for k in value if not isinstance(k, str)] if non_str: - raise ValueError('{0} must not contain non-string values.'.format(label)) + raise ValueError(f'{label} must not contain non-string values.') return value @classmethod @@ -146,10 +146,10 @@ def check_number_list(cls, label, value): if value is None or value == []: return None if not isinstance(value, list): - raise ValueError('{0} must be a list of numbers.'.format(label)) + raise ValueError(f'{label} must be a list of numbers.') non_number = [k for k in value if not isinstance(k, numbers.Number)] if non_number: - raise ValueError('{0} must not contain non-number values.'.format(label)) + raise ValueError(f'{label} must not contain non-number values.') return value @classmethod @@ -157,7 +157,7 @@ def check_analytics_label(cls, label, value): """Checks if the given value is a valid analytics label.""" value = _Validators.check_string(label, value) if value is not None and not re.match(r'^[a-zA-Z0-9-_.~%]{1,50}$', value): - raise ValueError('Malformed {}.'.format(label)) + raise ValueError(f'Malformed {label}.') return value @classmethod @@ -166,7 +166,7 @@ def check_boolean(cls, label, value): if value is None: return None if not isinstance(value, bool): - raise ValueError('{0} must be a boolean.'.format(label)) + raise ValueError(f'{label} must be a boolean.') return value @classmethod @@ -175,7 +175,7 @@ def check_datetime(cls, label, value): if value is None: return None if not isinstance(value, datetime.datetime): - raise ValueError('{0} must be a datetime.'.format(label)) + raise ValueError(f'{label} must be a datetime.') return value @@ -245,8 +245,8 @@ def encode_ttl(cls, ttl): seconds = int(math.floor(total_seconds)) nanos = int((total_seconds - seconds) * 1e9) if nanos: - return '{0}.{1}s'.format(seconds, str(nanos).zfill(9)) - return '{0}s'.format(seconds) + return f'{seconds}.{str(nanos).zfill(9)}s' + return f'{seconds}s' @classmethod def encode_milliseconds(cls, label, msec): @@ -256,16 +256,16 @@ def encode_milliseconds(cls, label, msec): if isinstance(msec, numbers.Number): msec = datetime.timedelta(milliseconds=msec) if not isinstance(msec, datetime.timedelta): - raise ValueError('{0} must be a duration in milliseconds or an instance of ' - 'datetime.timedelta.'.format(label)) + raise ValueError( + f'{label} must be a duration in milliseconds or an instance of datetime.timedelta.') total_seconds = msec.total_seconds() if total_seconds < 0: - raise ValueError('{0} must not be negative.'.format(label)) + raise ValueError(f'{label} must not be negative.') seconds = int(math.floor(total_seconds)) nanos = int((total_seconds - seconds) * 1e9) if nanos: - return '{0}.{1}s'.format(seconds, str(nanos).zfill(9)) - return '{0}s'.format(seconds) + return f'{seconds}.{str(nanos).zfill(9)}s' + return f'{seconds}s' @classmethod def encode_android_notification(cls, notification): @@ -409,7 +409,7 @@ def encode_light_settings(cls, light_settings): raise ValueError( 'LightSettings.color must be in the form #RRGGBB or #RRGGBBAA.') if len(color) == 7: - color = (color+'FF') + color = color+'FF' rgba = [int(color[i:i + 2], 16) / 255.0 for i in (1, 3, 5, 7)] result['color'] = {'red': rgba[0], 'green': rgba[1], 'blue': rgba[2], 'alpha': rgba[3]} @@ -475,7 +475,7 @@ def encode_webpush_notification(cls, notification): for key, value in notification.custom_data.items(): if key in result: raise ValueError( - 'Multiple specifications for {0} in WebpushNotification.'.format(key)) + f'Multiple specifications for {key} in WebpushNotification.') result[key] = value return cls.remove_null_values(result) @@ -585,7 +585,7 @@ def encode_aps(cls, aps): for key, val in aps.custom_data.items(): _Validators.check_string('Aps.custom_data key', key) if key in result: - raise ValueError('Multiple specifications for {0} in Aps.'.format(key)) + raise ValueError(f'Multiple specifications for {key} in Aps.') result[key] = val return cls.remove_null_values(result) @@ -698,7 +698,7 @@ def default(self, o): # pylint: disable=method-hidden } result['topic'] = MessageEncoder.sanitize_topic_name(result.get('topic')) result = MessageEncoder.remove_null_values(result) - target_count = sum([t in result for t in ['token', 'topic', 'condition']]) + target_count = sum(t in result for t in ['token', 'topic', 'condition']) if target_count != 1: raise ValueError('Exactly one of token, topic or condition must be specified.') return result diff --git a/firebase_admin/_rfc3339.py b/firebase_admin/_rfc3339.py index 2c720bdd1..8489bdcb9 100644 --- a/firebase_admin/_rfc3339.py +++ b/firebase_admin/_rfc3339.py @@ -84,4 +84,4 @@ def _parse_to_datetime(datestr): except ValueError: pass - raise ValueError('time data {0} does not match RFC3339 format'.format(datestr)) + raise ValueError(f'time data {datestr} does not match RFC3339 format') diff --git a/firebase_admin/_sseclient.py b/firebase_admin/_sseclient.py index ec20cb45c..3372fe5f2 100644 --- a/firebase_admin/_sseclient.py +++ b/firebase_admin/_sseclient.py @@ -86,7 +86,7 @@ def __init__(self, url, session, retry=3000, **kwargs): self.requests_kwargs = kwargs self.should_connect = True self.last_id = None - self.buf = u'' # Keep data here as it streams in + self.buf = '' # Keep data here as it streams in headers = self.requests_kwargs.get('headers', {}) # The SSE spec requires making requests with Cache-Control: no-cache @@ -153,9 +153,6 @@ def __next__(self): self.last_id = event.event_id return event - def next(self): - return self.__next__() - class Event: """Event represents the events fired by SSE.""" @@ -184,7 +181,7 @@ def parse(cls, raw): match = cls.sse_line_pattern.match(line) if match is None: # Malformed line. Discard but warn. - warnings.warn('Invalid SSE line: "%s"' % line, SyntaxWarning) + warnings.warn(f'Invalid SSE line: "{line}"', SyntaxWarning) continue name = match.groupdict()['name'] @@ -196,7 +193,7 @@ def parse(cls, raw): # If we already have some data, then join to it with a newline. # Else this is it. if event.data: - event.data = '%s\n%s' % (event.data, value) + event.data = f'{event.data}\n{value}' else: event.data = value elif name == 'event': diff --git a/firebase_admin/_token_gen.py b/firebase_admin/_token_gen.py index 6d82bf7a6..1607ef0ba 100644 --- a/firebase_admin/_token_gen.py +++ b/firebase_admin/_token_gen.py @@ -114,7 +114,7 @@ def __init__(self, app, http_client, url_override=None): self.http_client = http_client self.request = transport.requests.Request() url_prefix = url_override or self.ID_TOOLKIT_URL - self.base_url = '{0}/projects/{1}'.format(url_prefix, app.project_id) + self.base_url = f'{url_prefix}/projects/{app.project_id}' self._signing_provider = None def _init_signing_provider(self): @@ -142,7 +142,7 @@ def _init_signing_provider(self): resp = self.request(url=METADATA_SERVICE_URL, headers={'Metadata-Flavor': 'Google'}) if resp.status != 200: raise ValueError( - 'Failed to contact the local metadata service: {0}.'.format(resp.data.decode())) + f'Failed to contact the local metadata service: {resp.data.decode()}.') service_account = resp.data.decode() return _SigningProvider.from_iam(self.request, google_cred, service_account) @@ -155,10 +155,10 @@ def signing_provider(self): except Exception as error: url = 'https://firebase.google.com/docs/auth/admin/create-custom-tokens' raise ValueError( - 'Failed to determine service account: {0}. Make sure to initialize the SDK ' - 'with service account credentials or specify a service account ID with ' - 'iam.serviceAccounts.signBlob permission. Please refer to {1} for more ' - 'details on creating custom tokens.'.format(error, url)) from error + f'Failed to determine service account: {error}. Make sure to initialize the ' + 'SDK with service account credentials or specify a service account ID with ' + f'iam.serviceAccounts.signBlob permission. Please refer to {url} for more ' + 'details on creating custom tokens.') from error return self._signing_provider def create_custom_token(self, uid, developer_claims=None, tenant_id=None): @@ -170,13 +170,13 @@ def create_custom_token(self, uid, developer_claims=None, tenant_id=None): disallowed_keys = set(developer_claims.keys()) & RESERVED_CLAIMS if disallowed_keys: if len(disallowed_keys) > 1: - error_message = ('Developer claims {0} are reserved and ' - 'cannot be specified.'.format( - ', '.join(disallowed_keys))) + error_message = ( + f'Developer claims {", ".join(disallowed_keys)} are reserved and cannot be ' + 'specified.') else: - error_message = ('Developer claim {0} is reserved and ' - 'cannot be specified.'.format( - ', '.join(disallowed_keys))) + error_message = ( + f'Developer claim {", ".join(disallowed_keys)} is reserved and cannot be ' + 'specified.') raise ValueError(error_message) if not uid or not isinstance(uid, str) or len(uid) > 128: @@ -202,7 +202,7 @@ def create_custom_token(self, uid, developer_claims=None, tenant_id=None): try: return jwt.encode(signing_provider.signer, payload, header=header) except google.auth.exceptions.TransportError as error: - msg = 'Failed to sign custom token. {0}'.format(error) + msg = f'Failed to sign custom token. {error}' raise TokenSignError(msg, error) from error @@ -211,21 +211,22 @@ def create_session_cookie(self, id_token, expires_in): id_token = id_token.decode('utf-8') if isinstance(id_token, bytes) else id_token if not isinstance(id_token, str) or not id_token: raise ValueError( - 'Illegal ID token provided: {0}. ID token must be a non-empty ' - 'string.'.format(id_token)) + f'Illegal ID token provided: {id_token}. ID token must be a non-empty string.') if isinstance(expires_in, datetime.timedelta): expires_in = int(expires_in.total_seconds()) if isinstance(expires_in, bool) or not isinstance(expires_in, int): - raise ValueError('Illegal expiry duration: {0}.'.format(expires_in)) + raise ValueError(f'Illegal expiry duration: {expires_in}.') if expires_in < MIN_SESSION_COOKIE_DURATION_SECONDS: - raise ValueError('Illegal expiry duration: {0}. Duration must be at least {1} ' - 'seconds.'.format(expires_in, MIN_SESSION_COOKIE_DURATION_SECONDS)) + raise ValueError( + f'Illegal expiry duration: {expires_in}. Duration must be at least ' + f'{MIN_SESSION_COOKIE_DURATION_SECONDS} seconds.') if expires_in > MAX_SESSION_COOKIE_DURATION_SECONDS: - raise ValueError('Illegal expiry duration: {0}. Duration must be at most {1} ' - 'seconds.'.format(expires_in, MAX_SESSION_COOKIE_DURATION_SECONDS)) + raise ValueError( + f'Illegal expiry duration: {expires_in}. Duration must be at most ' + f'{MAX_SESSION_COOKIE_DURATION_SECONDS} seconds.') - url = '{0}:createSessionCookie'.format(self.base_url) + url = f'{self.base_url}:createSessionCookie' payload = { 'idToken': id_token, 'validDuration': expires_in, @@ -234,11 +235,10 @@ def create_session_cookie(self, id_token, expires_in): body, http_resp = self.http_client.body_and_response('post', url, json=payload) except requests.exceptions.RequestException as error: raise _auth_utils.handle_auth_backend_error(error) - else: - if not body or not body.get('sessionCookie'): - raise _auth_utils.UnexpectedResponseError( - 'Failed to create session cookie.', http_response=http_resp) - return body.get('sessionCookie') + if not body or not body.get('sessionCookie'): + raise _auth_utils.UnexpectedResponseError( + 'Failed to create session cookie.', http_response=http_resp) + return body.get('sessionCookie') class CertificateFetchRequest(transport.Request): @@ -307,9 +307,9 @@ def __init__(self, **kwargs): self.cert_url = kwargs.pop('cert_url') self.issuer = kwargs.pop('issuer') if self.short_name[0].lower() in 'aeiou': - self.articled_short_name = 'an {0}'.format(self.short_name) + self.articled_short_name = f'an {self.short_name}' else: - self.articled_short_name = 'a {0}'.format(self.short_name) + self.articled_short_name = f'a {self.short_name}' self._invalid_token_error = kwargs.pop('invalid_token_error') self._expired_token_error = kwargs.pop('expired_token_error') @@ -318,20 +318,20 @@ def verify(self, token, request, clock_skew_seconds=0): token = token.encode('utf-8') if isinstance(token, str) else token if not isinstance(token, bytes) or not token: raise ValueError( - 'Illegal {0} provided: {1}. {0} must be a non-empty ' - 'string.'.format(self.short_name, token)) + f'Illegal {self.short_name} provided: {token}. {self.short_name} must be a ' + 'non-empty string.') if not self.project_id: raise ValueError( 'Failed to ascertain project ID from the credential or the environment. Project ' - 'ID is required to call {0}. Initialize the app with a credentials.Certificate ' - 'or set your Firebase project ID as an app option. Alternatively set the ' - 'GOOGLE_CLOUD_PROJECT environment variable.'.format(self.operation)) + f'ID is required to call {self.operation}. Initialize the app with a ' + 'credentials.Certificate or set your Firebase project ID as an app option. ' + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.') if clock_skew_seconds < 0 or clock_skew_seconds > 60: raise ValueError( - 'Illegal clock_skew_seconds value: {0}. Must be between 0 and 60, inclusive.' - .format(clock_skew_seconds)) + f'Illegal clock_skew_seconds value: {clock_skew_seconds}. Must be between 0 and 60' + ', inclusive.') header, payload = self._decode_unverified(token) issuer = payload.get('iss') @@ -340,52 +340,51 @@ def verify(self, token, request, clock_skew_seconds=0): expected_issuer = self.issuer + self.project_id project_id_match_msg = ( - 'Make sure the {0} comes from the same Firebase project as the service account used ' - 'to authenticate this SDK.'.format(self.short_name)) + f'Make sure the {self.short_name} comes from the same Firebase project as the service ' + 'account used to authenticate this SDK.') verify_id_token_msg = ( - 'See {0} for details on how to retrieve {1}.'.format(self.url, self.short_name)) + f'See {self.url} for details on how to retrieve {self.short_name}.') emulated = _auth_utils.is_emulated() error_message = None if audience == FIREBASE_AUDIENCE: error_message = ( - '{0} expects {1}, but was given a custom ' - 'token.'.format(self.operation, self.articled_short_name)) + f'{self.operation} expects {self.articled_short_name}, but was given a custom ' + 'token.') elif not emulated and not header.get('kid'): if header.get('alg') == 'HS256' and payload.get( 'v') == 0 and 'uid' in payload.get('d', {}): error_message = ( - '{0} expects {1}, but was given a legacy custom ' - 'token.'.format(self.operation, self.articled_short_name)) + f'{self.operation} expects {self.articled_short_name}, but was given a legacy ' + 'custom token.') else: - error_message = 'Firebase {0} has no "kid" claim.'.format(self.short_name) + error_message = f'Firebase {self.short_name} has no "kid" claim.' elif not emulated and header.get('alg') != 'RS256': error_message = ( - 'Firebase {0} has incorrect algorithm. Expected "RS256" but got ' - '"{1}". {2}'.format(self.short_name, header.get('alg'), verify_id_token_msg)) + f'Firebase {self.short_name} has incorrect algorithm. Expected "RS256" but got ' + f'"{header.get("alg")}". {verify_id_token_msg}') elif audience != self.project_id: error_message = ( - 'Firebase {0} has incorrect "aud" (audience) claim. Expected "{1}" but ' - 'got "{2}". {3} {4}'.format(self.short_name, self.project_id, audience, - project_id_match_msg, verify_id_token_msg)) + f'Firebase {self.short_name} has incorrect "aud" (audience) claim. Expected ' + f'"{self.project_id}" but got "{audience}". {project_id_match_msg} ' + f'{verify_id_token_msg}') elif issuer != expected_issuer: error_message = ( - 'Firebase {0} has incorrect "iss" (issuer) claim. Expected "{1}" but ' - 'got "{2}". {3} {4}'.format(self.short_name, expected_issuer, issuer, - project_id_match_msg, verify_id_token_msg)) + f'Firebase {self.short_name} has incorrect "iss" (issuer) claim. Expected ' + f'"{expected_issuer}" but got "{issuer}". {project_id_match_msg} ' + f'{verify_id_token_msg}') elif subject is None or not isinstance(subject, str): error_message = ( - 'Firebase {0} has no "sub" (subject) claim. ' - '{1}'.format(self.short_name, verify_id_token_msg)) + f'Firebase {self.short_name} has no "sub" (subject) claim. {verify_id_token_msg}') elif not subject: error_message = ( - 'Firebase {0} has an empty string "sub" (subject) claim. ' - '{1}'.format(self.short_name, verify_id_token_msg)) + f'Firebase {self.short_name} has an empty string "sub" (subject) claim. ' + f'{verify_id_token_msg}') elif len(subject) > 128: error_message = ( - 'Firebase {0} has a "sub" (subject) claim longer than 128 characters. ' - '{1}'.format(self.short_name, verify_id_token_msg)) + f'Firebase {self.short_name} has a "sub" (subject) claim longer than 128 ' + f'characters. {verify_id_token_msg}') if error_message: raise self._invalid_token_error(error_message) diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index 957b749a6..9a75b7a2e 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -130,7 +130,7 @@ class UserRecord(UserInfo): def __init__(self, data): super().__init__() if not isinstance(data, dict): - raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data)) + raise ValueError(f'Invalid data argument: {data}. Must be a dictionary.') if not data.get('localId'): raise ValueError('User ID must not be None or empty.') self._data = data @@ -454,7 +454,7 @@ class ProviderUserInfo(UserInfo): def __init__(self, data): super().__init__() if not isinstance(data, dict): - raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data)) + raise ValueError(f'Invalid data argument: {data}. Must be a dictionary.') if not data.get('rawId'): raise ValueError('User ID must not be None or empty.') self._data = data @@ -516,30 +516,30 @@ def encode_action_code_settings(settings): try: parsed = parse.urlparse(settings.url) if not parsed.netloc: - raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) + raise ValueError(f'Malformed dynamic action links url: "{settings.url}".') parameters['continueUrl'] = settings.url except Exception as err: - raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) from err + raise ValueError(f'Malformed dynamic action links url: "{settings.url}".') from err # handle_code_in_app if settings.handle_code_in_app is not None: if not isinstance(settings.handle_code_in_app, bool): - raise ValueError('Invalid value provided for handle_code_in_app: {0}' - .format(settings.handle_code_in_app)) + raise ValueError( + f'Invalid value provided for handle_code_in_app: {settings.handle_code_in_app}') parameters['canHandleCodeInApp'] = settings.handle_code_in_app # dynamic_link_domain if settings.dynamic_link_domain is not None: if not isinstance(settings.dynamic_link_domain, str): - raise ValueError('Invalid value provided for dynamic_link_domain: {0}' - .format(settings.dynamic_link_domain)) + raise ValueError( + f'Invalid value provided for dynamic_link_domain: {settings.dynamic_link_domain}') parameters['dynamicLinkDomain'] = settings.dynamic_link_domain # ios_bundle_id if settings.ios_bundle_id is not None: if not isinstance(settings.ios_bundle_id, str): - raise ValueError('Invalid value provided for ios_bundle_id: {0}' - .format(settings.ios_bundle_id)) + raise ValueError( + f'Invalid value provided for ios_bundle_id: {settings.ios_bundle_id}') parameters['iOSBundleId'] = settings.ios_bundle_id # android_* attributes @@ -549,20 +549,21 @@ def encode_action_code_settings(settings): if settings.android_package_name is not None: if not isinstance(settings.android_package_name, str): - raise ValueError('Invalid value provided for android_package_name: {0}' - .format(settings.android_package_name)) + raise ValueError( + f'Invalid value provided for android_package_name: {settings.android_package_name}') parameters['androidPackageName'] = settings.android_package_name if settings.android_minimum_version is not None: if not isinstance(settings.android_minimum_version, str): - raise ValueError('Invalid value provided for android_minimum_version: {0}' - .format(settings.android_minimum_version)) + raise ValueError( + 'Invalid value provided for android_minimum_version: ' + f'{settings.android_minimum_version}') parameters['androidMinimumVersion'] = settings.android_minimum_version if settings.android_install_app is not None: if not isinstance(settings.android_install_app, bool): - raise ValueError('Invalid value provided for android_install_app: {0}' - .format(settings.android_install_app)) + raise ValueError( + f'Invalid value provided for android_install_app: {settings.android_install_app}') parameters['androidInstallApp'] = settings.android_install_app return parameters @@ -576,9 +577,9 @@ class UserManager: def __init__(self, http_client, project_id, tenant_id=None, url_override=None): self.http_client = http_client url_prefix = url_override or self.ID_TOOLKIT_URL - self.base_url = '{0}/projects/{1}'.format(url_prefix, project_id) + self.base_url = f'{url_prefix}/projects/{project_id}' if tenant_id: - self.base_url += '/tenants/{0}'.format(tenant_id) + self.base_url += f'/tenants/{tenant_id}' def get_user(self, **kwargs): """Gets the user data corresponding to the provided key.""" @@ -592,12 +593,12 @@ def get_user(self, **kwargs): key, key_type = kwargs.pop('phone_number'), 'phone number' payload = {'phoneNumber' : [_auth_utils.validate_phone(key, required=True)]} else: - raise TypeError('Unsupported keyword arguments: {0}.'.format(kwargs)) + raise TypeError(f'Unsupported keyword arguments: {kwargs}.') body, http_resp = self._make_request('post', '/accounts:lookup', json=payload) if not body or not body.get('users'): raise _auth_utils.UserNotFoundError( - 'No user record found for the provided {0}: {1}.'.format(key_type, key), + f'No user record found for the provided {key_type}: {key}.', http_response=http_resp) return body['users'][0] @@ -638,8 +639,7 @@ def get_users(self, identifiers): }) else: raise ValueError( - 'Invalid entry in "identifiers" list. Unsupported type: {}' - .format(type(identifier))) + f'Invalid entry in "identifiers" list. Unsupported type: {type(identifier)}') body, http_resp = self._make_request( 'post', '/accounts:lookup', json=payload) @@ -657,8 +657,7 @@ def list_users(self, page_token=None, max_results=MAX_LIST_USERS_RESULTS): raise ValueError('Max results must be an integer.') if max_results < 1 or max_results > MAX_LIST_USERS_RESULTS: raise ValueError( - 'Max results must be a positive integer less than ' - '{0}.'.format(MAX_LIST_USERS_RESULTS)) + f'Max results must be a positive integer less than {MAX_LIST_USERS_RESULTS}.') payload = {'maxResults': max_results} if page_token: @@ -734,7 +733,7 @@ def update_user(self, uid, display_name=None, email=None, phone_number=None, body, http_resp = self._make_request('post', '/accounts:update', json=payload) if not body or not body.get('localId'): raise _auth_utils.UnexpectedResponseError( - 'Failed to update user: {0}.'.format(uid), http_response=http_resp) + f'Failed to update user: {uid}.', http_response=http_resp) return body.get('localId') def delete_user(self, uid): @@ -743,7 +742,7 @@ def delete_user(self, uid): body, http_resp = self._make_request('post', '/accounts:delete', json={'localId' : uid}) if not body or not body.get('kind'): raise _auth_utils.UnexpectedResponseError( - 'Failed to delete user: {0}.'.format(uid), http_response=http_resp) + f'Failed to delete user: {uid}.', http_response=http_resp) def delete_users(self, uids, force_delete=False): """Deletes the users identified by the specified user ids. @@ -786,8 +785,8 @@ def import_users(self, users, hash_alg=None): try: if not users or len(users) > MAX_IMPORT_USERS_SIZE: raise ValueError( - 'Users must be a non-empty list with no more than {0} elements.'.format( - MAX_IMPORT_USERS_SIZE)) + 'Users must be a non-empty list with no more than ' + f'{MAX_IMPORT_USERS_SIZE} elements.') if any(not isinstance(u, _user_import.ImportUserRecord) for u in users): raise ValueError('One or more user objects are invalid.') except TypeError as err: @@ -837,7 +836,7 @@ def generate_email_action_link(self, action_type, email, action_code_settings=No return body.get('oobLink') def _make_request(self, method, path, **kwargs): - url = '{0}{1}'.format(self.base_url, path) + url = f'{self.base_url}{path}' try: return self.http_client.body_and_response(method, url, **kwargs) except requests.exceptions.RequestException as error: diff --git a/firebase_admin/_utils.py b/firebase_admin/_utils.py index 765d11587..d0aca884b 100644 --- a/firebase_admin/_utils.py +++ b/firebase_admin/_utils.py @@ -93,8 +93,9 @@ def _get_initialized_app(app): 'initialized via the firebase module.') return app - raise ValueError('Illegal app argument. Argument must be of type ' - ' firebase_admin.App, but given "{0}".'.format(type(app))) + raise ValueError( + 'Illegal app argument. Argument must be of type firebase_admin.App, but given ' + f'"{type(app)}".') @@ -172,7 +173,7 @@ def handle_operation_error(error): """ if not isinstance(error, dict): return exceptions.UnknownError( - message='Unknown error while making a remote service call: {0}'.format(error), + message=f'Unknown error while making a remote service call: {error}', cause=error) rpc_code = error.get('code') @@ -217,15 +218,15 @@ def handle_requests_error(error, message=None, code=None): """ if isinstance(error, requests.exceptions.Timeout): return exceptions.DeadlineExceededError( - message='Timed out while making an API call: {0}'.format(error), + message=f'Timed out while making an API call: {error}', cause=error) if isinstance(error, requests.exceptions.ConnectionError): return exceptions.UnavailableError( - message='Failed to establish a connection: {0}'.format(error), + message=f'Failed to establish a connection: {error}', cause=error) if error.response is None: return exceptions.UnknownError( - message='Unknown error while making a remote service call: {0}'.format(error), + message=f'Unknown error while making a remote service call: {error}', cause=error) if not code: @@ -271,11 +272,11 @@ def handle_httpx_error(error: httpx.HTTPError, message=None, code=None) -> excep """ if isinstance(error, httpx.TimeoutException): return exceptions.DeadlineExceededError( - message='Timed out while making an API call: {0}'.format(error), + message=f'Timed out while making an API call: {error}', cause=error) if isinstance(error, httpx.ConnectError): return exceptions.UnavailableError( - message='Failed to establish a connection: {0}'.format(error), + message=f'Failed to establish a connection: {error}', cause=error) if isinstance(error, httpx.HTTPStatusError): print("printing status error", error) @@ -288,7 +289,7 @@ def handle_httpx_error(error: httpx.HTTPError, message=None, code=None) -> excep return err_type(message=message, cause=error, http_response=error.response) return exceptions.UnknownError( - message='Unknown error while making a remote service call: {0}'.format(error), + message=f'Unknown error while making a remote service call: {error}', cause=error) def _http_status_to_error_code(status): @@ -326,7 +327,7 @@ def _parse_platform_error(content, status_code): error_dict = data.get('error', {}) msg = error_dict.get('message') if not msg: - msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(status_code, content) + msg = f'Unexpected HTTP response with status: {status_code}; body: {content}' return error_dict, msg diff --git a/firebase_admin/app_check.py b/firebase_admin/app_check.py index 1224f7d80..40d857f4e 100644 --- a/firebase_admin/app_check.py +++ b/firebase_admin/app_check.py @@ -156,6 +156,6 @@ class _Validators: def check_string(cls, label: str, value: Any): """Checks if the given value is a string.""" if value is None: - raise ValueError('{0} "{1}" must be a non-empty string.'.format(label, value)) + raise ValueError(f'{label} "{value}" must be a non-empty string.') if not isinstance(value, str): - raise ValueError('{0} "{1}" must be a string.'.format(label, value)) + raise ValueError(f'{label} "{value}" must be a string.') diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py index 8259c93b4..7117b71a9 100644 --- a/firebase_admin/credentials.py +++ b/firebase_admin/credentials.py @@ -94,24 +94,25 @@ def __init__(self, cert): """ super().__init__() if _is_file_path(cert): - with open(cert) as json_file: + with open(cert, encoding='utf-8') as json_file: json_data = json.load(json_file) elif isinstance(cert, dict): json_data = cert else: raise ValueError( - 'Invalid certificate argument: "{0}". Certificate argument must be a file path, ' - 'or a dict containing the parsed file contents.'.format(cert)) + f'Invalid certificate argument: "{cert}". Certificate argument must be a file ' + 'path, or a dict containing the parsed file contents.') if json_data.get('type') != self._CREDENTIAL_TYPE: - raise ValueError('Invalid service account certificate. Certificate must contain a ' - '"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE)) + raise ValueError( + 'Invalid service account certificate. Certificate must contain a ' + f'"type" field set to "{self._CREDENTIAL_TYPE}".') try: self._g_credential = service_account.Credentials.from_service_account_info( json_data, scopes=_scopes) except ValueError as error: - raise ValueError('Failed to initialize a certificate credential. ' - 'Caused by: "{0}"'.format(error)) from error + raise ValueError( + f'Failed to initialize a certificate credential. Caused by: "{error}"') from error @property def project_id(self): @@ -195,18 +196,19 @@ def __init__(self, refresh_token): """ super().__init__() if _is_file_path(refresh_token): - with open(refresh_token) as json_file: + with open(refresh_token, encoding='utf-8') as json_file: json_data = json.load(json_file) elif isinstance(refresh_token, dict): json_data = refresh_token else: raise ValueError( - 'Invalid refresh token argument: "{0}". Refresh token argument must be a file ' - 'path, or a dict containing the parsed file contents.'.format(refresh_token)) + f'Invalid refresh token argument: "{refresh_token}". Refresh token argument must ' + 'be a file path, or a dict containing the parsed file contents.') if json_data.get('type') != self._CREDENTIAL_TYPE: - raise ValueError('Invalid refresh token configuration. JSON must contain a ' - '"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE)) + raise ValueError( + 'Invalid refresh token configuration. JSON must contain a ' + f'"type" field set to "{self._CREDENTIAL_TYPE}".') self._g_credential = credentials.Credentials.from_authorized_user_info(json_data, _scopes) @property diff --git a/firebase_admin/db.py b/firebase_admin/db.py index fc69cbd83..800cbf8e3 100644 --- a/firebase_admin/db.py +++ b/firebase_admin/db.py @@ -39,8 +39,10 @@ _DB_ATTRIBUTE = '_database' _INVALID_PATH_CHARACTERS = '[].?#$' _RESERVED_FILTERS = ('$key', '$value', '$priority') -_USER_AGENT = 'Firebase/HTTP/{0}/{1}.{2}/AdminPython'.format( - firebase_admin.__version__, sys.version_info.major, sys.version_info.minor) +_USER_AGENT = ( + f'Firebase/HTTP/{firebase_admin.__version__}/{sys.version_info.major}' + f'.{sys.version_info.minor}/AdminPython' +) _TRANSACTION_MAX_RETRIES = 25 _EMULATOR_HOST_ENV_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST' @@ -72,10 +74,9 @@ def reference(path='/', app=None, url=None): def _parse_path(path): """Parses a path string into a set of segments.""" if not isinstance(path, str): - raise ValueError('Invalid path: "{0}". Path must be a string.'.format(path)) + raise ValueError(f'Invalid path: "{path}". Path must be a string.') if any(ch in path for ch in _INVALID_PATH_CHARACTERS): - raise ValueError( - 'Invalid path: "{0}". Path contains illegal characters.'.format(path)) + raise ValueError(f'Invalid path: "{path}". Path contains illegal characters.') return [seg for seg in path.split('/') if seg] @@ -184,11 +185,9 @@ def child(self, path): ValueError: If the child path is not a string, not well-formed or begins with '/'. """ if not path or not isinstance(path, str): - raise ValueError( - 'Invalid path argument: "{0}". Path must be a non-empty string.'.format(path)) + raise ValueError(f'Invalid path argument: "{path}". Path must be a non-empty string.') if path.startswith('/'): - raise ValueError( - 'Invalid path argument: "{0}". Child path must not start with "/"'.format(path)) + raise ValueError(f'Invalid path argument: "{path}". Child path must not start with "/"') full_path = self._pathurl + '/' + path return Reference(client=self._client, path=full_path) @@ -433,7 +432,7 @@ def order_by_child(self, path): ValueError: If the child path is not a string, not well-formed or None. """ if path in _RESERVED_FILTERS: - raise ValueError('Illegal child path: {0}'.format(path)) + raise ValueError(f'Illegal child path: {path}') return Query(order_by=path, client=self._client, pathurl=self._add_suffix()) def order_by_key(self): @@ -492,8 +491,8 @@ def __init__(self, **kwargs): raise ValueError('order_by field must be a non-empty string') if order_by not in _RESERVED_FILTERS: if order_by.startswith('/'): - raise ValueError('Invalid path argument: "{0}". Child path must not start ' - 'with "/"'.format(order_by)) + raise ValueError( + f'Invalid path argument: "{order_by}". Child path must not start with "/"') segments = _parse_path(order_by) order_by = '/'.join(segments) self._client = kwargs.pop('client') @@ -501,7 +500,7 @@ def __init__(self, **kwargs): self._order_by = order_by self._params = {'orderBy' : json.dumps(order_by)} if kwargs: - raise ValueError('Unexpected keyword arguments: {0}'.format(kwargs)) + raise ValueError(f'Unexpected keyword arguments: {kwargs}') def limit_to_first(self, limit): """Creates a query with limit, and anchors it to the start of the window. @@ -604,7 +603,7 @@ def equal_to(self, value): def _querystr(self): params = [] for key in sorted(self._params): - params.append('{0}={1}'.format(key, self._params[key])) + params.append(f'{key}={self._params[key]}') return '&'.join(params) def get(self): @@ -642,7 +641,7 @@ def __init__(self, results, order_by): self.dict_input = False entries = [_SortEntry(k, v, order_by) for k, v in enumerate(results)] else: - raise ValueError('Sorting not supported for "{0}" object.'.format(type(results))) + raise ValueError(f'Sorting not supported for "{type(results)}" object.') self.sort_entries = sorted(entries) def get(self): @@ -783,8 +782,8 @@ def __init__(self, app): if emulator_host: if '//' in emulator_host: raise ValueError( - 'Invalid {0}: "{1}". It must follow format "host:port".'.format( - _EMULATOR_HOST_ENV_VAR, emulator_host)) + f'Invalid {_EMULATOR_HOST_ENV_VAR}: "{emulator_host}". It must follow format ' + '"host:port".') self._emulator_host = emulator_host else: self._emulator_host = None @@ -796,14 +795,12 @@ def get_client(self, db_url=None): if not db_url or not isinstance(db_url, str): raise ValueError( - 'Invalid database URL: "{0}". Database URL must be a non-empty ' - 'URL string.'.format(db_url)) + f'Invalid database URL: "{db_url}". Database URL must be a non-empty URL string.') parsed_url = parse.urlparse(db_url) if not parsed_url.netloc: raise ValueError( - 'Invalid database URL: "{0}". Database URL must be a wellformed ' - 'URL string.'.format(db_url)) + f'Invalid database URL: "{db_url}". Database URL must be a wellformed URL string.') emulator_config = self._get_emulator_config(parsed_url) if emulator_config: @@ -813,7 +810,7 @@ def get_client(self, db_url=None): else: # Defer credential lookup until we are certain it's going to be prod connection. credential = self._credential.get_credential() - base_url = 'https://{0}'.format(parsed_url.netloc) + base_url = f'https://{parsed_url.netloc}' params = {} @@ -835,7 +832,7 @@ def _get_emulator_config(self, parsed_url): return EmulatorConfig(base_url, namespace) if self._emulator_host: # Emulator mode enabled via environment variable - base_url = 'http://{0}'.format(self._emulator_host) + base_url = f'http://{self._emulator_host}' namespace = parsed_url.netloc.split('.')[0] return EmulatorConfig(base_url, namespace) @@ -847,21 +844,23 @@ def _parse_emulator_url(cls, parsed_url): query_ns = parse.parse_qs(parsed_url.query).get('ns') if parsed_url.scheme != 'http' or (not query_ns or len(query_ns) != 1 or not query_ns[0]): raise ValueError( - 'Invalid database URL: "{0}". Database URL must be a valid URL to a ' - 'Firebase Realtime Database instance.'.format(parsed_url.geturl())) + f'Invalid database URL: "{parsed_url.geturl()}". Database URL must be a valid URL ' + 'to a Firebase Realtime Database instance.') namespace = query_ns[0] - base_url = '{0}://{1}'.format(parsed_url.scheme, parsed_url.netloc) + base_url = f'{parsed_url.scheme}://{parsed_url.netloc}' return base_url, namespace @classmethod def _get_auth_override(cls, app): + """Gets and validates the database auth override to be used.""" auth_override = app.options.get('databaseAuthVariableOverride', cls._DEFAULT_AUTH_OVERRIDE) if auth_override == cls._DEFAULT_AUTH_OVERRIDE or auth_override is None: return auth_override if not isinstance(auth_override, dict): - raise ValueError('Invalid databaseAuthVariableOverride option: "{0}". Override ' - 'value must be a dict or None.'.format(auth_override)) + raise ValueError( + f'Invalid databaseAuthVariableOverride option: "{auth_override}". Override ' + 'value must be a dict or None.') return auth_override @@ -916,7 +915,7 @@ def request(self, method, url, **kwargs): Raises: FirebaseError: If an error occurs while making the HTTP call. """ - query = '&'.join('{0}={1}'.format(key, self.params[key]) for key in self.params) + query = '&'.join(f'{key}={value}' for key, value in self.params.items()) extra_params = kwargs.get('params') if extra_params: if query: @@ -961,6 +960,6 @@ def _extract_error_message(cls, response): pass if not message: - message = 'Unexpected response from database: {0}'.format(response.content.decode()) + message = f'Unexpected response from database: {response.content.decode()}' return message diff --git a/firebase_admin/functions.py b/firebase_admin/functions.py index fa17dfc0c..86eea557a 100644 --- a/firebase_admin/functions.py +++ b/firebase_admin/functions.py @@ -48,7 +48,7 @@ _FUNCTIONS_HEADERS = { 'X-GOOG-API-FORMAT-VERSION': '2', - 'X-FIREBASE-CLIENT': 'fire-admin-python/{0}'.format(firebase_admin.__version__), + 'X-FIREBASE-CLIENT': f'fire-admin-python/{firebase_admin.__version__}', } # Default canonical location ID of the task queue. @@ -306,9 +306,9 @@ class _Validators: def check_non_empty_string(cls, label: str, value: Any): """Checks if given value is a non-empty string and throws error if not.""" if not isinstance(value, str): - raise ValueError('{0} "{1}" must be a string.'.format(label, value)) + raise ValueError(f'{label} "{value}" must be a string.') if value == '': - raise ValueError('{0} "{1}" must be a non-empty string.'.format(label, value)) + raise ValueError(f'{label} "{value}" must be a non-empty string.') @classmethod def is_non_empty_string(cls, value: Any): diff --git a/firebase_admin/instance_id.py b/firebase_admin/instance_id.py index 604158d9c..812daf40b 100644 --- a/firebase_admin/instance_id.py +++ b/firebase_admin/instance_id.py @@ -81,7 +81,7 @@ def __init__(self, app): def delete_instance_id(self, instance_id): if not isinstance(instance_id, str) or not instance_id: raise ValueError('Instance ID must be a non-empty string.') - path = 'project/{0}/instanceId/{1}'.format(self._project_id, instance_id) + path = f'project/{self._project_id}/instanceId/{instance_id}' try: self._client.request('delete', path) except requests.exceptions.RequestException as error: @@ -94,6 +94,6 @@ def _extract_message(self, instance_id, error): status = error.response.status_code msg = self.error_codes.get(status) if msg: - return 'Instance ID "{0}": {1}'.format(instance_id, msg) + return f'Instance ID "{instance_id}": {msg}' - return 'Instance ID "{0}": {1}'.format(instance_id, error) + return f'Instance ID "{instance_id}": {error}' diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 5b2e48e80..749044436 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -301,7 +301,7 @@ class TopicManagementResponse: def __init__(self, resp): if not isinstance(resp, dict) or 'results' not in resp: - raise ValueError('Unexpected topic management response: {0}.'.format(resp)) + raise ValueError(f'Unexpected topic management response: {resp}.') self._success_count = 0 self._failure_count = 0 self._errors = [] @@ -400,7 +400,7 @@ def __init__(self, app: App) -> None: self._fcm_url = _MessagingService.FCM_URL.format(project_id) self._fcm_headers = { 'X-GOOG-API-FORMAT-VERSION': '2', - 'X-FIREBASE-CLIENT': 'fire-admin-python/{0}'.format(firebase_admin.__version__), + 'X-FIREBASE-CLIENT': f'fire-admin-python/{firebase_admin.__version__}', } timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS) self._credential = app.credential.get_credential() @@ -426,8 +426,7 @@ def send(self, message: Message, dry_run: bool = False) -> str: ) except requests.exceptions.RequestException as error: raise self._handle_fcm_error(error) - else: - return cast(str, resp['name']) + return cast(str, resp['name']) def send_each(self, messages: List[Message], dry_run: bool = False) -> BatchResponse: """Sends the given messages to FCM via the FCM v1 API.""" @@ -445,8 +444,7 @@ def send_data(data): json=data) except requests.exceptions.RequestException as exception: return SendResponse(resp=None, exception=self._handle_fcm_error(exception)) - else: - return SendResponse(resp, exception=None) + return SendResponse(resp, exception=None) message_data = [self._message_data(message, dry_run) for message in messages] try: @@ -455,7 +453,7 @@ def send_data(data): return BatchResponse(responses) except Exception as error: raise exceptions.UnknownError( - message='Unknown error while making remote service calls: {0}'.format(error), + message=f'Unknown error while making remote service calls: {error}', cause=error) async def send_each_async(self, messages: List[Message], dry_run: bool = True) -> BatchResponse: @@ -477,8 +475,7 @@ async def send_data(data): # Catch errors caused by the requests library during authorization except requests.exceptions.RequestException as exception: return SendResponse(resp=None, exception=self._handle_fcm_error(exception)) - else: - return SendResponse(resp.json(), exception=None) + return SendResponse(resp.json(), exception=None) message_data = [self._message_data(message, dry_run) for message in messages] try: @@ -486,7 +483,7 @@ async def send_data(data): return BatchResponse(responses) except Exception as error: raise exceptions.UnknownError( - message='Unknown error while making remote service calls: {0}'.format(error), + message=f'Unknown error while making remote service calls: {error}', cause=error) def make_topic_management_request(self, tokens, topic, operation): @@ -502,12 +499,12 @@ def make_topic_management_request(self, tokens, topic, operation): if not isinstance(topic, str) or not topic: raise ValueError('Topic must be a non-empty string.') if not topic.startswith('/topics/'): - topic = '/topics/{0}'.format(topic) + topic = f'/topics/{topic}' data = { 'to': topic, 'registration_tokens': tokens, } - url = '{0}/{1}'.format(_MessagingService.IID_URL, operation) + url = f'{_MessagingService.IID_URL}/{operation}' try: resp = self._client.body( 'post', @@ -517,8 +514,7 @@ def make_topic_management_request(self, tokens, topic, operation): ) except requests.exceptions.RequestException as error: raise self._handle_iid_error(error) - else: - return TopicManagementResponse(resp) + return TopicManagementResponse(resp) def _message_data(self, message, dry_run): data = {'message': _MessagingService.encode_message(message)} @@ -558,10 +554,12 @@ def _handle_iid_error(self, error): code = data.get('error') msg = None if code: - msg = 'Error while calling the IID service: {0}'.format(code) + msg = f'Error while calling the IID service: {code}' else: - msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format( - error.response.status_code, error.response.content.decode()) + msg = ( + f'Unexpected HTTP response with status: {error.response.status_code}; body: ' + f'{error.response.content.decode()}' + ) return _utils.handle_requests_error(error, msg) diff --git a/firebase_admin/ml.py b/firebase_admin/ml.py index 5fffbd836..3a77dd05f 100644 --- a/firebase_admin/ml.py +++ b/firebase_admin/ml.py @@ -507,8 +507,8 @@ def _assert_tf_enabled(): raise ImportError('Failed to import the tensorflow library for Python. Make sure ' 'to install the tensorflow module.') if not tf.version.VERSION.startswith('1.') and not tf.version.VERSION.startswith('2.'): - raise ImportError('Expected tensorflow version 1.x or 2.x, but found {0}' - .format(tf.version.VERSION)) + raise ImportError( + f'Expected tensorflow version 1.x or 2.x, but found {tf.version.VERSION}') @staticmethod def _tf_convert_from_saved_model(saved_model_dir): @@ -760,8 +760,8 @@ def _validate_page_size(page_size): # Specifically type() to disallow boolean which is a subtype of int raise TypeError('Page size must be a number or None.') if page_size < 1 or page_size > _MAX_PAGE_SIZE: - raise ValueError('Page size must be a positive integer between ' - '1 and {0}'.format(_MAX_PAGE_SIZE)) + raise ValueError( + f'Page size must be a positive integer between 1 and {_MAX_PAGE_SIZE}') def _validate_page_token(page_token): @@ -786,7 +786,7 @@ def __init__(self, app): 'projectId option, or use service account credentials.') self._project_url = _MLService.PROJECT_URL.format(self._project_id) ml_headers = { - 'X-FIREBASE-CLIENT': 'fire-admin-python/{0}'.format(firebase_admin.__version__), + 'X-FIREBASE-CLIENT': f'fire-admin-python/{firebase_admin.__version__}', } self._client = _http_client.JsonHttpClient( credential=app.credential.get_credential(), @@ -883,9 +883,9 @@ def create_model(self, model): def update_model(self, model, update_mask=None): _validate_model(model, update_mask) - path = 'models/{0}'.format(model.model_id) + path = f'models/{model.model_id}' if update_mask is not None: - path = path + '?updateMask={0}'.format(update_mask) + path = path + f'?updateMask={update_mask}' try: return self.handle_operation( self._client.body('patch', url=path, json=model.as_dict(for_upload=True))) @@ -894,7 +894,7 @@ def update_model(self, model, update_mask=None): def set_published(self, model_id, publish): _validate_model_id(model_id) - model_name = 'projects/{0}/models/{1}'.format(self._project_id, model_id) + model_name = f'projects/{self._project_id}/models/{model_id}' model = Model.from_dict({ 'name': model_name, 'state': { @@ -906,7 +906,7 @@ def set_published(self, model_id, publish): def get_model(self, model_id): _validate_model_id(model_id) try: - return self._client.body('get', url='models/{0}'.format(model_id)) + return self._client.body('get', url=f'models/{model_id}') except requests.exceptions.RequestException as error: raise _utils.handle_platform_error_from_requests(error) @@ -934,6 +934,6 @@ def list_models(self, list_filter, page_size, page_token): def delete_model(self, model_id): _validate_model_id(model_id) try: - self._client.body('delete', url='models/{0}'.format(model_id)) + self._client.body('delete', url=f'models/{model_id}') except requests.exceptions.RequestException as error: raise _utils.handle_platform_error_from_requests(error) diff --git a/firebase_admin/project_management.py b/firebase_admin/project_management.py index 9405c8318..73c100d3a 100644 --- a/firebase_admin/project_management.py +++ b/firebase_admin/project_management.py @@ -118,13 +118,13 @@ def create_ios_app(bundle_id, display_name=None, app=None): def _check_is_string_or_none(obj, field_name): if obj is None or isinstance(obj, str): return obj - raise ValueError('{0} must be a string.'.format(field_name)) + raise ValueError(f'{field_name} must be a string.') def _check_is_nonempty_string(obj, field_name): if isinstance(obj, str) and obj: return obj - raise ValueError('{0} must be a non-empty string.'.format(field_name)) + raise ValueError(f'{field_name} must be a non-empty string.') def _check_is_nonempty_string_or_none(obj, field_name): @@ -135,7 +135,7 @@ def _check_is_nonempty_string_or_none(obj, field_name): def _check_not_none(obj, field_name): if obj is None: - raise ValueError('{0} cannot be None.'.format(field_name)) + raise ValueError(f'{field_name} cannot be None.') return obj @@ -477,7 +477,7 @@ def __init__(self, app): 'set the projectId option, or use service account credentials. Alternatively, set ' 'the GOOGLE_CLOUD_PROJECT environment variable.') self._project_id = project_id - version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__) + version_header = f'Python/Admin/{firebase_admin.__version__}' timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS) self._client = _http_client.JsonHttpClient( credential=app.credential.get_credential(), @@ -502,7 +502,7 @@ def get_ios_app_metadata(self, app_id): def _get_app_metadata(self, platform_resource_name, identifier_name, metadata_class, app_id): """Retrieves detailed information about an Android or iOS app.""" _check_is_nonempty_string(app_id, 'app_id') - path = '/v1beta1/projects/-/{0}/{1}'.format(platform_resource_name, app_id) + path = f'/v1beta1/projects/-/{platform_resource_name}/{app_id}' response = self._make_request('get', path) return metadata_class( response[identifier_name], @@ -525,8 +525,7 @@ def set_ios_app_display_name(self, app_id, new_display_name): def _set_display_name(self, app_id, new_display_name, platform_resource_name): """Sets the display name of an Android or iOS app.""" - path = '/v1beta1/projects/-/{0}/{1}?updateMask=displayName'.format( - platform_resource_name, app_id) + path = f'/v1beta1/projects/-/{platform_resource_name}/{app_id}?updateMask=displayName' request_body = {'displayName': new_display_name} self._make_request('patch', path, json=request_body) @@ -542,10 +541,10 @@ def list_ios_apps(self): def _list_apps(self, platform_resource_name, app_class): """Lists all the Android or iOS apps within the Firebase project.""" - path = '/v1beta1/projects/{0}/{1}?pageSize={2}'.format( - self._project_id, - platform_resource_name, - _ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE) + path = ( + f'/v1beta1/projects/{self._project_id}/{platform_resource_name}?pageSize=' + f'{_ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE}' + ) response = self._make_request('get', path) apps_list = [] while True: @@ -557,11 +556,11 @@ def _list_apps(self, platform_resource_name, app_class): if not next_page_token: break # Retrieve the next page of apps. - path = '/v1beta1/projects/{0}/{1}?pageToken={2}&pageSize={3}'.format( - self._project_id, - platform_resource_name, - next_page_token, - _ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE) + path = ( + f'/v1beta1/projects/{self._project_id}/{platform_resource_name}' + f'?pageToken={next_page_token}' + f'&pageSize={_ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE}' + ) response = self._make_request('get', path) return apps_list @@ -590,7 +589,7 @@ def _create_app( app_class): """Creates an Android or iOS app.""" _check_is_string_or_none(display_name, 'display_name') - path = '/v1beta1/projects/{0}/{1}'.format(self._project_id, platform_resource_name) + path = f'/v1beta1/projects/{self._project_id}/{platform_resource_name}' request_body = {identifier_name: identifier} if display_name: request_body['displayName'] = display_name @@ -606,7 +605,7 @@ def _poll_app_creation(self, operation_name): _ProjectManagementService.POLL_EXPONENTIAL_BACKOFF_FACTOR, current_attempt) wait_time_seconds = delay_factor * _ProjectManagementService.POLL_BASE_WAIT_TIME_SECONDS time.sleep(wait_time_seconds) - path = '/v1/{0}'.format(operation_name) + path = f'/v1/{operation_name}' poll_response, http_response = self._body_and_response('get', path) done = poll_response.get('done') if done: @@ -629,20 +628,20 @@ def get_ios_app_config(self, app_id): platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME, app_id=app_id) def _get_app_config(self, platform_resource_name, app_id): - path = '/v1beta1/projects/-/{0}/{1}/config'.format(platform_resource_name, app_id) + path = f'/v1beta1/projects/-/{platform_resource_name}/{app_id}/config' response = self._make_request('get', path) # In Python 2.7, the base64 module works with strings, while in Python 3, it works with # bytes objects. This line works in both versions. return base64.standard_b64decode(response['configFileContents']).decode(encoding='utf-8') def get_sha_certificates(self, app_id): - path = '/v1beta1/projects/-/androidApps/{0}/sha'.format(app_id) + path = f'/v1beta1/projects/-/androidApps/{app_id}/sha' response = self._make_request('get', path) cert_list = response.get('certificates') or [] return [SHACertificate(sha_hash=cert['shaHash'], name=cert['name']) for cert in cert_list] def add_sha_certificate(self, app_id, certificate_to_add): - path = '/v1beta1/projects/-/androidApps/{0}/sha'.format(app_id) + path = f'/v1beta1/projects/-/androidApps/{app_id}/sha' sha_hash = _check_not_none(certificate_to_add, 'certificate_to_add').sha_hash cert_type = certificate_to_add.cert_type request_body = {'shaHash': sha_hash, 'certType': cert_type} @@ -650,7 +649,7 @@ def add_sha_certificate(self, app_id, certificate_to_add): def delete_sha_certificate(self, certificate_to_delete): name = _check_not_none(certificate_to_delete, 'certificate_to_delete').name - path = '/v1beta1/{0}'.format(name) + path = f'/v1beta1/{name}' self._make_request('delete', path) def _make_request(self, method, url, json=None): diff --git a/firebase_admin/remote_config.py b/firebase_admin/remote_config.py index 943141ccf..880804d3d 100644 --- a/firebase_admin/remote_config.py +++ b/firebase_admin/remote_config.py @@ -251,7 +251,7 @@ def __init__(self, app): 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__), } + 'X-FIREBASE-CLIENT': f'fire-admin-python/{firebase_admin.__version__}', } timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS) self._client = _http_client.JsonHttpClient(credential=app_credential, @@ -268,14 +268,12 @@ async def get_server_template(self): 'get', self._get_url()) except requests.exceptions.RequestException as error: raise self._handle_remote_config_error(error) - else: - template_data['etag'] = headers.get('etag') - return _ServerTemplateData(template_data) + template_data['etag'] = headers.get('etag') + return _ServerTemplateData(template_data) def _get_url(self): """Returns project prefix for url, in the format of /v1/projects/${projectId}""" - return "/v1/projects/{0}/namespaces/firebase-server/serverRemoteConfig".format( - self._project_id) + return f"/v1/projects/{self._project_id}/namespaces/firebase-server/serverRemoteConfig" @classmethod def _handle_remote_config_error(cls, error: Any): diff --git a/firebase_admin/storage.py b/firebase_admin/storage.py index 567a6abad..d2f004be6 100644 --- a/firebase_admin/storage.py +++ b/firebase_admin/storage.py @@ -82,6 +82,6 @@ def bucket(self, name=None): 'name explicitly when calling the storage.bucket() function.') if not bucket_name or not isinstance(bucket_name, str): raise ValueError( - 'Invalid storage bucket name: "{0}". Bucket name must be a non-empty ' - 'string.'.format(bucket_name)) + f'Invalid storage bucket name: "{bucket_name}". Bucket name must be a non-empty ' + 'string.') return self._client.bucket(bucket_name) diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index 133e80b45..9e713d988 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -205,7 +205,7 @@ class Tenant: def __init__(self, data): if not isinstance(data, dict): - raise ValueError('Invalid data argument in Tenant constructor: {0}'.format(data)) + raise ValueError(f'Invalid data argument in Tenant constructor: {data}') if not 'name' in data: raise ValueError('Tenant response missing required keys.') @@ -236,8 +236,8 @@ class _TenantManagementService: def __init__(self, app): credential = app.credential.get_credential() - version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__) - base_url = '{0}/projects/{1}'.format(self.TENANT_MGT_URL, app.project_id) + version_header = f'Python/Admin/{firebase_admin.__version__}' + base_url = f'{self.TENANT_MGT_URL}/projects/{app.project_id}' self.app = app self.client = _http_client.JsonHttpClient( credential=credential, base_url=base_url, headers={'X-Client-Version': version_header}) @@ -248,7 +248,7 @@ def auth_for_tenant(self, tenant_id): """Gets an Auth Client instance scoped to the given tenant ID.""" if not isinstance(tenant_id, str) or not tenant_id: raise ValueError( - 'Invalid tenant ID: {0}. Tenant ID must be a non-empty string.'.format(tenant_id)) + f'Invalid tenant ID: {tenant_id}. Tenant ID must be a non-empty string.') with self.lock: if tenant_id in self.tenant_clients: @@ -262,14 +262,13 @@ def get_tenant(self, tenant_id): """Gets the tenant corresponding to the given ``tenant_id``.""" if not isinstance(tenant_id, str) or not tenant_id: raise ValueError( - 'Invalid tenant ID: {0}. Tenant ID must be a non-empty string.'.format(tenant_id)) + f'Invalid tenant ID: {tenant_id}. Tenant ID must be a non-empty string.') try: - body = self.client.body('get', '/tenants/{0}'.format(tenant_id)) + body = self.client.body('get', f'/tenants/{tenant_id}') except requests.exceptions.RequestException as error: raise _auth_utils.handle_auth_backend_error(error) - else: - return Tenant(body) + return Tenant(body) def create_tenant( self, display_name, allow_password_sign_up=None, enable_email_link_sign_in=None): @@ -287,8 +286,7 @@ def create_tenant( body = self.client.body('post', '/tenants', json=payload) except requests.exceptions.RequestException as error: raise _auth_utils.handle_auth_backend_error(error) - else: - return Tenant(body) + return Tenant(body) def update_tenant( self, tenant_id, display_name=None, allow_password_sign_up=None, @@ -310,24 +308,23 @@ def update_tenant( if not payload: raise ValueError('At least one parameter must be specified for update.') - url = '/tenants/{0}'.format(tenant_id) + url = f'/tenants/{tenant_id}' update_mask = ','.join(_auth_utils.build_update_mask(payload)) - params = 'updateMask={0}'.format(update_mask) + params = f'updateMask={update_mask}' try: body = self.client.body('patch', url, json=payload, params=params) except requests.exceptions.RequestException as error: raise _auth_utils.handle_auth_backend_error(error) - else: - return Tenant(body) + return Tenant(body) def delete_tenant(self, tenant_id): """Deletes the tenant corresponding to the given ``tenant_id``.""" if not isinstance(tenant_id, str) or not tenant_id: raise ValueError( - 'Invalid tenant ID: {0}. Tenant ID must be a non-empty string.'.format(tenant_id)) + f'Invalid tenant ID: {tenant_id}. Tenant ID must be a non-empty string.') try: - self.client.request('delete', '/tenants/{0}'.format(tenant_id)) + self.client.request('delete', f'/tenants/{tenant_id}') except requests.exceptions.RequestException as error: raise _auth_utils.handle_auth_backend_error(error) @@ -341,7 +338,7 @@ def list_tenants(self, page_token=None, max_results=_MAX_LIST_TENANTS_RESULTS): if max_results < 1 or max_results > _MAX_LIST_TENANTS_RESULTS: raise ValueError( 'Max results must be a positive integer less than or equal to ' - '{0}.'.format(_MAX_LIST_TENANTS_RESULTS)) + f'{_MAX_LIST_TENANTS_RESULTS}.') payload = {'pageSize': max_results} if page_token: diff --git a/integration/conftest.py b/integration/conftest.py index 169e02d5b..ebaf9297a 100644 --- a/integration/conftest.py +++ b/integration/conftest.py @@ -36,7 +36,7 @@ def _get_cert_path(request): def integration_conf(request): cert_path = _get_cert_path(request) - with open(cert_path) as cert: + with open(cert_path, encoding='utf-8') as cert: project_id = json.load(cert).get('project_id') if not project_id: raise ValueError('Failed to determine project ID from service account certificate.') @@ -57,8 +57,8 @@ def default_app(request): """ cred, project_id = integration_conf(request) ops = { - 'databaseURL' : 'https://{0}.firebaseio.com'.format(project_id), - 'storageBucket' : '{0}.appspot.com'.format(project_id) + 'databaseURL' : f'https://{project_id}.firebaseio.com', + 'storageBucket' : f'{project_id}.appspot.com' } return firebase_admin.initialize_app(cred, ops) @@ -68,5 +68,5 @@ def api_key(request): if not path: raise ValueError('API key file not specified. Make sure to specify the "--apikey" ' 'command-line option.') - with open(path) as keyfile: + with open(path, encoding='utf-8') as keyfile: return keyfile.read().strip() diff --git a/integration/test_auth.py b/integration/test_auth.py index e1d01a254..7f4725dfe 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -30,6 +30,7 @@ import firebase_admin from firebase_admin import auth from firebase_admin import credentials +from firebase_admin._http_client import DEFAULT_TIMEOUT_SECONDS as timeout _verify_token_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken' @@ -67,14 +68,14 @@ def _sign_in(custom_token, api_key): body = {'token' : custom_token.decode(), 'returnSecureToken' : True} params = {'key' : api_key} - resp = requests.request('post', _verify_token_url, params=params, json=body) + resp = requests.request('post', _verify_token_url, params=params, json=body, timeout=timeout) resp.raise_for_status() return resp.json().get('idToken') def _sign_in_with_password(email, password, api_key): body = {'email': email, 'password': password, 'returnSecureToken': True} params = {'key' : api_key} - resp = requests.request('post', _verify_password_url, params=params, json=body) + resp = requests.request('post', _verify_password_url, params=params, json=body, timeout=timeout) resp.raise_for_status() return resp.json().get('idToken') @@ -84,7 +85,7 @@ def _random_string(length=10): def _random_id(): random_id = str(uuid.uuid4()).lower().replace('-', '') - email = 'test{0}@example.{1}.com'.format(random_id[:12], random_id[12:]) + email = f'test{random_id[:12]}@example.{random_id[12:]}.com' return random_id, email def _random_phone(): @@ -93,21 +94,21 @@ def _random_phone(): def _reset_password(oob_code, new_password, api_key): body = {'oobCode': oob_code, 'newPassword': new_password} params = {'key' : api_key} - resp = requests.request('post', _password_reset_url, params=params, json=body) + resp = requests.request('post', _password_reset_url, params=params, json=body, timeout=timeout) resp.raise_for_status() return resp.json().get('email') def _verify_email(oob_code, api_key): body = {'oobCode': oob_code} params = {'key' : api_key} - resp = requests.request('post', _verify_email_url, params=params, json=body) + resp = requests.request('post', _verify_email_url, params=params, json=body, timeout=timeout) resp.raise_for_status() return resp.json().get('email') def _sign_in_with_email_link(email, oob_code, api_key): body = {'oobCode': oob_code, 'email': email} params = {'key' : api_key} - resp = requests.request('post', _email_sign_in_url, params=params, json=body) + resp = requests.request('post', _email_sign_in_url, params=params, json=body, timeout=timeout) resp.raise_for_status() return resp.json().get('idToken') @@ -870,7 +871,7 @@ def test_delete_saml_provider_config(): def _create_oidc_provider_config(): - provider_id = 'oidc.{0}'.format(_random_string()) + provider_id = f'oidc.{_random_string()}' return auth.create_oidc_provider_config( provider_id=provider_id, client_id='OIDC_CLIENT_ID', @@ -882,7 +883,7 @@ def _create_oidc_provider_config(): def _create_saml_provider_config(): - provider_id = 'saml.{0}'.format(_random_string()) + provider_id = f'saml.{_random_string()}' return auth.create_saml_provider_config( provider_id=provider_id, idp_entity_id='IDP_ENTITY_ID', diff --git a/integration/test_db.py b/integration/test_db.py index 0170743dd..1ceb0b992 100644 --- a/integration/test_db.py +++ b/integration/test_db.py @@ -39,7 +39,7 @@ def integration_conf(request): def app(request): cred, project_id = integration_conf(request) ops = { - 'databaseURL' : 'https://{0}.firebaseio.com'.format(project_id), + 'databaseURL' : f'https://{project_id}.firebaseio.com', } return firebase_admin.initialize_app(cred, ops, name='integration-db') @@ -53,7 +53,7 @@ def default_app(): @pytest.fixture(scope='module') def update_rules(app): - with open(testutils.resource_filename('dinosaurs_index.json')) as rules_file: + with open(testutils.resource_filename('dinosaurs_index.json'), encoding='utf-8') as rules_file: new_rules = json.load(rules_file) client = db.reference('', app)._client rules = client.body('get', '/.settings/rules.json', params='format=strict') @@ -64,7 +64,7 @@ def update_rules(app): @pytest.fixture(scope='module') def testdata(): - with open(testutils.resource_filename('dinosaurs.json')) as dino_file: + with open(testutils.resource_filename('dinosaurs.json'), encoding='utf-8') as dino_file: return json.load(dino_file) @pytest.fixture(scope='module') @@ -195,8 +195,8 @@ def test_update_nested_children(self, testref): edward = python.child('users').push({'name' : 'Edward Cope', 'since' : 1800}) jack = python.child('users').push({'name' : 'Jack Horner', 'since' : 1940}) delta = { - '{0}/since'.format(edward.key) : 1840, - '{0}/since'.format(jack.key) : 1946 + f'{edward.key}/since' : 1840, + f'{jack.key}/since' : 1946 } python.child('users').update(delta) assert edward.get() == {'name' : 'Edward Cope', 'since' : 1840} @@ -363,7 +363,7 @@ def override_app(request, update_rules): del update_rules cred, project_id = integration_conf(request) ops = { - 'databaseURL' : 'https://{0}.firebaseio.com'.format(project_id), + 'databaseURL' : f'https://{project_id}.firebaseio.com', 'databaseAuthVariableOverride' : {'uid' : 'user1'} } app = firebase_admin.initialize_app(cred, ops, 'db-override') @@ -375,7 +375,7 @@ def none_override_app(request, update_rules): del update_rules cred, project_id = integration_conf(request) ops = { - 'databaseURL' : 'https://{0}.firebaseio.com'.format(project_id), + 'databaseURL' : f'https://{project_id}.firebaseio.com', 'databaseAuthVariableOverride' : None } app = firebase_admin.initialize_app(cred, ops, 'db-none-override') diff --git a/integration/test_firestore.py b/integration/test_firestore.py index fd39d9b8a..96cdd3fb1 100644 --- a/integration/test_firestore.py +++ b/integration/test_firestore.py @@ -18,16 +18,16 @@ from firebase_admin import firestore _CITY = { - 'name': u'Mountain View', - 'country': u'USA', + 'name': 'Mountain View', + 'country': 'USA', 'population': 77846, 'capital': False } _MOVIE = { - 'Name': u'Interstellar', + 'Name': 'Interstellar', 'Year': 2014, - 'Runtime': u'2h 49m', + 'Runtime': '2h 49m', 'Academy Award Winner': True } @@ -35,8 +35,8 @@ def test_firestore(): client = firestore.client() expected = { - 'name': u'Mountain View', - 'country': u'USA', + 'name': 'Mountain View', + 'country': 'USA', 'population': 77846, 'capital': False } @@ -93,7 +93,7 @@ def test_firestore_multi_db(): def test_server_timestamp(): client = firestore.client() expected = { - 'name': u'Mountain View', + 'name': 'Mountain View', 'timestamp': firestore.SERVER_TIMESTAMP # pylint: disable=no-member } doc = client.collection('cities').document() diff --git a/integration/test_firestore_async.py b/integration/test_firestore_async.py index 584ef590a..e899f25b2 100644 --- a/integration/test_firestore_async.py +++ b/integration/test_firestore_async.py @@ -20,16 +20,16 @@ from firebase_admin import firestore_async _CITY = { - 'name': u'Mountain View', - 'country': u'USA', + 'name': 'Mountain View', + 'country': 'USA', 'population': 77846, 'capital': False } _MOVIE = { - 'Name': u'Interstellar', + 'Name': 'Interstellar', 'Year': 2014, - 'Runtime': u'2h 49m', + 'Runtime': '2h 49m', 'Academy Award Winner': True } @@ -102,7 +102,7 @@ async def test_firestore_async_multi_db(): async def test_server_timestamp(): client = firestore_async.client() expected = { - 'name': u'Mountain View', + 'name': 'Mountain View', 'timestamp': firestore_async.SERVER_TIMESTAMP # pylint: disable=no-member } doc = client.collection('cities').document() diff --git a/integration/test_messaging.py b/integration/test_messaging.py index 7ab707c82..e72086741 100644 --- a/integration/test_messaging.py +++ b/integration/test_messaging.py @@ -121,7 +121,7 @@ def test_send_each(): def test_send_each_500(): messages = [] for msg_number in range(500): - topic = 'foo-bar-{0}'.format(msg_number % 10) + topic = f'foo-bar-{msg_number % 10}' messages.append(messaging.Message(topic=topic)) batch_response = messaging.send_each(messages, dry_run=True) @@ -193,7 +193,7 @@ async def test_send_each_async(): async def test_send_each_async_500(): messages = [] for msg_number in range(500): - topic = 'foo-bar-{0}'.format(msg_number % 10) + topic = f'foo-bar-{msg_number % 10}' messages.append(messaging.Message(topic=topic)) batch_response = await messaging.send_each_async(messages, dry_run=True) diff --git a/integration/test_ml.py b/integration/test_ml.py index 6deb22a69..ea5b10be9 100644 --- a/integration/test_ml.py +++ b/integration/test_ml.py @@ -37,7 +37,7 @@ def _random_identifier(prefix): #pylint: disable=unused-variable suffix = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(8)]) - return '{0}_{1}'.format(prefix, suffix) + return f'{prefix}_{suffix}' NAME_ONLY_ARGS = { @@ -170,7 +170,7 @@ def test_create_already_existing_fails(firebase_model): ml.create_model(model=firebase_model) check_operation_error( excinfo, - 'Model \'{0}\' already exists'.format(firebase_model.display_name)) + f'Model \'{firebase_model.display_name}\' already exists') @pytest.mark.parametrize('firebase_model', [INVALID_FULL_MODEL_ARGS], indirect=True) @@ -219,7 +219,7 @@ def test_update_non_existing_model(firebase_model): ml.update_model(firebase_model) check_operation_error( excinfo, - 'Model \'{0}\' was not found'.format(firebase_model.as_dict().get('name'))) + f'Model \'{firebase_model.as_dict().get("name")}\' was not found') @pytest.mark.parametrize('firebase_model', [FULL_MODEL_ARGS], indirect=True) @@ -252,18 +252,17 @@ def test_publish_unpublish_non_existing_model(firebase_model): ml.publish_model(firebase_model.model_id) check_operation_error( excinfo, - 'Model \'{0}\' was not found'.format(firebase_model.as_dict().get('name'))) + f'Model \'{firebase_model.as_dict().get("name")}\' was not found') with pytest.raises(exceptions.NotFoundError) as excinfo: ml.unpublish_model(firebase_model.model_id) check_operation_error( excinfo, - 'Model \'{0}\' was not found'.format(firebase_model.as_dict().get('name'))) + f'Model \'{firebase_model.as_dict().get("name")}\' was not found') def test_list_models(model_list): - filter_str = 'displayName={0} OR tags:{1}'.format( - model_list[0].display_name, model_list[1].tags[0]) + filter_str = f'displayName={model_list[0].display_name} OR tags:{model_list[1].tags[0]}' all_models = ml.list_models(list_filter=filter_str) all_model_ids = [mdl.model_id for mdl in all_models.iterate_all()] diff --git a/integration/test_project_management.py b/integration/test_project_management.py index b0b7fa52a..ba2c5ec16 100644 --- a/integration/test_project_management.py +++ b/integration/test_project_management.py @@ -74,14 +74,13 @@ def test_create_android_app_already_exists(android_app): def test_android_set_display_name_and_get_metadata(android_app, project_id): app_id = android_app.app_id android_app = project_management.android_app(app_id) - new_display_name = '{0} helloworld {1}'.format( - TEST_APP_DISPLAY_NAME_PREFIX, random.randint(0, 10000)) + new_display_name = f'{TEST_APP_DISPLAY_NAME_PREFIX} helloworld {random.randint(0, 10000)}' android_app.set_display_name(new_display_name) metadata = project_management.android_app(app_id).get_metadata() android_app.set_display_name(TEST_APP_DISPLAY_NAME_PREFIX) # Revert the display name. - assert metadata._name == 'projects/{0}/androidApps/{1}'.format(project_id, app_id) + assert metadata._name == f'projects/{project_id}/androidApps/{app_id}' assert metadata.app_id == app_id assert metadata.project_id == project_id assert metadata.display_name == new_display_name @@ -149,15 +148,13 @@ def test_create_ios_app_already_exists(ios_app): def test_ios_set_display_name_and_get_metadata(ios_app, project_id): app_id = ios_app.app_id ios_app = project_management.ios_app(app_id) - new_display_name = '{0} helloworld {1}'.format( - TEST_APP_DISPLAY_NAME_PREFIX, random.randint(0, 10000)) + new_display_name = f'{TEST_APP_DISPLAY_NAME_PREFIX} helloworld {random.randint(0, 10000)}' ios_app.set_display_name(new_display_name) metadata = project_management.ios_app(app_id).get_metadata() ios_app.set_display_name(TEST_APP_DISPLAY_NAME_PREFIX) # Revert the display name. - assert metadata._name == 'projects/{0}/iosApps/{1}'.format(project_id, app_id) - assert metadata.app_id == app_id + assert metadata._name == f'projects/{project_id}/iosApps/{app_id}' assert metadata.project_id == project_id assert metadata.display_name == new_display_name assert metadata.bundle_id == TEST_APP_BUNDLE_ID diff --git a/integration/test_storage.py b/integration/test_storage.py index 4f0faf76c..32e4d86a3 100644 --- a/integration/test_storage.py +++ b/integration/test_storage.py @@ -20,10 +20,10 @@ def test_default_bucket(project_id): bucket = storage.bucket() - _verify_bucket(bucket, '{0}.appspot.com'.format(project_id)) + _verify_bucket(bucket, f'{project_id}.appspot.com') def test_custom_bucket(project_id): - bucket_name = '{0}.appspot.com'.format(project_id) + bucket_name = f'{project_id}.appspot.com' bucket = storage.bucket(bucket_name) _verify_bucket(bucket, bucket_name) @@ -33,7 +33,7 @@ def test_non_existing_bucket(): def _verify_bucket(bucket, expected_name): assert bucket.name == expected_name - file_name = 'data_{0}.txt'.format(int(time.time())) + file_name = f'data_{int(time.time())}.txt' blob = bucket.blob(file_name) blob.upload_from_string('Hello World') diff --git a/integration/test_tenant_mgt.py b/integration/test_tenant_mgt.py index c9eefd96e..f0bad58b2 100644 --- a/integration/test_tenant_mgt.py +++ b/integration/test_tenant_mgt.py @@ -25,6 +25,7 @@ from firebase_admin import auth from firebase_admin import tenant_mgt +from firebase_admin._http_client import DEFAULT_TIMEOUT_SECONDS as timeout from integration import test_auth @@ -359,7 +360,7 @@ def test_delete_saml_provider_config(sample_tenant): def _create_oidc_provider_config(client): - provider_id = 'oidc.{0}'.format(_random_string()) + provider_id = f'oidc.{_random_string()}' return client.create_oidc_provider_config( provider_id=provider_id, client_id='OIDC_CLIENT_ID', @@ -369,7 +370,7 @@ def _create_oidc_provider_config(client): def _create_saml_provider_config(client): - provider_id = 'saml.{0}'.format(_random_string()) + provider_id = f'saml.{_random_string()}' return client.create_saml_provider_config( provider_id=provider_id, idp_entity_id='IDP_ENTITY_ID', @@ -387,7 +388,7 @@ def _random_uid(): def _random_email(): random_id = str(uuid.uuid4()).lower().replace('-', '') - return 'test{0}@example.{1}.com'.format(random_id[:12], random_id[12:]) + return f'test{random_id[:12]}@example.{random_id[12:]}.com' def _random_phone(): @@ -412,6 +413,6 @@ def _sign_in(custom_token, tenant_id, api_key): 'tenantId': tenant_id, } params = {'key' : api_key} - resp = requests.request('post', VERIFY_TOKEN_URL, params=params, json=body) + resp = requests.request('post', VERIFY_TOKEN_URL, params=params, json=body, timeout=timeout) resp.raise_for_status() return resp.json().get('idToken') diff --git a/requirements.txt b/requirements.txt index 76eeb7582..3e67d1dd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -astroid == 2.5.8 -pylint == 2.7.4 +astroid == 3.3.10 +pylint == 3.3.7 pytest >= 8.2.2 pytest-cov >= 2.4.0 pytest-localserver >= 0.4.1 diff --git a/snippets/auth/get_service_account_tokens.py b/snippets/auth/get_service_account_tokens.py index 9f60590fe..7ad67a093 100644 --- a/snippets/auth/get_service_account_tokens.py +++ b/snippets/auth/get_service_account_tokens.py @@ -26,4 +26,4 @@ # After expiration_time, you must generate a new access token # [END get_service_account_tokens] -print('The access token {} expires at {}'.format(access_token, expiration_time)) +print(f'The access token {access_token} expires at {expiration_time}') diff --git a/snippets/auth/index.py b/snippets/auth/index.py index ed324e486..6a509b8f5 100644 --- a/snippets/auth/index.py +++ b/snippets/auth/index.py @@ -169,7 +169,7 @@ def revoke_refresh_token_uid(): user = auth.get_user(uid) # Convert to seconds as the auth_time in the token claims is in seconds. revocation_second = user.tokens_valid_after_timestamp / 1000 - print('Tokens revoked at: {0}'.format(revocation_second)) + print(f'Tokens revoked at: {revocation_second}') # [END revoke_tokens] # [START save_revocation_in_db] metadata_ref = firebase_admin.db.reference("metadata/" + uid) @@ -183,7 +183,7 @@ def get_user(uid): from firebase_admin import auth user = auth.get_user(uid) - print('Successfully fetched user data: {0}'.format(user.uid)) + print(f'Successfully fetched user data: {user.uid}') # [END get_user] def get_user_by_email(): @@ -192,7 +192,7 @@ def get_user_by_email(): from firebase_admin import auth user = auth.get_user_by_email(email) - print('Successfully fetched user data: {0}'.format(user.uid)) + print(f'Successfully fetched user data: {user.uid}') # [END get_user_by_email] def bulk_get_users(): @@ -221,7 +221,7 @@ def get_user_by_phone_number(): from firebase_admin import auth user = auth.get_user_by_phone_number(phone) - print('Successfully fetched user data: {0}'.format(user.uid)) + print(f'Successfully fetched user data: {user.uid}') # [END get_user_by_phone] def create_user(): @@ -234,7 +234,7 @@ def create_user(): display_name='John Doe', photo_url='http://www.example.com/12345678/photo.png', disabled=False) - print('Sucessfully created new user: {0}'.format(user.uid)) + print(f'Sucessfully created new user: {user.uid}') # [END create_user] return user.uid @@ -242,7 +242,7 @@ def create_user_with_id(): # [START create_user_with_id] user = auth.create_user( uid='some-uid', email='user@example.com', phone_number='+15555550100') - print('Sucessfully created new user: {0}'.format(user.uid)) + print(f'Sucessfully created new user: {user.uid}') # [END create_user_with_id] def update_user(uid): @@ -256,7 +256,7 @@ def update_user(uid): display_name='John Doe', photo_url='http://www.example.com/12345678/photo.png', disabled=True) - print('Sucessfully updated user: {0}'.format(user.uid)) + print(f'Sucessfully updated user: {user.uid}') # [END update_user] def delete_user(uid): @@ -271,10 +271,10 @@ def bulk_delete_users(): result = auth.delete_users(["uid1", "uid2", "uid3"]) - print('Successfully deleted {0} users'.format(result.success_count)) - print('Failed to delete {0} users'.format(result.failure_count)) + print(f'Successfully deleted {result.success_count} users') + print(f'Failed to delete {result.failure_count} users') for err in result.errors: - print('error #{0}, reason: {1}'.format(result.index, result.reason)) + print(f'error #{result.index}, reason: {result.reason}') # [END bulk_delete_users] def set_custom_user_claims(uid): @@ -475,10 +475,11 @@ def import_users(): hash_alg = auth.UserImportHash.hmac_sha256(key=b'secret_key') try: result = auth.import_users(users, hash_alg=hash_alg) - print('Successfully imported {0} users. Failed to import {1} users.'.format( - result.success_count, result.failure_count)) + print( + f'Successfully imported {result.success_count} users. Failed to import ' + f'{result.failure_count} users.') for err in result.errors: - print('Failed to import {0} due to {1}'.format(users[err.index].uid, err.reason)) + print(f'Failed to import {users[err.index].uid} due to {err.reason}') except exceptions.FirebaseError: # Some unrecoverable error occurred that prevented the operation from running. pass @@ -1012,7 +1013,7 @@ def revoke_refresh_tokens_tenant(tenant_client, uid): user = tenant_client.get_user(uid) # Convert to seconds as the auth_time in the token claims is in seconds. revocation_second = user.tokens_valid_after_timestamp / 1000 - print('Tokens revoked at: {0}'.format(revocation_second)) + print(f'Tokens revoked at: {revocation_second}') # [END revoke_tokens_tenant] def verify_id_token_and_check_revoked_tenant(tenant_client, id_token): diff --git a/snippets/database/index.py b/snippets/database/index.py index adfa13476..99bb4981e 100644 --- a/snippets/database/index.py +++ b/snippets/database/index.py @@ -235,7 +235,7 @@ def order_by_child(): ref = db.reference('dinosaurs') snapshot = ref.order_by_child('height').get() for key, val in snapshot.items(): - print('{0} was {1} meters tall'.format(key, val)) + print(f'{key} was {val} meters tall') # [END order_by_child] def order_by_nested_child(): @@ -243,7 +243,7 @@ def order_by_nested_child(): ref = db.reference('dinosaurs') snapshot = ref.order_by_child('dimensions/height').get() for key, val in snapshot.items(): - print('{0} was {1} meters tall'.format(key, val)) + print(f'{key} was {val} meters tall') # [END order_by_nested_child] def order_by_key(): @@ -258,7 +258,7 @@ def order_by_value(): ref = db.reference('scores') snapshot = ref.order_by_value().get() for key, val in snapshot.items(): - print('The {0} dinosaur\'s score is {1}'.format(key, val)) + print(f'The {key} dinosaur\'s score is {val}') # [END order_by_value] def limit_query(): @@ -280,7 +280,7 @@ def limit_query(): scores_ref = db.reference('scores') snapshot = scores_ref.order_by_value().limit_to_last(3).get() for key, val in snapshot.items(): - print('The {0} dinosaur\'s score is {1}'.format(key, val)) + print(f'The {key} dinosaur\'s score is {val}') # [END limit_query_3] def range_query(): @@ -300,7 +300,7 @@ def range_query(): # [START range_query_3] ref = db.reference('dinosaurs') - snapshot = ref.order_by_key().start_at('b').end_at(u'b\uf8ff').get() + snapshot = ref.order_by_key().start_at('b').end_at('b\uf8ff').get() for key in snapshot: print(key) # [END range_query_3] @@ -322,7 +322,7 @@ def complex_query(): # Data is ordered by increasing height, so we want the first entry. # Second entry is stegosarus. for key in snapshot: - print('The dinosaur just shorter than the stegosaurus is {0}'.format(key)) + print(f'The dinosaur just shorter than the stegosaurus is {key}') return else: print('The stegosaurus is the shortest dino') diff --git a/snippets/messaging/cloud_messaging.py b/snippets/messaging/cloud_messaging.py index 18a992dcc..3efd223ea 100644 --- a/snippets/messaging/cloud_messaging.py +++ b/snippets/messaging/cloud_messaging.py @@ -241,7 +241,7 @@ def send_each(): response = messaging.send_each(messages) # See the BatchResponse reference documentation # for the contents of response. - print('{0} messages were sent successfully'.format(response.success_count)) + print(f'{response.success_count} messages were sent successfully') # [END send_each] @@ -262,7 +262,7 @@ def send_each_for_multicast(): response = messaging.send_each_for_multicast(message) # See the BatchResponse reference documentation # for the contents of response. - print('{0} messages were sent successfully'.format(response.success_count)) + print(f'{response.success_count} messages were sent successfully') # [END send_each_for_multicast] @@ -287,5 +287,5 @@ def send_each_for_multicast_and_handle_errors(): if not resp.success: # The order of responses corresponds to the order of the registration tokens. failed_tokens.append(registration_tokens[idx]) - print('List of tokens that caused failures: {0}'.format(failed_tokens)) + print(f'List of tokens that caused failures: {failed_tokens}') # [END send_each_for_multicast_error] diff --git a/tests/test_app.py b/tests/test_app.py index 5b203661f..0ff0854b4 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -215,11 +215,11 @@ def revert_config_env(config_old): class TestFirebaseApp: """Test cases for App initialization and life cycle.""" - invalid_credentials = ['', 'foo', 0, 1, dict(), list(), tuple(), True, False] - invalid_options = ['', 0, 1, list(), tuple(), True, False] - invalid_names = [None, '', 0, 1, dict(), list(), tuple(), True, False] + invalid_credentials = ['', 'foo', 0, 1, {}, [], tuple(), True, False] + invalid_options = ['', 0, 1, [], tuple(), True, False] + invalid_names = [None, '', 0, 1, {}, [], tuple(), True, False] invalid_apps = [ - None, '', 0, 1, dict(), list(), tuple(), True, False, + None, '', 0, 1, {}, [], tuple(), True, False, firebase_admin.App('uninitialized', CREDENTIAL, {}) ] @@ -308,11 +308,11 @@ def test_project_id_from_environment(self): variables = ['GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT'] for idx, var in enumerate(variables): old_project_id = os.environ.get(var) - new_project_id = 'env-project-{0}'.format(idx) + new_project_id = f'env-project-{idx}' os.environ[var] = new_project_id try: app = firebase_admin.initialize_app( - testutils.MockCredential(), name='myApp{0}'.format(var)) + testutils.MockCredential(), name=f'myApp{var}') assert app.project_id == new_project_id finally: if old_project_id: @@ -388,7 +388,7 @@ def test_app_services(self, init_app): with pytest.raises(ValueError): _utils.get_app_service(init_app, 'test.service', AppService) - @pytest.mark.parametrize('arg', [0, 1, True, False, 'str', list(), dict(), tuple()]) + @pytest.mark.parametrize('arg', [0, 1, True, False, 'str', [], {}, tuple()]) def test_app_services_invalid_arg(self, arg): with pytest.raises(ValueError): _utils.get_app_service(arg, 'test.service', AppService) diff --git a/tests/test_app_check.py b/tests/test_app_check.py index 168d0a972..e55ae39de 100644 --- a/tests/test_app_check.py +++ b/tests/test_app_check.py @@ -22,7 +22,7 @@ from firebase_admin import app_check from tests import testutils -NON_STRING_ARGS = [list(), tuple(), dict(), True, False, 1, 0] +NON_STRING_ARGS = [[], tuple(), {}, True, False, 1, 0] APP_ID = "1234567890" PROJECT_ID = "1334" @@ -71,7 +71,7 @@ def evaluate(): def test_verify_token_with_non_string_raises_error(self, token): with pytest.raises(ValueError) as excinfo: app_check.verify_token(token) - expected = 'app check token "{0}" must be a string.'.format(token) + expected = f'app check token "{token}" must be a string.' assert str(excinfo.value) == expected def test_has_valid_token_headers(self): diff --git a/tests/test_auth_providers.py b/tests/test_auth_providers.py index 304e0fd78..106e1cae3 100644 --- a/tests/test_auth_providers.py +++ b/tests/test_auth_providers.py @@ -27,8 +27,7 @@ ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v2' EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST' AUTH_EMULATOR_HOST = 'localhost:9099' -EMULATED_ID_TOOLKIT_URL = 'http://{}/identitytoolkit.googleapis.com/v2'.format( - AUTH_EMULATOR_HOST) +EMULATED_ID_TOOLKIT_URL = f'http://{AUTH_EMULATOR_HOST}/identitytoolkit.googleapis.com/v2' URL_PROJECT_SUFFIX = '/projects/mock-project-id' USER_MGT_URLS = { 'ID_TOOLKIT': ID_TOOLKIT_URL, @@ -45,7 +44,7 @@ } }""" -INVALID_PROVIDER_IDS = [None, True, False, 1, 0, list(), tuple(), dict(), ''] +INVALID_PROVIDER_IDS = [None, True, False, 1, 0, [], tuple(), {}, ''] @pytest.fixture(scope='module', params=[{'emulated': False}, {'emulated': True}]) @@ -282,12 +281,12 @@ def test_delete(self, user_mgt_app): _assert_request(recorder[0], 'DELETE', f'{USER_MGT_URLS["PREFIX"]}/oauthIdpConfigs/oidc.provider') - @pytest.mark.parametrize('arg', [None, 'foo', list(), dict(), 0, -1, 101, False]) + @pytest.mark.parametrize('arg', [None, 'foo', [], {}, 0, -1, 101, False]) def test_invalid_max_results(self, user_mgt_app, arg): with pytest.raises(ValueError): auth.list_oidc_provider_configs(max_results=arg, app=user_mgt_app) - @pytest.mark.parametrize('arg', ['', list(), dict(), 0, -1, 101, False]) + @pytest.mark.parametrize('arg', ['', [], {}, 0, -1, 101, False]) def test_invalid_page_token(self, user_mgt_app, arg): with pytest.raises(ValueError): auth.list_oidc_provider_configs(page_token=arg, app=user_mgt_app) @@ -346,7 +345,7 @@ def test_paged_iteration(self, user_mgt_app): for index in range(2): provider_config = next(iterator) - assert provider_config.provider_id == 'oidc.provider{0}'.format(index) + assert provider_config.provider_id == f'oidc.provider{index}' assert len(recorder) == 1 _assert_request(recorder[0], 'GET', f'{USER_MGT_URLS["PREFIX"]}/oauthIdpConfigs?pageSize=100') @@ -403,7 +402,7 @@ def _assert_page(self, page, count=2, start=0, next_page_token=''): index = start assert len(page.provider_configs) == count for provider_config in page.provider_configs: - self._assert_provider_config(provider_config, want_id='oidc.provider{0}'.format(index)) + self._assert_provider_config(provider_config, want_id=f'oidc.provider{index}') index += 1 if next_page_token: @@ -621,12 +620,12 @@ def test_config_not_found(self, user_mgt_app): assert excinfo.value.http_response is not None assert excinfo.value.cause is not None - @pytest.mark.parametrize('arg', [None, 'foo', list(), dict(), 0, -1, 101, False]) + @pytest.mark.parametrize('arg', [None, 'foo', [], {}, 0, -1, 101, False]) def test_invalid_max_results(self, user_mgt_app, arg): with pytest.raises(ValueError): auth.list_saml_provider_configs(max_results=arg, app=user_mgt_app) - @pytest.mark.parametrize('arg', ['', list(), dict(), 0, -1, 101, False]) + @pytest.mark.parametrize('arg', ['', [], {}, 0, -1, 101, False]) def test_invalid_page_token(self, user_mgt_app, arg): with pytest.raises(ValueError): auth.list_saml_provider_configs(page_token=arg, app=user_mgt_app) @@ -686,7 +685,7 @@ def test_paged_iteration(self, user_mgt_app): for index in range(2): provider_config = next(iterator) - assert provider_config.provider_id == 'saml.provider{0}'.format(index) + assert provider_config.provider_id == f'saml.provider{index}' assert len(recorder) == 1 _assert_request( recorder[0], 'GET', f'{USER_MGT_URLS["PREFIX"]}/inboundSamlConfigs?pageSize=100') @@ -735,7 +734,7 @@ def _assert_page(self, page, count=2, start=0, next_page_token=''): index = start assert len(page.provider_configs) == count for provider_config in page.provider_configs: - self._assert_provider_config(provider_config, want_id='saml.provider{0}'.format(index)) + self._assert_provider_config(provider_config, want_id=f'saml.provider{index}') index += 1 if next_page_token: diff --git a/tests/test_credentials.py b/tests/test_credentials.py index cceb6b6f9..1e1db6460 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -64,7 +64,7 @@ def test_init_from_invalid_certificate(self, file_name, error): with pytest.raises(error): credentials.Certificate(testutils.resource_filename(file_name)) - @pytest.mark.parametrize('arg', [None, 0, 1, True, False, list(), tuple(), dict()]) + @pytest.mark.parametrize('arg', [None, 0, 1, True, False, [], tuple(), {}]) def test_invalid_args(self, arg): with pytest.raises(ValueError): credentials.Certificate(arg) @@ -156,7 +156,7 @@ def test_init_from_invalid_file(self): credentials.RefreshToken( testutils.resource_filename('service_account.json')) - @pytest.mark.parametrize('arg', [None, 0, 1, True, False, list(), tuple(), dict()]) + @pytest.mark.parametrize('arg', [None, 0, 1, True, False, [], tuple(), {}]) def test_invalid_args(self, arg): with pytest.raises(ValueError): credentials.RefreshToken(arg) diff --git a/tests/test_db.py b/tests/test_db.py index 93f4672f1..abba3baa8 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -87,7 +87,7 @@ class TestReferencePath: } invalid_paths = [ - None, True, False, 0, 1, dict(), list(), tuple(), _Object(), + None, True, False, 0, 1, {}, [], tuple(), _Object(), 'foo#', 'foo.', 'foo$', 'foo[', 'foo]', ] @@ -98,7 +98,7 @@ class TestReferencePath: } invalid_children = [ - None, '', '/foo', '/foo/bar', True, False, 0, 1, dict(), list(), tuple(), + None, '', '/foo', '/foo/bar', True, False, 0, 1, {}, [], tuple(), 'foo#', 'foo.', 'foo$', 'foo[', 'foo]', _Object() ] @@ -248,7 +248,7 @@ def test_get_if_changed(self, data): self._assert_request(recorder[1], 'GET', 'https://test.firebaseio.com/test.json') assert recorder[1].headers['if-none-match'] == MockAdapter.ETAG - @pytest.mark.parametrize('etag', [0, 1, True, False, dict(), list(), tuple()]) + @pytest.mark.parametrize('etag', [0, 1, True, False, {}, [], tuple()]) def test_get_if_changed_invalid_etag(self, etag): ref = db.reference('/test') with pytest.raises(ValueError): @@ -347,7 +347,7 @@ def test_set_if_unchanged_failure(self, data): assert json.loads(recorder[0].body.decode()) == data assert recorder[0].headers['if-match'] == 'invalid-etag' - @pytest.mark.parametrize('etag', [0, 1, True, False, dict(), list(), tuple()]) + @pytest.mark.parametrize('etag', [0, 1, True, False, {}, [], tuple()]) def test_set_if_unchanged_invalid_etag(self, etag): ref = db.reference('/test') with pytest.raises(ValueError): @@ -369,7 +369,7 @@ def test_set_if_unchanged_non_json_value(self, value): ref.set_if_unchanged(MockAdapter.ETAG, value) @pytest.mark.parametrize('update', [ - None, {}, {None:'foo'}, '', 'foo', 0, 1, list(), tuple(), _Object() + None, {}, {None:'foo'}, '', 'foo', 0, 1, [], tuple(), _Object() ]) def test_set_invalid_update(self, update): ref = db.reference('/test') @@ -466,7 +466,7 @@ def test_transaction_abort(self): assert excinfo.value.http_response is None assert len(recorder) == 1 + 25 - @pytest.mark.parametrize('func', [None, 0, 1, True, False, 'foo', dict(), list(), tuple()]) + @pytest.mark.parametrize('func', [None, 0, 1, True, False, 'foo', {}, [], tuple()]) def test_transaction_invalid_function(self, func): ref = db.reference('/test') with pytest.raises(ValueError): @@ -672,7 +672,7 @@ def _assert_request(self, request, expected_method, expected_url): def test_get_value(self): ref = db.reference('/test') recorder = self.instrument(ref, json.dumps('data')) - query_str = 'auth_variable_override={0}'.format(self.encoded_override) + query_str = f'auth_variable_override={self.encoded_override}' assert ref.get() == 'data' assert len(recorder) == 1 self._assert_request( @@ -683,7 +683,7 @@ def test_set_value(self): recorder = self.instrument(ref, '') data = {'foo' : 'bar'} ref.set(data) - query_str = 'print=silent&auth_variable_override={0}'.format(self.encoded_override) + query_str = f'print=silent&auth_variable_override={self.encoded_override}' assert len(recorder) == 1 self._assert_request( recorder[0], 'PUT', 'https://test.firebaseio.com/test.json?' + query_str) @@ -693,7 +693,7 @@ def test_order_by_query(self): ref = db.reference('/test') recorder = self.instrument(ref, json.dumps('data')) query = ref.order_by_child('foo') - query_str = 'orderBy=%22foo%22&auth_variable_override={0}'.format(self.encoded_override) + query_str = f'orderBy=%22foo%22&auth_variable_override={self.encoded_override}' assert query.get() == 'data' assert len(recorder) == 1 self._assert_request( @@ -703,8 +703,9 @@ def test_range_query(self): ref = db.reference('/test') recorder = self.instrument(ref, json.dumps('data')) query = ref.order_by_child('foo').start_at(1).end_at(10) - query_str = ('endAt=10&orderBy=%22foo%22&startAt=1&' - 'auth_variable_override={0}'.format(self.encoded_override)) + query_str = ( + f'endAt=10&orderBy=%22foo%22&startAt=1&auth_variable_override={self.encoded_override}' + ) assert query.get() == 'data' assert len(recorder) == 1 self._assert_request( @@ -794,7 +795,7 @@ def test_valid_db_url(self, url): @pytest.mark.parametrize('url', [ None, '', 'foo', 'http://test.firebaseio.com', 'http://test.firebasedatabase.app', - True, False, 1, 0, dict(), list(), tuple(), _Object() + True, False, 1, 0, {}, [], tuple(), _Object() ]) def test_invalid_db_url(self, url): firebase_admin.initialize_app(testutils.MockCredential(), {'databaseURL' : url}) @@ -838,7 +839,7 @@ def test_valid_auth_override(self, override): assert ref._client.params['auth_variable_override'] == encoded @pytest.mark.parametrize('override', [ - '', 'foo', 0, 1, True, False, list(), tuple(), _Object()]) + '', 'foo', 0, 1, True, False, [], tuple(), _Object()]) def test_invalid_auth_override(self, override): firebase_admin.initialize_app(testutils.MockCredential(), { 'databaseURL' : 'https://test.firebaseio.com', @@ -885,8 +886,10 @@ def test_app_delete(self): assert other_ref._client.session is None def test_user_agent_format(self): - expected = 'Firebase/HTTP/{0}/{1}.{2}/AdminPython'.format( - firebase_admin.__version__, sys.version_info.major, sys.version_info.minor) + expected = ( + f'Firebase/HTTP/{firebase_admin.__version__}/{sys.version_info.major}.' + f'{sys.version_info.minor}/AdminPython' + ) assert db._USER_AGENT == expected def _check_timeout(self, ref, timeout): @@ -925,7 +928,7 @@ class TestQuery: ref = db.Reference(path='foo') @pytest.mark.parametrize('path', [ - '', None, '/', '/foo', 0, 1, True, False, dict(), list(), tuple(), _Object(), + '', None, '/', '/foo', 0, 1, True, False, {}, [], tuple(), _Object(), '$foo', '.foo', '#foo', '[foo', 'foo]', '$key', '$value', '$priority' ]) def test_invalid_path(self, path): @@ -935,13 +938,13 @@ def test_invalid_path(self, path): @pytest.mark.parametrize('path, expected', valid_paths.items()) def test_order_by_valid_path(self, path, expected): query = self.ref.order_by_child(path) - assert query._querystr == 'orderBy="{0}"'.format(expected) + assert query._querystr == f'orderBy="{expected}"' @pytest.mark.parametrize('path, expected', valid_paths.items()) def test_filter_by_valid_path(self, path, expected): query = self.ref.order_by_child(path) query.equal_to(10) - assert query._querystr == 'equalTo=10&orderBy="{0}"'.format(expected) + assert query._querystr == f'equalTo=10&orderBy="{expected}"' def test_order_by_key(self): query = self.ref.order_by_key() @@ -972,7 +975,7 @@ def test_multiple_limits(self): with pytest.raises(ValueError): query.limit_to_first(1) - @pytest.mark.parametrize('limit', [None, -1, 'foo', 1.2, list(), dict(), tuple(), _Object()]) + @pytest.mark.parametrize('limit', [None, -1, 'foo', 1.2, [], {}, tuple(), _Object()]) def test_invalid_limit(self, limit): query = self.ref.order_by_child('foo') with pytest.raises(ValueError): @@ -985,47 +988,47 @@ def test_start_at_none(self): with pytest.raises(ValueError): query.start_at(None) - @pytest.mark.parametrize('arg', ['', 'foo', True, False, 0, 1, dict()]) + @pytest.mark.parametrize('arg', ['', 'foo', True, False, 0, 1, {}]) def test_valid_start_at(self, arg): query = self.ref.order_by_child('foo').start_at(arg) - assert query._querystr == 'orderBy="foo"&startAt={0}'.format(json.dumps(arg)) + assert query._querystr == f'orderBy="foo"&startAt={json.dumps(arg)}' def test_end_at_none(self): query = self.ref.order_by_child('foo') with pytest.raises(ValueError): query.end_at(None) - @pytest.mark.parametrize('arg', ['', 'foo', True, False, 0, 1, dict()]) + @pytest.mark.parametrize('arg', ['', 'foo', True, False, 0, 1, {}]) def test_valid_end_at(self, arg): query = self.ref.order_by_child('foo').end_at(arg) - assert query._querystr == 'endAt={0}&orderBy="foo"'.format(json.dumps(arg)) + assert query._querystr == f'endAt={json.dumps(arg)}&orderBy="foo"' def test_equal_to_none(self): query = self.ref.order_by_child('foo') with pytest.raises(ValueError): query.equal_to(None) - @pytest.mark.parametrize('arg', ['', 'foo', True, False, 0, 1, dict()]) + @pytest.mark.parametrize('arg', ['', 'foo', True, False, 0, 1, {}]) def test_valid_equal_to(self, arg): query = self.ref.order_by_child('foo').equal_to(arg) - assert query._querystr == 'equalTo={0}&orderBy="foo"'.format(json.dumps(arg)) + assert query._querystr == f'equalTo={json.dumps(arg)}&orderBy="foo"' def test_range_query(self, initquery): query, order_by = initquery query.start_at(1) query.equal_to(2) query.end_at(3) - assert query._querystr == 'endAt=3&equalTo=2&orderBy="{0}"&startAt=1'.format(order_by) + assert query._querystr == f'endAt=3&equalTo=2&orderBy="{order_by}"&startAt=1' def test_limit_first_query(self, initquery): query, order_by = initquery query.limit_to_first(1) - assert query._querystr == 'limitToFirst=1&orderBy="{0}"'.format(order_by) + assert query._querystr == f'limitToFirst=1&orderBy="{order_by}"' def test_limit_last_query(self, initquery): query, order_by = initquery query.limit_to_last(1) - assert query._querystr == 'limitToLast=1&orderBy="{0}"'.format(order_by) + assert query._querystr == f'limitToLast=1&orderBy="{order_by}"' def test_all_in(self, initquery): query, order_by = initquery @@ -1033,7 +1036,7 @@ def test_all_in(self, initquery): query.equal_to(2) query.end_at(3) query.limit_to_first(10) - expected = 'endAt=3&equalTo=2&limitToFirst=10&orderBy="{0}"&startAt=1'.format(order_by) + expected = f'endAt=3&equalTo=2&limitToFirst=10&orderBy="{order_by}"&startAt=1' assert query._querystr == expected def test_invalid_query_args(self): @@ -1059,9 +1062,9 @@ class TestSorter: ({'k1' : False, 'k2' : 'bar', 'k3' : None}, ['k3', 'k1', 'k2']), ({'k1' : False, 'k2' : 1, 'k3' : None}, ['k3', 'k1', 'k2']), ({'k1' : True, 'k2' : 0, 'k3' : None, 'k4' : 'foo'}, ['k3', 'k1', 'k2', 'k4']), - ({'k1' : True, 'k2' : 0, 'k3' : None, 'k4' : 'foo', 'k5' : False, 'k6' : dict()}, + ({'k1' : True, 'k2' : 0, 'k3' : None, 'k4' : 'foo', 'k5' : False, 'k6' : {}}, ['k3', 'k5', 'k1', 'k2', 'k4', 'k6']), - ({'k1' : True, 'k2' : 0, 'k3' : 'foo', 'k4' : 'foo', 'k5' : False, 'k6' : dict()}, + ({'k1' : True, 'k2' : 0, 'k3' : 'foo', 'k4' : 'foo', 'k5' : False, 'k6' : {}}, ['k5', 'k1', 'k2', 'k3', 'k4', 'k6']), ] diff --git a/tests/test_instance_id.py b/tests/test_instance_id.py index 387e067c9..2b0e21079 100644 --- a/tests/test_instance_id.py +++ b/tests/test_instance_id.py @@ -72,7 +72,7 @@ def _assert_request(self, request, expected_method, expected_url): assert request.headers['x-goog-api-client'] == expected_metrics_header def _get_url(self, project_id, iid): - return instance_id._IID_SERVICE_URL + 'project/{0}/instanceId/{1}'.format(project_id, iid) + return instance_id._IID_SERVICE_URL + f'project/{project_id}/instanceId/{iid}' def test_no_project_id(self): def evaluate(): @@ -131,14 +131,14 @@ def test_delete_instance_id_unexpected_error(self): with pytest.raises(exceptions.UnknownError) as excinfo: instance_id.delete_instance_id('test_iid') url = self._get_url('explicit-project-id', 'test_iid') - message = 'Instance ID "test_iid": 501 Server Error: None for url: {0}'.format(url) + message = f'Instance ID "test_iid": 501 Server Error: None for url: {url}' assert str(excinfo.value) == message assert excinfo.value.cause is not None assert excinfo.value.http_response is not None assert len(recorder) == 1 self._assert_request(recorder[0], 'DELETE', url) - @pytest.mark.parametrize('iid', [None, '', 0, 1, True, False, list(), dict(), tuple()]) + @pytest.mark.parametrize('iid', [None, '', 0, 1, True, False, [], {}, tuple()]) def test_invalid_instance_id(self, iid): cred = testutils.MockCredential() app = firebase_admin.initialize_app(cred, {'projectId': 'explicit-project-id'}) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 63b649485..9fa30fef9 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -30,12 +30,12 @@ from tests import testutils -NON_STRING_ARGS = [list(), tuple(), dict(), True, False, 1, 0] -NON_DICT_ARGS = ['', list(), tuple(), True, False, 1, 0, {1: 'foo'}, {'foo': 1}] -NON_OBJECT_ARGS = [list(), tuple(), dict(), 'foo', 0, 1, True, False] -NON_LIST_ARGS = ['', tuple(), dict(), True, False, 1, 0, [1], ['foo', 1]] -NON_UINT_ARGS = ['1.23s', list(), tuple(), dict(), -1.23] -NON_BOOL_ARGS = ['', list(), tuple(), dict(), 1, 0, [1], ['foo', 1], {1: 'foo'}, {'foo': 1}] +NON_STRING_ARGS = [[], tuple(), {}, True, False, 1, 0] +NON_DICT_ARGS = ['', [], tuple(), True, False, 1, 0, {1: 'foo'}, {'foo': 1}] +NON_OBJECT_ARGS = [[], tuple(), {}, 'foo', 0, 1, True, False] +NON_LIST_ARGS = ['', tuple(), {}, True, False, 1, 0, [1], ['foo', 1]] +NON_UINT_ARGS = ['1.23s', [], tuple(), {}, -1.23] +NON_BOOL_ARGS = ['', [], tuple(), {}, 1, 0, [1], ['foo', 1], {1: 'foo'}, {'foo': 1}] HTTP_ERROR_CODES = { 400: exceptions.InvalidArgumentError, 403: exceptions.PermissionDeniedError, @@ -501,7 +501,7 @@ def test_invalid_channel_id(self, data): excinfo = self._check_notification(notification) assert str(excinfo.value) == 'AndroidNotification.channel_id must be a string.' - @pytest.mark.parametrize('timestamp', [100, '', 'foo', {}, [], list(), dict()]) + @pytest.mark.parametrize('timestamp', [100, '', 'foo', {}, []]) def test_invalid_event_timestamp(self, timestamp): notification = messaging.AndroidNotification(event_timestamp=timestamp) excinfo = self._check_notification(notification) @@ -568,7 +568,7 @@ def test_negative_vibrate_timings_millis(self): expected = 'AndroidNotification.vibrate_timings_millis must not be negative.' assert str(excinfo.value) == expected - @pytest.mark.parametrize('notification_count', ['', 'foo', list(), tuple(), dict()]) + @pytest.mark.parametrize('notification_count', ['', 'foo', [], tuple(), {}]) def test_invalid_notification_count(self, notification_count): notification = messaging.AndroidNotification(notification_count=notification_count) excinfo = self._check_notification(notification) @@ -939,19 +939,19 @@ def test_invalid_tag(self, data): excinfo = self._check_notification(notification) assert str(excinfo.value) == 'WebpushNotification.tag must be a string.' - @pytest.mark.parametrize('data', ['', 'foo', list(), tuple(), dict()]) + @pytest.mark.parametrize('data', ['', 'foo', [], tuple(), {}]) def test_invalid_timestamp(self, data): notification = messaging.WebpushNotification(timestamp_millis=data) excinfo = self._check_notification(notification) assert str(excinfo.value) == 'WebpushNotification.timestamp_millis must be a number.' - @pytest.mark.parametrize('data', ['', list(), tuple(), True, False, 1, 0]) + @pytest.mark.parametrize('data', ['', [], tuple(), True, False, 1, 0]) def test_invalid_custom_data(self, data): notification = messaging.WebpushNotification(custom_data=data) excinfo = self._check_notification(notification) assert str(excinfo.value) == 'WebpushNotification.custom_data must be a dict.' - @pytest.mark.parametrize('data', ['', dict(), tuple(), True, False, 1, 0, [1, 2]]) + @pytest.mark.parametrize('data', ['', {}, tuple(), True, False, 1, 0, [1, 2]]) def test_invalid_actions(self, data): notification = messaging.WebpushNotification(actions=data) excinfo = self._check_notification(notification) @@ -1172,7 +1172,7 @@ def test_invalid_alert(self, data): expected = 'Aps.alert must be a string or an instance of ApsAlert class.' assert str(excinfo.value) == expected - @pytest.mark.parametrize('data', [list(), tuple(), dict(), 'foo']) + @pytest.mark.parametrize('data', [[], tuple(), {}, 'foo']) def test_invalid_badge(self, data): aps = messaging.Aps(badge=data) with pytest.raises(ValueError) as excinfo: @@ -1204,7 +1204,7 @@ def test_invalid_thread_id(self, data): expected = 'Aps.thread_id must be a string.' assert str(excinfo.value) == expected - @pytest.mark.parametrize('data', ['', list(), tuple(), True, False, 1, 0, ]) + @pytest.mark.parametrize('data', ['', [], tuple(), True, False, 1, 0, ]) def test_invalid_custom_data_dict(self, data): if isinstance(data, dict): return @@ -1309,7 +1309,7 @@ def test_invalid_name(self, data): expected = 'CriticalSound.name must be a non-empty string.' assert str(excinfo.value) == expected - @pytest.mark.parametrize('data', [list(), tuple(), dict(), 'foo']) + @pytest.mark.parametrize('data', [[], tuple(), {}, 'foo']) def test_invalid_volume(self, data): sound = messaging.CriticalSound(name='default', volume=data) excinfo = self._check_sound(sound) @@ -1659,7 +1659,7 @@ def test_topic_management_custom_timeout(self, options, timeout): class TestSend: _DEFAULT_RESPONSE = json.dumps({'name': 'message-id'}) - _CLIENT_VERSION = 'fire-admin-python/{0}'.format(firebase_admin.__version__) + _CLIENT_VERSION = f'fire-admin-python/{firebase_admin.__version__}' @classmethod def setup_class(cls): @@ -1736,7 +1736,7 @@ def test_send_error(self, status, exc_type): msg = messaging.Message(topic='foo') with pytest.raises(exc_type) as excinfo: messaging.send(msg) - expected = 'Unexpected HTTP response with status: {0}; body: {{}}'.format(status) + expected = f'Unexpected HTTP response with status: {status}; body: {{}}' check_exception(excinfo.value, expected, status) assert len(recorder) == 1 body = {'message': messaging._MessagingService.JSON_ENCODER.default(msg)} @@ -2332,9 +2332,9 @@ def _assert_request(self, request, expected_method, expected_url): assert request.headers['x-goog-api-client'] == expected_metrics_header def _get_url(self, path): - return '{0}/{1}'.format(messaging._MessagingService.IID_URL, path) + return f'{messaging._MessagingService.IID_URL}/{path}' - @pytest.mark.parametrize('tokens', [None, '', list(), dict(), tuple()]) + @pytest.mark.parametrize('tokens', [None, '', [], {}, tuple()]) def test_invalid_tokens(self, tokens): expected = 'Tokens must be a string or a non-empty list of strings.' if isinstance(tokens, str): @@ -2383,7 +2383,7 @@ def test_subscribe_to_topic_non_json_error(self, status, exc_type): _, recorder = self._instrument_iid_service(status=status, payload='not json') with pytest.raises(exc_type) as excinfo: messaging.subscribe_to_topic('foo', 'test-topic') - reason = 'Unexpected HTTP response with status: {0}; body: not json'.format(status) + reason = f'Unexpected HTTP response with status: {status}; body: not json' assert str(excinfo.value) == reason assert len(recorder) == 1 self._assert_request(recorder[0], 'POST', self._get_url('iid/v1:batchAdd')) @@ -2412,7 +2412,7 @@ def test_unsubscribe_from_topic_non_json_error(self, status, exc_type): _, recorder = self._instrument_iid_service(status=status, payload='not json') with pytest.raises(exc_type) as excinfo: messaging.unsubscribe_from_topic('foo', 'test-topic') - reason = 'Unexpected HTTP response with status: {0}; body: not json'.format(status) + reason = f'Unexpected HTTP response with status: {status}; body: not json' assert str(excinfo.value) == reason assert len(recorder) == 1 self._assert_request(recorder[0], 'POST', self._get_url('iid/v1:batchRemove')) diff --git a/tests/test_ml.py b/tests/test_ml.py index 2af9ae42f..bcc93fd05 100644 --- a/tests/test_ml.py +++ b/tests/test_ml.py @@ -49,7 +49,7 @@ TAGS_2 = [TAG_1, TAG_3] MODEL_ID_1 = 'modelId1' -MODEL_NAME_1 = 'projects/{0}/models/{1}'.format(PROJECT_ID, MODEL_ID_1) +MODEL_NAME_1 = f'projects/{PROJECT_ID}/models/{MODEL_ID_1}' DISPLAY_NAME_1 = 'displayName1' MODEL_JSON_1 = { 'name': MODEL_NAME_1, @@ -58,7 +58,7 @@ MODEL_1 = ml.Model.from_dict(MODEL_JSON_1) MODEL_ID_2 = 'modelId2' -MODEL_NAME_2 = 'projects/{0}/models/{1}'.format(PROJECT_ID, MODEL_ID_2) +MODEL_NAME_2 = f'projects/{PROJECT_ID}/models/{MODEL_ID_2}' DISPLAY_NAME_2 = 'displayName2' MODEL_JSON_2 = { 'name': MODEL_NAME_2, @@ -67,7 +67,7 @@ MODEL_2 = ml.Model.from_dict(MODEL_JSON_2) MODEL_ID_3 = 'modelId3' -MODEL_NAME_3 = 'projects/{0}/models/{1}'.format(PROJECT_ID, MODEL_ID_3) +MODEL_NAME_3 = f'projects/{PROJECT_ID}/models/{MODEL_ID_3}' DISPLAY_NAME_3 = 'displayName3' MODEL_JSON_3 = { 'name': MODEL_NAME_3, @@ -79,7 +79,7 @@ 'published': True } VALIDATION_ERROR_CODE = 400 -VALIDATION_ERROR_MSG = 'No model format found for {0}.'.format(MODEL_ID_1) +VALIDATION_ERROR_MSG = f'No model format found for {MODEL_ID_1}.' MODEL_STATE_ERROR_JSON = { 'validationError': { 'code': VALIDATION_ERROR_CODE, @@ -87,19 +87,19 @@ } } -OPERATION_NAME_1 = 'projects/{0}/operations/123'.format(PROJECT_ID) +OPERATION_NAME_1 = f'projects/{PROJECT_ID}/operations/123' OPERATION_NOT_DONE_JSON_1 = { 'name': OPERATION_NAME_1, 'metadata': { '@type': 'type.googleapis.com/google.firebase.ml.v1beta2.ModelOperationMetadata', - 'name': 'projects/{0}/models/{1}'.format(PROJECT_ID, MODEL_ID_1), + 'name': f'projects/{PROJECT_ID}/models/{MODEL_ID_1}', 'basic_operation_status': 'BASIC_OPERATION_STATUS_UPLOADING' } } GCS_BUCKET_NAME = 'my_bucket' GCS_BLOB_NAME = 'mymodel.tflite' -GCS_TFLITE_URI = 'gs://{0}/{1}'.format(GCS_BUCKET_NAME, GCS_BLOB_NAME) +GCS_TFLITE_URI = f'gs://{GCS_BUCKET_NAME}/{GCS_BLOB_NAME}' GCS_TFLITE_URI_JSON = {'gcsTfliteUri': GCS_TFLITE_URI} GCS_TFLITE_MODEL_SOURCE = ml.TFLiteGCSModelSource(GCS_TFLITE_URI) TFLITE_FORMAT_JSON = { @@ -257,8 +257,8 @@ INVALID_MODEL_ARGS = [ 'abc', 4.2, - list(), - dict(), + [], + {}, True, -1, 0, @@ -272,9 +272,10 @@ 'projects/$#@/operations/123', 'projects/1234/operations/123/extrathing', ] -PAGE_SIZE_VALUE_ERROR_MSG = 'Page size must be a positive integer between ' \ - '1 and {0}'.format(ml._MAX_PAGE_SIZE) -INVALID_STRING_OR_NONE_ARGS = [0, -1, 4.2, 0x10, False, list(), dict()] +PAGE_SIZE_VALUE_ERROR_MSG = ( + f'Page size must be a positive integer between 1 and {ml._MAX_PAGE_SIZE}' +) +INVALID_STRING_OR_NONE_ARGS = [0, -1, 4.2, 0x10, False, [], {}] # For validation type errors @@ -358,8 +359,7 @@ def teardown_class(cls): @staticmethod def _op_url(project_id): - return BASE_URL + \ - 'projects/{0}/operations/123'.format(project_id) + return BASE_URL + f'projects/{project_id}/operations/123' def test_model_success_err_state_lro(self): model = ml.Model.from_dict(FULL_MODEL_ERR_STATE_LRO_JSON) @@ -569,16 +569,15 @@ def teardown_class(cls): @staticmethod def _url(project_id): - return BASE_URL + 'projects/{0}/models'.format(project_id) + return BASE_URL + f'projects/{project_id}/models' @staticmethod def _op_url(project_id): - return BASE_URL + \ - 'projects/{0}/operations/123'.format(project_id) + return BASE_URL + f'projects/{project_id}/operations/123' @staticmethod def _get_url(project_id, model_id): - return BASE_URL + 'projects/{0}/models/{1}'.format(project_id, model_id) + return BASE_URL + f'projects/{project_id}/models/{model_id}' def test_immediate_done(self): instrument_ml_service(status=200, payload=OPERATION_DONE_RESPONSE) @@ -663,12 +662,11 @@ def teardown_class(cls): @staticmethod def _url(project_id, model_id): - return BASE_URL + 'projects/{0}/models/{1}'.format(project_id, model_id) + return BASE_URL + f'projects/{project_id}/models/{model_id}' @staticmethod def _op_url(project_id): - return BASE_URL + \ - 'projects/{0}/operations/123'.format(project_id) + return BASE_URL + f'projects/{project_id}/operations/123' def test_immediate_done(self): instrument_ml_service(status=200, payload=OPERATION_DONE_RESPONSE) @@ -760,18 +758,16 @@ def teardown_class(cls): @staticmethod def _update_url(project_id, model_id): - update_url = 'projects/{0}/models/{1}?updateMask=state.published'.format( - project_id, model_id) + update_url = f'projects/{project_id}/models/{model_id}?updateMask=state.published' return BASE_URL + update_url @staticmethod def _get_url(project_id, model_id): - return BASE_URL + 'projects/{0}/models/{1}'.format(project_id, model_id) + return BASE_URL + f'projects/{project_id}/models/{model_id}' @staticmethod def _op_url(project_id): - return BASE_URL + \ - 'projects/{0}/operations/123'.format(project_id) + return BASE_URL + f'projects/{project_id}/operations/123' @pytest.mark.parametrize('publish_function, published', PUBLISH_UNPUBLISH_WITH_ARGS) def test_immediate_done(self, publish_function, published): @@ -842,7 +838,7 @@ def teardown_class(cls): @staticmethod def _url(project_id, model_id): - return BASE_URL + 'projects/{0}/models/{1}'.format(project_id, model_id) + return BASE_URL + f'projects/{project_id}/models/{model_id}' def test_get_model(self): recorder = instrument_ml_service(status=200, payload=DEFAULT_GET_RESPONSE) @@ -893,7 +889,7 @@ def teardown_class(cls): @staticmethod def _url(project_id, model_id): - return BASE_URL + 'projects/{0}/models/{1}'.format(project_id, model_id) + return BASE_URL + f'projects/{project_id}/models/{model_id}' def test_delete_model(self): recorder = instrument_ml_service(status=200, payload=EMPTY_RESPONSE) @@ -941,7 +937,7 @@ def teardown_class(cls): @staticmethod def _url(project_id): - return BASE_URL + 'projects/{0}/models'.format(project_id) + return BASE_URL + f'projects/{project_id}/models' @staticmethod def _check_page(page, model_count): @@ -970,8 +966,8 @@ def test_list_models_with_all_args(self): assert len(recorder) == 1 _assert_request(recorder[0], 'GET', ( TestListModels._url(PROJECT_ID) + - '?filter=display_name%3DdisplayName3&page_size=10&page_token={0}' - .format(PAGE_TOKEN))) + f'?filter=display_name%3DdisplayName3&page_size=10&page_token={PAGE_TOKEN}' + )) assert isinstance(models_page, ml.ListModelsPage) assert len(models_page.models) == 1 assert models_page.models[0] == MODEL_3 @@ -986,8 +982,8 @@ def test_list_models_list_filter_validation(self, list_filter): @pytest.mark.parametrize('page_size, exc_type, error_message', [ ('abc', TypeError, 'Page size must be a number or None.'), (4.2, TypeError, 'Page size must be a number or None.'), - (list(), TypeError, 'Page size must be a number or None.'), - (dict(), TypeError, 'Page size must be a number or None.'), + ([], TypeError, 'Page size must be a number or None.'), + ({}, TypeError, 'Page size must be a number or None.'), (True, TypeError, 'Page size must be a number or None.'), (-1, ValueError, PAGE_SIZE_VALUE_ERROR_MSG), (0, ValueError, PAGE_SIZE_VALUE_ERROR_MSG), @@ -1061,7 +1057,7 @@ def test_list_models_paged_iteration(self): iterator = page.iterate_all() for index in range(2): model = next(iterator) - assert model.display_name == 'displayName{0}'.format(index+1) + assert model.display_name == f'displayName{index+1}' assert len(recorder) == 1 # Page 2 diff --git a/tests/test_project_management.py b/tests/test_project_management.py index a242f523f..89e48c2e5 100644 --- a/tests/test_project_management.py +++ b/tests/test_project_management.py @@ -545,7 +545,7 @@ def test_custom_timeout(self, timeout): 'projectId': 'test-project-id' } app = firebase_admin.initialize_app( - testutils.MockCredential(), options, 'timeout-{0}'.format(timeout)) + testutils.MockCredential(), options, f'timeout-{timeout}') project_management_service = project_management._get_project_management_service(app) assert project_management_service._client.timeout == timeout @@ -820,7 +820,7 @@ def test_list_android_apps_rpc_error(self): assert len(recorder) == 1 def test_list_android_apps_empty_list(self): - recorder = self._instrument_service(statuses=[200], responses=[json.dumps(dict())]) + recorder = self._instrument_service(statuses=[200], responses=[json.dumps({})]) android_apps = project_management.list_android_apps() @@ -883,7 +883,7 @@ def test_list_ios_apps_rpc_error(self): assert len(recorder) == 1 def test_list_ios_apps_empty_list(self): - recorder = self._instrument_service(statuses=[200], responses=[json.dumps(dict())]) + recorder = self._instrument_service(statuses=[200], responses=[json.dumps({})]) ios_apps = project_management.list_ios_apps() diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py index 14b54838f..7bbf9721d 100644 --- a/tests/test_remote_config.py +++ b/tests/test_remote_config.py @@ -865,7 +865,7 @@ async def test_rc_instance_get_server_template(self): template = await rc_instance.get_server_template() - assert template.parameters == dict(test_key="test_value") + assert template.parameters == {"test_key": 'test_value'} assert str(template.version) == 'test' assert str(template.etag) == 'etag' diff --git a/tests/test_storage.py b/tests/test_storage.py index e15c4e2ab..c874ef640 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -33,7 +33,7 @@ def test_invalid_config(): with pytest.raises(ValueError): storage.bucket() -@pytest.mark.parametrize('name', [None, '', 0, 1, True, False, list(), tuple(), dict()]) +@pytest.mark.parametrize('name', [None, '', 0, 1, True, False, [], tuple(), {}]) def test_invalid_name(name): with pytest.raises(ValueError): storage.bucket(name) diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 156846343..900faa376 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -107,8 +107,8 @@ LIST_OIDC_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_oidc_provider_configs.json') LIST_SAML_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_saml_provider_configs.json') -INVALID_TENANT_IDS = [None, '', 0, 1, True, False, list(), tuple(), dict()] -INVALID_BOOLEANS = ['', 1, 0, list(), tuple(), dict()] +INVALID_TENANT_IDS = [None, '', 0, 1, True, False, [], tuple(), {}] +INVALID_BOOLEANS = ['', 1, 0, [], tuple(), {}] USER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v1/projects/mock-project-id' PROVIDER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2/projects/mock-project-id' @@ -152,7 +152,7 @@ def _instrument_provider_mgt(client, status, payload): class TestTenant: - @pytest.mark.parametrize('data', [None, 'foo', 0, 1, True, False, list(), tuple(), dict()]) + @pytest.mark.parametrize('data', [None, 'foo', 0, 1, True, False, [], tuple(), {}]) def test_invalid_data(self, data): with pytest.raises(ValueError): tenant_mgt.Tenant(data) @@ -197,7 +197,7 @@ def test_get_tenant(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'GET' - assert req.url == '{0}/tenants/tenant-id'.format(TENANT_MGT_URL_PREFIX) + assert req.url == f'{TENANT_MGT_URL_PREFIX}/tenants/tenant-id' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' assert req.headers['x-goog-api-client'] == expected_metrics_header @@ -216,7 +216,7 @@ def test_tenant_not_found(self, tenant_mgt_app): class TestCreateTenant: - @pytest.mark.parametrize('display_name', [True, False, 1, 0, list(), tuple(), dict()]) + @pytest.mark.parametrize('display_name', [True, False, 1, 0, [], tuple(), {}]) def test_invalid_display_name_type(self, display_name, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: tenant_mgt.create_tenant(display_name=display_name, app=tenant_mgt_app) @@ -290,7 +290,7 @@ def _assert_request(self, recorder, body): assert len(recorder) == 1 req = recorder[0] assert req.method == 'POST' - assert req.url == '{0}/tenants'.format(TENANT_MGT_URL_PREFIX) + assert req.url == f'{TENANT_MGT_URL_PREFIX}/tenants' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' assert req.headers['x-goog-api-client'] == expected_metrics_header @@ -306,7 +306,7 @@ def test_invalid_tenant_id(self, tenant_id, tenant_mgt_app): tenant_mgt.update_tenant(tenant_id, display_name='My Tenant', app=tenant_mgt_app) assert str(excinfo.value).startswith('Tenant ID must be a non-empty string') - @pytest.mark.parametrize('display_name', [True, False, 1, 0, list(), tuple(), dict()]) + @pytest.mark.parametrize('display_name', [True, False, 1, 0, [], tuple(), {}]) def test_invalid_display_name_type(self, display_name, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: tenant_mgt.update_tenant('tenant-id', display_name=display_name, app=tenant_mgt_app) @@ -390,8 +390,7 @@ def _assert_request(self, recorder, body, mask): assert len(recorder) == 1 req = recorder[0] assert req.method == 'PATCH' - assert req.url == '{0}/tenants/tenant-id?updateMask={1}'.format( - TENANT_MGT_URL_PREFIX, ','.join(mask)) + assert req.url == f'{TENANT_MGT_URL_PREFIX}/tenants/tenant-id?updateMask={",".join(mask)}' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' assert req.headers['x-goog-api-client'] == expected_metrics_header @@ -414,7 +413,7 @@ def test_delete_tenant(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'DELETE' - assert req.url == '{0}/tenants/tenant-id'.format(TENANT_MGT_URL_PREFIX) + assert req.url == f'{TENANT_MGT_URL_PREFIX}/tenants/tenant-id' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' assert req.headers['x-goog-api-client'] == expected_metrics_header @@ -433,12 +432,12 @@ def test_tenant_not_found(self, tenant_mgt_app): class TestListTenants: - @pytest.mark.parametrize('arg', [None, 'foo', list(), dict(), 0, -1, 101, False]) + @pytest.mark.parametrize('arg', [None, 'foo', [], {}, 0, -1, 101, False]) def test_invalid_max_results(self, tenant_mgt_app, arg): with pytest.raises(ValueError): tenant_mgt.list_tenants(max_results=arg, app=tenant_mgt_app) - @pytest.mark.parametrize('arg', ['', list(), dict(), 0, -1, True, False]) + @pytest.mark.parametrize('arg', ['', [], {}, 0, -1, True, False]) def test_invalid_page_token(self, tenant_mgt_app, arg): with pytest.raises(ValueError): tenant_mgt.list_tenants(page_token=arg, app=tenant_mgt_app) @@ -480,7 +479,7 @@ def test_list_tenants_paged_iteration(self, tenant_mgt_app): iterator = page.iterate_all() for index in range(3): tenant = next(iterator) - assert tenant.tenant_id == 'tenant{0}'.format(index) + assert tenant.tenant_id == f'tenant{index}' self._assert_request(recorder) # Page 2 (also the last page) @@ -551,7 +550,7 @@ def _assert_tenants_page(self, page): assert isinstance(page, tenant_mgt.ListTenantsPage) assert len(page.tenants) == 2 for idx, tenant in enumerate(page.tenants): - _assert_tenant(tenant, 'tenant{0}'.format(idx)) + _assert_tenant(tenant, f'tenant{idx}') def _assert_request(self, recorder, expected=None): if expected is None: @@ -671,8 +670,7 @@ def test_revoke_refresh_tokens(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'POST' - assert req.url == '{0}/tenants/tenant-id/accounts:update'.format( - USER_MGT_URL_PREFIX) + assert req.url == f'{USER_MGT_URL_PREFIX}/tenants/tenant-id/accounts:update' body = json.loads(req.body.decode()) assert body['localId'] == 'testuser' assert 'validSince' in body @@ -693,8 +691,9 @@ def test_list_users(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'GET' - assert req.url == '{0}/tenants/tenant-id/accounts:batchGet?maxResults=1000'.format( - USER_MGT_URL_PREFIX) + assert req.url == ( + f'{USER_MGT_URL_PREFIX}/tenants/tenant-id/accounts:batchGet?maxResults=1000' + ) def test_import_users(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) @@ -765,8 +764,9 @@ def test_get_oidc_provider_config(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'GET' - assert req.url == '{0}/tenants/tenant-id/oauthIdpConfigs/oidc.provider'.format( - PROVIDER_MGT_URL_PREFIX) + assert req.url == ( + f'{PROVIDER_MGT_URL_PREFIX}/tenants/tenant-id/oauthIdpConfigs/oidc.provider' + ) def test_create_oidc_provider_config(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) @@ -791,7 +791,7 @@ def test_update_oidc_provider_config(self, tenant_mgt_app): self._assert_oidc_provider_config(provider_config) mask = ['clientId', 'displayName', 'enabled', 'issuer'] - url = '/oauthIdpConfigs/oidc.provider?updateMask={0}'.format(','.join(mask)) + url = f'/oauthIdpConfigs/oidc.provider?updateMask={",".join(mask)}' self._assert_request( recorder, url, OIDC_PROVIDER_CONFIG_REQUEST, method='PATCH', prefix=PROVIDER_MGT_URL_PREFIX) @@ -805,8 +805,9 @@ def test_delete_oidc_provider_config(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'DELETE' - assert req.url == '{0}/tenants/tenant-id/oauthIdpConfigs/oidc.provider'.format( - PROVIDER_MGT_URL_PREFIX) + assert req.url == ( + f'{PROVIDER_MGT_URL_PREFIX}/tenants/tenant-id/oauthIdpConfigs/oidc.provider' + ) def test_list_oidc_provider_configs(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) @@ -819,7 +820,7 @@ def test_list_oidc_provider_configs(self, tenant_mgt_app): assert len(page.provider_configs) == 2 for provider_config in page.provider_configs: self._assert_oidc_provider_config( - provider_config, want_id='oidc.provider{0}'.format(index)) + provider_config, want_id=f'oidc.provider{index}') index += 1 assert page.next_page_token == '' @@ -831,8 +832,9 @@ def test_list_oidc_provider_configs(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'GET' - assert req.url == '{0}{1}'.format( - PROVIDER_MGT_URL_PREFIX, '/tenants/tenant-id/oauthIdpConfigs?pageSize=100') + assert req.url == ( + f'{PROVIDER_MGT_URL_PREFIX}/tenants/tenant-id/oauthIdpConfigs?pageSize=100' + ) def test_get_saml_provider_config(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) @@ -844,8 +846,9 @@ def test_get_saml_provider_config(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'GET' - assert req.url == '{0}/tenants/tenant-id/inboundSamlConfigs/saml.provider'.format( - PROVIDER_MGT_URL_PREFIX) + assert req.url == ( + f'{PROVIDER_MGT_URL_PREFIX}/tenants/tenant-id/inboundSamlConfigs/saml.provider' + ) def test_create_saml_provider_config(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) @@ -877,7 +880,7 @@ def test_update_saml_provider_config(self, tenant_mgt_app): 'displayName', 'enabled', 'idpConfig.idpCertificates', 'idpConfig.idpEntityId', 'idpConfig.ssoUrl', 'spConfig.callbackUri', 'spConfig.spEntityId', ] - url = '/inboundSamlConfigs/saml.provider?updateMask={0}'.format(','.join(mask)) + url = f'/inboundSamlConfigs/saml.provider?updateMask={",".join(mask)}' self._assert_request( recorder, url, SAML_PROVIDER_CONFIG_REQUEST, method='PATCH', prefix=PROVIDER_MGT_URL_PREFIX) @@ -891,8 +894,9 @@ def test_delete_saml_provider_config(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'DELETE' - assert req.url == '{0}/tenants/tenant-id/inboundSamlConfigs/saml.provider'.format( - PROVIDER_MGT_URL_PREFIX) + assert req.url == ( + f'{PROVIDER_MGT_URL_PREFIX}/tenants/tenant-id/inboundSamlConfigs/saml.provider' + ) def test_list_saml_provider_configs(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) @@ -905,7 +909,7 @@ def test_list_saml_provider_configs(self, tenant_mgt_app): assert len(page.provider_configs) == 2 for provider_config in page.provider_configs: self._assert_saml_provider_config( - provider_config, want_id='saml.provider{0}'.format(index)) + provider_config, want_id=f'saml.provider{index}') index += 1 assert page.next_page_token == '' @@ -917,8 +921,9 @@ def test_list_saml_provider_configs(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'GET' - assert req.url == '{0}{1}'.format( - PROVIDER_MGT_URL_PREFIX, '/tenants/tenant-id/inboundSamlConfigs?pageSize=100') + assert req.url == ( + f'{PROVIDER_MGT_URL_PREFIX}/tenants/tenant-id/inboundSamlConfigs?pageSize=100' + ) def test_tenant_not_found(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) @@ -937,7 +942,7 @@ def _assert_request( assert len(recorder) == 1 req = recorder[0] assert req.method == method - assert req.url == '{0}/tenants/tenant-id{1}'.format(prefix, want_url) + assert req.url == f'{prefix}/tenants/tenant-id{want_url}' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' assert req.headers['x-goog-api-client'] == expected_metrics_header diff --git a/tests/test_token_gen.py b/tests/test_token_gen.py index fe0b28dbe..384bc22c3 100644 --- a/tests/test_token_gen.py +++ b/tests/test_token_gen.py @@ -48,8 +48,8 @@ MOCK_SERVICE_ACCOUNT_EMAIL = MOCK_CREDENTIAL.service_account_email MOCK_REQUEST = testutils.MockRequest(200, MOCK_PUBLIC_CERTS) -INVALID_STRINGS = [None, '', 0, 1, True, False, list(), tuple(), dict()] -INVALID_BOOLS = [None, '', 'foo', 0, 1, list(), tuple(), dict()] +INVALID_STRINGS = [None, '', 0, 1, True, False, [], tuple(), {}] +INVALID_BOOLS = [None, '', 'foo', 0, 1, [], tuple(), {}] INVALID_JWT_ARGS = { 'NoneToken': None, 'EmptyToken': '', @@ -63,7 +63,7 @@ ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1' EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST' AUTH_EMULATOR_HOST = 'localhost:9099' -EMULATED_ID_TOOLKIT_URL = 'http://{}/identitytoolkit.googleapis.com/v1'.format(AUTH_EMULATOR_HOST) +EMULATED_ID_TOOLKIT_URL = f'http://{AUTH_EMULATOR_HOST}/identitytoolkit.googleapis.com/v1' TOKEN_MGT_URLS = { 'ID_TOOLKIT': ID_TOOLKIT_URL, } @@ -136,8 +136,9 @@ def _get_session_cookie( payload_overrides=None, header_overrides=None, current_time=MOCK_CURRENT_TIME): payload_overrides = payload_overrides or {} if 'iss' not in payload_overrides: - payload_overrides['iss'] = 'https://session.firebase.google.com/{0}'.format( - MOCK_CREDENTIAL.project_id) + payload_overrides['iss'] = ( + f'https://session.firebase.google.com/{MOCK_CREDENTIAL.project_id}' + ) return _get_id_token(payload_overrides, header_overrides, current_time=current_time) def _instrument_user_manager(app, status, payload): @@ -282,7 +283,7 @@ def test_sign_with_iam(self): testutils.MockCredential(), name='iam-signer-app', options=options) try: signature = base64.b64encode(b'test').decode() - iam_resp = '{{"signedBlob": "{0}"}}'.format(signature) + iam_resp = json.dumps({'signedBlob': signature}) _overwrite_iam_request(app, testutils.MockRequest(200, iam_resp)) custom_token = auth.create_custom_token(MOCK_UID, app=app).decode() assert custom_token.endswith('.' + signature.rstrip('=')) @@ -319,8 +320,7 @@ def test_sign_with_discovered_service_account(self): # Now invoke the IAM signer. signature = base64.b64encode(b'test').decode() - request.response = testutils.MockResponse( - 200, '{{"signedBlob": "{0}"}}'.format(signature)) + request.response = testutils.MockResponse(200, json.dumps({'signedBlob': signature})) custom_token = auth.create_custom_token(MOCK_UID, app=app).decode() assert custom_token.endswith('.' + signature.rstrip('=')) self._verify_signer(custom_token, 'discovered-service-account') @@ -354,13 +354,13 @@ def _verify_signer(self, token, signer): class TestCreateSessionCookie: - @pytest.mark.parametrize('id_token', [None, '', 0, 1, True, False, list(), dict(), tuple()]) + @pytest.mark.parametrize('id_token', [None, '', 0, 1, True, False, [], {}, tuple()]) def test_invalid_id_token(self, user_mgt_app, id_token): with pytest.raises(ValueError): auth.create_session_cookie(id_token, expires_in=3600, app=user_mgt_app) @pytest.mark.parametrize('expires_in', [ - None, '', True, False, list(), dict(), tuple(), + None, '', True, False, [], {}, tuple(), _token_gen.MIN_SESSION_COOKIE_DURATION_SECONDS - 1, _token_gen.MAX_SESSION_COOKIE_DURATION_SECONDS + 1, ]) diff --git a/tests/test_user_mgt.py b/tests/test_user_mgt.py index 34b698be4..2c747ee5e 100644 --- a/tests/test_user_mgt.py +++ b/tests/test_user_mgt.py @@ -32,10 +32,10 @@ from tests import testutils -INVALID_STRINGS = [None, '', 0, 1, True, False, list(), tuple(), dict()] -INVALID_DICTS = [None, 'foo', 0, 1, True, False, list(), tuple()] -INVALID_INTS = [None, 'foo', '1', -1, 1.1, True, False, list(), tuple(), dict()] -INVALID_TIMESTAMPS = ['foo', '1', 0, -1, 1.1, True, False, list(), tuple(), dict()] +INVALID_STRINGS = [None, '', 0, 1, True, False, [], tuple(), {}] +INVALID_DICTS = [None, 'foo', 0, 1, True, False, [], tuple()] +INVALID_INTS = [None, 'foo', '1', -1, 1.1, True, False, [], tuple(), {}] +INVALID_TIMESTAMPS = ['foo', '1', 0, -1, 1.1, True, False, [], tuple(), {}] MOCK_GET_USER_RESPONSE = testutils.resource('get_user.json') MOCK_LIST_USERS_RESPONSE = testutils.resource('list_users.json') @@ -56,7 +56,7 @@ ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1' EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST' AUTH_EMULATOR_HOST = 'localhost:9099' -EMULATED_ID_TOOLKIT_URL = 'http://{}/identitytoolkit.googleapis.com/v1'.format(AUTH_EMULATOR_HOST) +EMULATED_ID_TOOLKIT_URL = f'http://{AUTH_EMULATOR_HOST}/identitytoolkit.googleapis.com/v1' URL_PROJECT_SUFFIX = '/projects/mock-project-id' USER_MGT_URLS = { 'ID_TOOLKIT': ID_TOOLKIT_URL, @@ -135,7 +135,7 @@ def _check_request(recorder, want_url, want_body=None, want_timeout=None): assert len(recorder) == 1 req = recorder[0] assert req.method == 'POST' - assert req.url == '{0}{1}'.format(USER_MGT_URLS['PREFIX'], want_url) + assert req.url == f'{USER_MGT_URLS["PREFIX"]}{want_url}' expected_metrics_header = [ _utils.get_metrics_header(), _utils.get_metrics_header() + ' mock-cred-metric-tag' @@ -538,7 +538,7 @@ def test_user_already_exists(self, user_mgt_app, error_code): with pytest.raises(exc_type) as excinfo: auth.create_user(app=user_mgt_app) assert isinstance(excinfo.value, exceptions.AlreadyExistsError) - assert str(excinfo.value) == '{0} ({1}).'.format(exc_type.default_message, error_code) + assert str(excinfo.value) == f'{exc_type.default_message} ({error_code}).' assert excinfo.value.http_response is not None assert excinfo.value.cause is not None @@ -704,15 +704,14 @@ def test_single_reserved_claim(self, user_mgt_app, key): claims = {key : 'value'} with pytest.raises(ValueError) as excinfo: auth.set_custom_user_claims('user', claims, app=user_mgt_app) - assert str(excinfo.value) == 'Claim "{0}" is reserved, and must not be set.'.format(key) + assert str(excinfo.value) == f'Claim "{key}" is reserved, and must not be set.' def test_multiple_reserved_claims(self, user_mgt_app): claims = {key : 'value' for key in _auth_utils.RESERVED_CLAIMS} with pytest.raises(ValueError) as excinfo: auth.set_custom_user_claims('user', claims, app=user_mgt_app) joined = ', '.join(sorted(claims.keys())) - assert str(excinfo.value) == ('Claims "{0}" are reserved, and must not be ' - 'set.'.format(joined)) + assert str(excinfo.value) == f'Claims "{joined}" are reserved, and must not be set.' def test_large_claims_payload(self, user_mgt_app): claims = {'key' : 'A'*1000} @@ -830,12 +829,12 @@ def test_success(self, user_mgt_app): class TestListUsers: - @pytest.mark.parametrize('arg', [None, 'foo', list(), dict(), 0, -1, 1001, False]) + @pytest.mark.parametrize('arg', [None, 'foo', [], {}, 0, -1, 1001, False]) def test_invalid_max_results(self, user_mgt_app, arg): with pytest.raises(ValueError): auth.list_users(max_results=arg, app=user_mgt_app) - @pytest.mark.parametrize('arg', ['', list(), dict(), 0, -1, 1001, False]) + @pytest.mark.parametrize('arg', ['', [], {}, 0, -1, 1001, False]) def test_invalid_page_token(self, user_mgt_app, arg): with pytest.raises(ValueError): auth.list_users(page_token=arg, app=user_mgt_app) @@ -887,7 +886,7 @@ def test_list_users_paged_iteration(self, user_mgt_app): iterator = page.iterate_all() for index in range(3): user = next(iterator) - assert user.uid == 'user{0}'.format(index+1) + assert user.uid == f'user{index+1}' assert len(recorder) == 1 self._check_rpc_calls(recorder) @@ -912,7 +911,7 @@ def test_list_users_iterator_state(self, user_mgt_app): iterator = page.iterate_all() for user in iterator: index += 1 - assert user.uid == 'user{0}'.format(index) + assert user.uid == f'user{index}' if index == 2: break @@ -986,7 +985,7 @@ def _check_page(self, page): assert len(page.users) == 2 for user in page.users: assert isinstance(user, auth.ExportedUserRecord) - _check_user_record(user, 'testuser{0}'.format(index)) + _check_user_record(user, f'testuser{index}') assert user.password_hash == 'passwordHash' assert user.password_salt == 'passwordSalt' index += 1 @@ -1061,8 +1060,8 @@ class TestImportUserRecord: [{'email': arg} for arg in INVALID_STRINGS[1:] + ['not-an-email']] + [{'photo_url': arg} for arg in INVALID_STRINGS[1:] + ['not-a-url']] + [{'phone_number': arg} for arg in INVALID_STRINGS[1:] + ['not-a-phone']] + - [{'password_hash': arg} for arg in INVALID_STRINGS[1:] + [u'test']] + - [{'password_salt': arg} for arg in INVALID_STRINGS[1:] + [u'test']] + + [{'password_hash': arg} for arg in INVALID_STRINGS[1:] + ['test']] + + [{'password_salt': arg} for arg in INVALID_STRINGS[1:] + ['test']] + [{'custom_claims': arg} for arg in INVALID_DICTS[1:] + ['"json"', {'key': 'a'*1000}]] + [{'provider_data': arg} for arg in ['foo', 1, True]] ) @@ -1245,13 +1244,13 @@ def test_invalid_standard_scrypt(self, arg): class TestImportUsers: - @pytest.mark.parametrize('arg', [None, list(), tuple(), dict(), 0, 1, 'foo']) + @pytest.mark.parametrize('arg', [None, [], tuple(), {}, 0, 1, 'foo']) def test_invalid_users(self, user_mgt_app, arg): with pytest.raises(Exception): auth.import_users(arg, app=user_mgt_app) def test_too_many_users(self, user_mgt_app): - users = [auth.ImportUserRecord(uid='test{0}'.format(i)) for i in range(1001)] + users = [auth.ImportUserRecord(uid=f'test{i}') for i in range(1001)] with pytest.raises(ValueError): auth.import_users(users, app=user_mgt_app) @@ -1384,7 +1383,7 @@ def test_valid_data(self): {'android_install_app':'nonboolean'}, {'dynamic_link_domain': False}, {'ios_bundle_id':11}, - {'android_package_name':dict()}, + {'android_package_name':{}}, {'android_minimum_version':tuple()}, {'android_minimum_version':'7'}, {'android_install_app': True}]) diff --git a/tests/testutils.py b/tests/testutils.py index 0505eb6c7..598a929b4 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -33,7 +33,7 @@ def resource_filename(filename): def resource(filename): """Returns the contents of a test resource.""" - with open(resource_filename(filename), 'r') as file_obj: + with open(resource_filename(filename), 'r', encoding='utf-8') as file_obj: return file_obj.read() From 7f6e9f11be8edbbd0a7e877d5105fd26107777bf Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:05:51 -0400 Subject: [PATCH 5/6] chore: Upgraded Google API Core, Cloud Firestore, and Cloud Storage dependencies (#897) * chore: Bump dependencies * fix: Also update setup.py --- requirements.txt | 10 +++++----- setup.py | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3e67d1dd5..ff15072a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,9 +7,9 @@ pytest-asyncio >= 0.26.0 pytest-mock >= 3.6.1 respx == 0.22.0 -cachecontrol >= 0.12.14 -google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != 'PyPy' -google-cloud-firestore >= 2.19.0; platform.python_implementation != 'PyPy' -google-cloud-storage >= 1.37.1 -pyjwt[crypto] >= 2.5.0 +cachecontrol >= 0.14.3 +google-api-core[grpc] >= 2.25.1, < 3.0.0dev; platform.python_implementation != 'PyPy' +google-cloud-firestore >= 2.21.0; platform.python_implementation != 'PyPy' +google-cloud-storage >= 3.1.1 +pyjwt[crypto] >= 2.10.1 httpx[http2] == 0.28.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 25cf12672..21e29332e 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ (major, minor) = (sys.version_info.major, sys.version_info.minor) -if major != 3 or minor < 7: +if major != 3 or minor < 9: print('firebase_admin requires python >= 3.9', file=sys.stderr) sys.exit(1) @@ -37,11 +37,11 @@ long_description = ('The Firebase Admin Python SDK enables server-side (backend) Python developers ' 'to integrate Firebase into their services and applications.') install_requires = [ - 'cachecontrol>=0.12.14', - 'google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != "PyPy"', - 'google-cloud-firestore>=2.19.0; platform.python_implementation != "PyPy"', - 'google-cloud-storage>=1.37.1', - 'pyjwt[crypto] >= 2.5.0', + 'cachecontrol>=0.14.3', + 'google-api-core[grpc] >= 2.25.1, < 3.0.0dev; platform.python_implementation != "PyPy"', + 'google-cloud-firestore>=2.21.0; platform.python_implementation != "PyPy"', + 'google-cloud-storage>=3.1.1', + 'pyjwt[crypto] >= 2.10.1', 'httpx[http2] == 0.28.1', ] From 4dfb7399de352fcf2eb9c329c137042e041b5849 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:04:09 -0400 Subject: [PATCH 6/6] fix(functions): Remove usage of deprecated `datetime.utcnow() and fix flaky unit test` (#896) --- firebase_admin/functions.py | 5 ++-- tests/test_functions.py | 58 +++++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/firebase_admin/functions.py b/firebase_admin/functions.py index 86eea557a..6db0fbb42 100644 --- a/firebase_admin/functions.py +++ b/firebase_admin/functions.py @@ -15,7 +15,7 @@ """Firebase Functions module.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from urllib import parse import re import json @@ -255,7 +255,8 @@ def _validate_task_options( if not isinstance(opts.schedule_delay_seconds, int) \ or opts.schedule_delay_seconds < 0: raise ValueError('schedule_delay_seconds should be positive int.') - schedule_time = datetime.utcnow() + timedelta(seconds=opts.schedule_delay_seconds) + schedule_time = ( + datetime.now(timezone.utc) + timedelta(seconds=opts.schedule_delay_seconds)) task.schedule_time = schedule_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ') if opts.dispatch_deadline_seconds is not None: if not isinstance(opts.dispatch_deadline_seconds, int) \ diff --git a/tests/test_functions.py b/tests/test_functions.py index 1856426d9..52e92c1b2 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -14,7 +14,7 @@ """Test cases for the firebase_admin.functions module.""" -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import json import time import pytest @@ -33,8 +33,6 @@ _CLOUD_TASKS_URL + 'projects/test-project/locations/us-central1/queues/test-function-name/tasks' _DEFAULT_TASK_URL = _CLOUD_TASKS_URL + _DEFAULT_TASK_PATH _DEFAULT_RESPONSE = json.dumps({'name': _DEFAULT_TASK_PATH}) -_ENQUEUE_TIME = datetime.utcnow() -_SCHEDULE_TIME = _ENQUEUE_TIME + timedelta(seconds=100) class TestTaskQueue: @classmethod @@ -185,27 +183,46 @@ def _instrument_functions_service(self, app=None, status=200, payload=_DEFAULT_R testutils.MockAdapter(payload, status, recorder)) return functions_service, recorder - - @pytest.mark.parametrize('task_opts_params', [ - { + def test_task_options_delay_seconds(self): + _, recorder = self._instrument_functions_service() + enqueue_time = datetime.now(timezone.utc) + expected_schedule_time = enqueue_time + timedelta(seconds=100) + task_opts_params = { 'schedule_delay_seconds': 100, 'schedule_time': None, 'dispatch_deadline_seconds': 200, 'task_id': 'test-task-id', 'headers': {'x-test-header': 'test-header-value'}, 'uri': 'https://google.com' - }, - { + } + queue = functions.task_queue('test-function-name') + task_opts = functions.TaskOptions(**task_opts_params) + queue.enqueue(_DEFAULT_DATA, task_opts) + + assert len(recorder) == 1 + task = json.loads(recorder[0].body.decode())['task'] + + task_schedule_time = datetime.fromisoformat(task['schedule_time'].replace('Z', '+00:00')) + delta = abs(task_schedule_time - expected_schedule_time) + assert delta <= timedelta(seconds=1) + + assert task['dispatch_deadline'] == '200s' + assert task['http_request']['headers']['x-test-header'] == 'test-header-value' + assert task['http_request']['url'] in ['http://google.com', 'https://google.com'] + assert task['name'] == _DEFAULT_TASK_PATH + + def test_task_options_utc_time(self): + _, recorder = self._instrument_functions_service() + enqueue_time = datetime.now(timezone.utc) + expected_schedule_time = enqueue_time + timedelta(seconds=100) + task_opts_params = { 'schedule_delay_seconds': None, - 'schedule_time': _SCHEDULE_TIME, + 'schedule_time': expected_schedule_time, 'dispatch_deadline_seconds': 200, 'task_id': 'test-task-id', 'headers': {'x-test-header': 'test-header-value'}, 'uri': 'http://google.com' - }, - ]) - def test_task_options(self, task_opts_params): - _, recorder = self._instrument_functions_service() + } queue = functions.task_queue('test-function-name') task_opts = functions.TaskOptions(**task_opts_params) queue.enqueue(_DEFAULT_DATA, task_opts) @@ -213,19 +230,18 @@ def test_task_options(self, task_opts_params): assert len(recorder) == 1 task = json.loads(recorder[0].body.decode())['task'] - schedule_time = datetime.fromisoformat(task['schedule_time'][:-1]) - delta = abs(schedule_time - _SCHEDULE_TIME) - assert delta <= timedelta(seconds=15) + task_schedule_time = datetime.fromisoformat(task['schedule_time'].replace('Z', '+00:00')) + assert task_schedule_time == expected_schedule_time assert task['dispatch_deadline'] == '200s' assert task['http_request']['headers']['x-test-header'] == 'test-header-value' assert task['http_request']['url'] in ['http://google.com', 'https://google.com'] assert task['name'] == _DEFAULT_TASK_PATH - def test_schedule_set_twice_error(self): _, recorder = self._instrument_functions_service() - opts = functions.TaskOptions(schedule_delay_seconds=100, schedule_time=datetime.utcnow()) + opts = functions.TaskOptions( + schedule_delay_seconds=100, schedule_time=datetime.now(timezone.utc)) queue = functions.task_queue('test-function-name') with pytest.raises(ValueError) as excinfo: queue.enqueue(_DEFAULT_DATA, opts) @@ -236,9 +252,9 @@ def test_schedule_set_twice_error(self): @pytest.mark.parametrize('schedule_time', [ time.time(), - str(datetime.utcnow()), - datetime.utcnow().isoformat(), - datetime.utcnow().isoformat() + 'Z', + str(datetime.now(timezone.utc)), + datetime.now(timezone.utc).isoformat(), + datetime.now(timezone.utc).isoformat() + 'Z', '', ' ' ]) def test_invalid_schedule_time_error(self, schedule_time):