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/2] 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/2] 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):