Skip to content

chore(deps): Bump minimum supported Python version to 3.9 and add 3.13 to CIs #892

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -45,6 +45,7 @@ jobs:
pip install setuptools wheel
pip install tensorflow
pip install keras
pip install build

- name: Run unit tests
run: pytest
Expand All @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -56,6 +56,7 @@ jobs:
pip install setuptools wheel
pip install tensorflow
pip install keras
pip install build

- name: Run unit tests
run: pytest
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/)

Expand Down Expand Up @@ -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.

Expand Down
5 changes: 3 additions & 2 deletions firebase_admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}


Expand Down
6 changes: 3 additions & 3 deletions firebase_admin/_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
16 changes: 8 additions & 8 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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))
Expand All @@ -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.
Expand Down Expand Up @@ -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.')
Expand Down
2 changes: 1 addition & 1 deletion firebase_admin/_sseclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions firebase_admin/_token_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions firebase_admin/_user_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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())
Expand Down
20 changes: 10 additions & 10 deletions firebase_admin/app_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions firebase_admin/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion firebase_admin/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading