From 25f2e65fcd3c262dabf94b96e4f096de891e95fc Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:31:19 -0400 Subject: [PATCH 1/3] add ml api deprecation warnings --- firebase_admin/ml.py | 113 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 3 deletions(-) diff --git a/firebase_admin/ml.py b/firebase_admin/ml.py index 3a77dd05..bc780591 100644 --- a/firebase_admin/ml.py +++ b/firebase_admin/ml.py @@ -14,16 +14,24 @@ """Firebase ML module. +.. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + This module contains functions for creating, updating, getting, listing, deleting, publishing and unpublishing Firebase ML models. """ +# pylint: disable=too-many-lines import datetime import re import time import os from urllib import parse +import warnings import requests @@ -46,6 +54,13 @@ except ImportError: _TF_ENABLED = False +_DEPRECATION_WARNING = ( + 'Firebase ML is deprecated and will be shut down on June 15, 2027. ' + 'To host custom models, you must migrate to another solution. You can use Cloud Storage for ' + 'Firebase as an alternative for hosting custom models. For more info, see ' + 'https://firebase.google.com/docs/ml/migrate-to-cloud-storage' +) + _ML_ATTRIBUTE = '_ml' _MAX_PAGE_SIZE = 100 _MODEL_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{1,60}$') @@ -77,6 +92,12 @@ def _get_ml_service(app): def create_model(model, app=None): """Creates a model in the current Firebase project. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Args: model: An ml.Model to create. app: A Firebase app instance (or None to use the default app). @@ -84,6 +105,7 @@ def create_model(model, app=None): Returns: Model: The model that was created in Firebase ML. """ + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.create_model(model), app=app) @@ -91,6 +113,12 @@ def create_model(model, app=None): def update_model(model, app=None): """Updates a model's metadata or model file. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Args: model: The ml.Model to update. app: A Firebase app instance (or None to use the default app). @@ -98,6 +126,7 @@ def update_model(model, app=None): Returns: Model: The updated model. """ + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.update_model(model), app=app) @@ -105,6 +134,12 @@ def update_model(model, app=None): def publish_model(model_id, app=None): """Publishes a Firebase ML model. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + A published model can be downloaded to client apps. Args: @@ -114,6 +149,7 @@ def publish_model(model_id, app=None): Returns: Model: The published model. """ + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.set_published(model_id, publish=True), app=app) @@ -121,6 +157,12 @@ def publish_model(model_id, app=None): def unpublish_model(model_id, app=None): """Unpublishes a Firebase ML model. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Args: model_id: The id of the model to unpublish. app: A Firebase app instance (or None to use the default app). @@ -128,6 +170,7 @@ def unpublish_model(model_id, app=None): Returns: Model: The unpublished model. """ + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.set_published(model_id, publish=False), app=app) @@ -135,6 +178,12 @@ def unpublish_model(model_id, app=None): def get_model(model_id, app=None): """Gets the model specified by the given ID. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Args: model_id: The id of the model to get. app: A Firebase app instance (or None to use the default app). @@ -142,6 +191,7 @@ def get_model(model_id, app=None): Returns: Model: The requested model. """ + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.get_model(model_id), app=app) @@ -149,6 +199,12 @@ def get_model(model_id, app=None): def list_models(list_filter=None, page_size=None, page_token=None, app=None): """Lists the current project's models. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Args: list_filter: a list filter string such as ``tags:'tag_1'``. None will return all models. page_size: A number between 1 and 100 inclusive that specifies the maximum @@ -160,6 +216,7 @@ def list_models(list_filter=None, page_size=None, page_token=None, app=None): Returns: ListModelsPage: A (filtered) list of models. """ + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return ListModelsPage( ml_service.list_models, list_filter, page_size, page_token, app=app) @@ -168,10 +225,17 @@ def list_models(list_filter=None, page_size=None, page_token=None, app=None): def delete_model(model_id, app=None): """Deletes a model from the current project. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Args: model_id: The id of the model you wish to delete. app: A Firebase app instance (or None to use the default app). """ + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) ml_service.delete_model(model_id) @@ -179,12 +243,19 @@ def delete_model(model_id, app=None): class Model: """A Firebase ML Model object. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Args: display_name: The display name of your model - used to identify your model in code. tags: Optional list of strings associated with your model. Can be used in list queries. model_format: A subclass of ModelFormat. (e.g. TFLiteFormat) Specifies the model details. """ def __init__(self, display_name=None, tags=None, model_format=None): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) self._app = None # Only needed for wait_for_unlo self._data = {} self._model_format = None @@ -342,7 +413,14 @@ def as_dict(self, for_upload=False): class ModelFormat: - """Abstract base class representing a Model Format such as TFLite.""" + """Abstract base class representing a Model Format such as TFLite. + + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + """ def as_dict(self, for_upload=False): """Returns a serializable representation of the object.""" raise NotImplementedError @@ -351,10 +429,17 @@ def as_dict(self, for_upload=False): class TFLiteFormat(ModelFormat): """Model format representing a TFLite model. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Args: model_source: A TFLiteModelSource sub class. Specifies the details of the model source. """ def __init__(self, model_source=None): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) self._data = {} self._model_source = None @@ -412,7 +497,14 @@ def as_dict(self, for_upload=False): class TFLiteModelSource: - """Abstract base class representing a model source for TFLite format models.""" + """Abstract base class representing a model source for TFLite format models. + + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + """ def as_dict(self, for_upload=False): """Returns a serializable representation of the object.""" raise NotImplementedError @@ -466,11 +558,19 @@ def sign_uri(gcs_tflite_uri, app): class TFLiteGCSModelSource(TFLiteModelSource): - """TFLite model source representing a tflite model file stored in GCS.""" + """TFLite model source representing a tflite model file stored in GCS. + + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + """ _STORAGE_CLIENT = _CloudStorageClient() def __init__(self, gcs_tflite_uri, app=None): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) self._app = app self._gcs_tflite_uri = _validate_gcs_tflite_uri(gcs_tflite_uri) @@ -600,12 +700,19 @@ def as_dict(self, for_upload=False): class ListModelsPage: """Represents a page of models in a Firebase project. + .. deprecated:: + Firebase ML is deprecated and will be shut down on June 15, 2027. + To host custom models, you must migrate to another solution. You can use Cloud Storage for + Firebase as an alternative for hosting custom models. For more info, see + https://firebase.google.com/docs/ml/migrate-to-cloud-storage + Provides methods for traversing the models included in this page, as well as retrieving subsequent pages of models. The iterator returned by ``iterate_all()`` can be used to iterate through all the models in the Firebase project starting from this page. """ def __init__(self, list_models_func, list_filter, page_size, page_token, app): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) self._list_models_func = list_models_func self._list_filter = list_filter self._page_size = page_size From 4f28a370184539dbae6b797da9cd8ffc2c8c4084 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:32:57 -0400 Subject: [PATCH 2/3] Verify deprecation warnings in unit tests --- tests/test_ml.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/test_ml.py b/tests/test_ml.py index bcc93fd0..91a6d303 100644 --- a/tests/test_ml.py +++ b/tests/test_ml.py @@ -1086,3 +1086,72 @@ def test_list_models_no_models(self): assert len(page.models) == 0 models = list(page.iterate_all()) assert len(models) == 0 + + +class TestMLDeprecation: + """Tests for verifying deprecation warnings in the ML module.""" + + DEPRECATION_MSG = ( + r'Firebase ML is deprecated and will be shut down on June 15, 2027\. ' + r'To host custom models, you must migrate to another solution\. ' + r'You can use Cloud Storage for Firebase as an alternative for hosting custom models\. ' + r'For more info, see https://firebase\.google\.com/docs/ml/migrate-to-cloud-storage' + ) + + @classmethod + def setup_class(cls): + cred = testutils.MockCredential() + firebase_admin.initialize_app(cred, {'projectId': PROJECT_ID}) + ml._MLService.POLL_BASE_WAIT_TIME_SECONDS = 0.1 + ml.TFLiteGCSModelSource._STORAGE_CLIENT = _TestStorageClient() + + @classmethod + def teardown_class(cls): + testutils.cleanup_apps() + + def test_model_init_deprecation_warning(self): + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.Model() + + def test_tflite_format_init_deprecation_warning(self): + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.TFLiteFormat() + + def test_tflite_gcs_model_source_init_deprecation_warning(self): + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.TFLiteGCSModelSource('gs://bucket/model.tflite') + + def test_get_model_deprecation_warning(self): + instrument_ml_service(status=200, payload=DEFAULT_GET_RESPONSE) + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.get_model(MODEL_ID_1) + + def test_list_models_deprecation_warning(self): + instrument_ml_service(status=200, payload=ONE_PAGE_LIST_RESPONSE) + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.list_models() + + def test_delete_model_deprecation_warning(self): + instrument_ml_service(status=200, payload=EMPTY_RESPONSE) + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.delete_model(MODEL_ID_1) + + def test_create_model_deprecation_warning(self): + instrument_ml_service(status=200, payload=OPERATION_DONE_RESPONSE) + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.create_model(ml.Model(display_name=DISPLAY_NAME_1)) + + def test_update_model_deprecation_warning(self): + instrument_ml_service(status=200, payload=OPERATION_DONE_RESPONSE) + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.update_model(ml.Model(display_name=DISPLAY_NAME_1)) + + def test_publish_model_deprecation_warning(self): + instrument_ml_service(status=200, payload=OPERATION_DONE_RESPONSE) + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.publish_model(MODEL_ID_1) + + def test_unpublish_model_deprecation_warning(self): + instrument_ml_service(status=200, payload=OPERATION_DONE_RESPONSE) + with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): + ml.unpublish_model(MODEL_ID_1) From 6b561ee81363ec5473b2bb6b07eb88c5c6b0b789 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:12:54 -0400 Subject: [PATCH 3/3] add helper function to prevent duplicate warning flooding and unit tests for it --- firebase_admin/ml.py | 53 ++++++++++++++++++++++++++++++++++---------- tests/test_ml.py | 20 ++++++++++++++++- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/firebase_admin/ml.py b/firebase_admin/ml.py index bc780591..2257383b 100644 --- a/firebase_admin/ml.py +++ b/firebase_admin/ml.py @@ -27,9 +27,10 @@ # pylint: disable=too-many-lines import datetime +import os import re +import sys import time -import os from urllib import parse import warnings @@ -61,6 +62,23 @@ 'https://firebase.google.com/docs/ml/migrate-to-cloud-storage' ) + +def _is_internal_call(): + """Checks if the current call originated from within the ML module.""" + try: + # pylint: disable=protected-access + frame = sys._getframe(2) + while frame: + if frame.f_code.co_name in ( + 'from_dict', '_update_from_dict', 'handle_operation', 'list_models' + ): + return True + frame = frame.f_back + except (AttributeError, ValueError): + pass + return False + + _ML_ATTRIBUTE = '_ml' _MAX_PAGE_SIZE = 100 _MODEL_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{1,60}$') @@ -105,7 +123,8 @@ def create_model(model, app=None): Returns: Model: The model that was created in Firebase ML. """ - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.create_model(model), app=app) @@ -126,7 +145,8 @@ def update_model(model, app=None): Returns: Model: The updated model. """ - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.update_model(model), app=app) @@ -149,7 +169,8 @@ def publish_model(model_id, app=None): Returns: Model: The published model. """ - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.set_published(model_id, publish=True), app=app) @@ -170,7 +191,8 @@ def unpublish_model(model_id, app=None): Returns: Model: The unpublished model. """ - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.set_published(model_id, publish=False), app=app) @@ -191,7 +213,8 @@ def get_model(model_id, app=None): Returns: Model: The requested model. """ - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return Model.from_dict(ml_service.get_model(model_id), app=app) @@ -216,7 +239,8 @@ def list_models(list_filter=None, page_size=None, page_token=None, app=None): Returns: ListModelsPage: A (filtered) list of models. """ - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) return ListModelsPage( ml_service.list_models, list_filter, page_size, page_token, app=app) @@ -235,7 +259,8 @@ def delete_model(model_id, app=None): model_id: The id of the model you wish to delete. app: A Firebase app instance (or None to use the default app). """ - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) ml_service = _get_ml_service(app) ml_service.delete_model(model_id) @@ -255,7 +280,8 @@ class Model: model_format: A subclass of ModelFormat. (e.g. TFLiteFormat) Specifies the model details. """ def __init__(self, display_name=None, tags=None, model_format=None): - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) self._app = None # Only needed for wait_for_unlo self._data = {} self._model_format = None @@ -439,7 +465,8 @@ class TFLiteFormat(ModelFormat): model_source: A TFLiteModelSource sub class. Specifies the details of the model source. """ def __init__(self, model_source=None): - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) self._data = {} self._model_source = None @@ -570,7 +597,8 @@ class TFLiteGCSModelSource(TFLiteModelSource): _STORAGE_CLIENT = _CloudStorageClient() def __init__(self, gcs_tflite_uri, app=None): - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) self._app = app self._gcs_tflite_uri = _validate_gcs_tflite_uri(gcs_tflite_uri) @@ -712,7 +740,8 @@ class ListModelsPage: Firebase project starting from this page. """ def __init__(self, list_models_func, list_filter, page_size, page_token, app): - warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) + if not _is_internal_call(): + warnings.warn(_DEPRECATION_WARNING, DeprecationWarning, stacklevel=2) self._list_models_func = list_models_func self._list_filter = list_filter self._page_size = page_size diff --git a/tests/test_ml.py b/tests/test_ml.py index 91a6d303..af97b174 100644 --- a/tests/test_ml.py +++ b/tests/test_ml.py @@ -15,6 +15,7 @@ """Test cases for the firebase_admin.ml module.""" import json +import warnings import pytest @@ -1119,7 +1120,7 @@ def test_tflite_format_init_deprecation_warning(self): def test_tflite_gcs_model_source_init_deprecation_warning(self): with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): - ml.TFLiteGCSModelSource('gs://bucket/model.tflite') + ml.TFLiteGCSModelSource(GCS_TFLITE_URI) def test_get_model_deprecation_warning(self): instrument_ml_service(status=200, payload=DEFAULT_GET_RESPONSE) @@ -1155,3 +1156,20 @@ def test_unpublish_model_deprecation_warning(self): instrument_ml_service(status=200, payload=OPERATION_DONE_RESPONSE) with pytest.warns(DeprecationWarning, match=self.DEPRECATION_MSG): ml.unpublish_model(MODEL_ID_1) + + def test_single_warning_on_get_model(self): + instrument_ml_service(status=200, payload=DEFAULT_GET_RESPONSE) + with warnings.catch_warnings(record=True) as captured_warnings: + warnings.simplefilter('always') + ml.get_model(MODEL_ID_1) + assert len(captured_warnings) == 1 + assert issubclass(captured_warnings[0].category, DeprecationWarning) + + def test_single_warning_on_list_models(self): + instrument_ml_service(status=200, payload=ONE_PAGE_LIST_RESPONSE) + with warnings.catch_warnings(record=True) as captured_warnings: + warnings.simplefilter('always') + page = ml.list_models() + _ = page.models + assert len(captured_warnings) == 1 + assert issubclass(captured_warnings[0].category, DeprecationWarning)