Repository: CartoDB/cartoframes
Pull request #1697 (2020-10-20 12:21:58)

Deprecating DataObsClient
 - Added `deprecated` decorator
 - Deprecating `DataObsClient`
 - Added tests for the `deprecated` decorator
 - Added tests for the deprecation of `DataObsClient`



Patch:
diff --git a/cartoframes/data/clients/data_obs_client.py b/cartoframes/data/clients/data_obs_client.py
index c9097de0..d2527327 100644
--- a/cartoframes/data/clients/data_obs_client.py
+++ b/cartoframes/data/clients/data_obs_client.py
@@ -9,14 +9,16 @@ from carto.exceptions import CartoException
 
 from ...io.carto import read_carto, to_carto
 from ...utils import utils
+from ...utils.utils import deprecated
 from ...io.managers.context_manager import ContextManager
 
 
+@deprecated(message='The Data Observatory v1 is being deprecated. Use the `data.observatory` package instead')
 class DataObsClient:
-    """Data Observatory v1 class. `Data Observatory documentation
+    """Data Observatory v1 class (deprecated). `Data Observatory v1 documentation (deprecated)
     <https://carto.com/developers/data-observatory/>`__.
 
-    This class provides the following methods to interact with Data Observatory:
+    This class provides the following methods to interact with Data Observatory v1 (deprecated):
         - boundaries: returns a geopandas.GeoDataFrame with
             the geographic boundaries (geometries) or their metadata.
         - discovery: returns a pandas.DataFrame with the measures found.
@@ -33,6 +35,7 @@ class DataObsClient:
         self._credentials = credentials
         self._manager = ContextManager(credentials)
 
+    @deprecated(message='The Data Observatory v1 is being deprecated. Use the `data.observatory` package instead')
     def boundaries(self, boundary=None, region=None, decode_geom=False,
                    timespan=None, include_nonclipped=False):
         """
@@ -63,7 +66,8 @@ class DataObsClient:
 
             Get census tracts around Idaho Falls, Idaho, USA, and add median
             income from the US census. Without limiting the metadata, we get
-            median income measures for each census in the Data Observatory.
+            median income measures for each census in the
+            Data Observatory v1 (deprecated).
 
             >>> # Note: default credentials will be supported in a future release
             >>> do = DataObsClient(credentials)
@@ -94,8 +98,8 @@ class DataObsClient:
                 Municipios have an ID of ``br.geo.municipios``. Find IDs by
                 running :py:meth:`DataObsClient.boundaries
                 <cartoframes.client.DataObsClient.boundaries>`
-                without any arguments, or by looking in the `Data Observatory
-                catalog <http://cartodb.github.io/bigmetadata/>`__.
+                without any arguments, or by looking in the `Data Observatory v1 catalog (deprecated)
+                <http://cartodb.github.io/bigmetadata/>`__.
             region (str, optional):
                 Region where boundary information or,
                 if `boundary` is specified, boundary polygons are of interest.
@@ -111,7 +115,7 @@ class DataObsClient:
                         ``[5.9559111595,45.8179931641,10.4920501709,47.808380127]``
             timespan (str, optional):
                 Specific timespan to get geometries from.
-                Defaults to use the most recent. See the Data Observatory catalog
+                Defaults to use the most recent. See the Data Observatory v1 catalog (deprecated)
                 for more information.
             decode_geom (bool, optional):
                 Whether to return the geometries as
@@ -193,14 +197,15 @@ class DataObsClient:
                 time=utils.pgquote(timespan))
         return self._fetch(query, decode_geom=decode_geom)
 
+    @deprecated(message='The Data Observatory v1 is being deprecated. Use the `data.observatory` package instead')
     def discovery(self, region, keywords=None, regex=None, time=None,
                   boundaries=None, include_quantiles=False):
-        """Discover Data Observatory measures. This method returns the full
-        Data Observatory metadata model for each measure or measures that
+        """Discover Data Observatory v1 measures (deprecated). This method returns the full
+        Data Observatory v1 metadata model (deprecated) for each measure or measures that
         match the conditions from the inputs. The full metadata in each row
         uniquely defines a measure based on the timespan, geographic
         resolution, and normalization (if any). Read more about the metadata
-        response in `Data Observatory
+        response in `Data Observatory v1 (deprecated)
         <https://carto.com/developers/data-observatory/reference/#discovery-functions>`__
         documentation.
 
@@ -223,13 +228,13 @@ class DataObsClient:
 
             To remove the guesswork in how geographical resolutions are
             selected, specify one or more boundaries in `boundaries`. See
-            the boundaries section for each region in the `Data Observatory
+            the boundaries section for each region in the `Data Observatory v1 (deprecated)
             catalog <http://cartodb.github.io/bigmetadata/>`__.
 
         The metadata returned from this method can then be used to create raw
         tables or for augmenting an existing table from these measures using
         :py:meth:`DataObsClient.augment <cartoframes.client.DataObsClient.augment>`.
-        For the full Data Observatory catalog, visit
+        For the full Data Observatory v1 catalog (deprecated), visit
         https://cartodb.github.io/bigmetadata/. When working with the metadata
         DataFrame returned from this method, be careful to only remove rows not
         columns as `DataObsClient.augment <cartoframes.client.DataObsClient.augment>`
@@ -269,8 +274,8 @@ class DataObsClient:
                         with geometries. The region will be the bounding box of
                         the table.
 
-                  .. Note:: If a table name is also a valid Data Observatory
-                      region name, the Data Observatory name will be chosen
+                  .. Note:: If a table name is also a valid Data Observatory v1 (deprecated)
+                      region name, the Data Observatory v1 (deprecated) name will be chosen
                       over the table.
 
                 - bounding box (list of float):
@@ -298,8 +303,8 @@ class DataObsClient:
                 for more information.
             boundaries (str or list of str, optional):
                 Boundary or list of boundaries that specify the measure resolution. See the
-                boundaries section for each region in the `Data Observatory
-                catalog <http://cartodb.github.io/bigmetadata/>`__.
+                boundaries section for each region in the `Data Observatory v1
+                catalog (deprecated) <http://cartodb.github.io/bigmetadata/>`__.
             include_quantiles (bool, optional):
                 Include quantiles calculations which are a calculation
                 of how a measure compares to all measures in the full GeoDataFrame.
@@ -459,18 +464,19 @@ class DataObsClient:
         utils.debug_print(self._verbose, query=query)
         return DataFrame(self._fetch(query, decode_geom=True))
 
+    @deprecated(message='The Data Observatory v1 is being deprecated. Use the `data.observatory` package instead')
     def augment(self, table_name, metadata, persist_as=None, how='the_geom'):
-        """Get an augmented CARTO dataset with `Data Observatory
+        """Get an augmented CARTO dataset with `Data Observatory v1 (deprecated)
         <https://carto.com/data-observatory>`__ measures. Use
         `DataObsClient.discovery
         <#DataObsClient.discovery>`__ to search for available
-        measures, or see the full `Data Observatory catalog
+        measures, or see the full `Data Observatory v1 catalog (deprecated)
         <https://cartodb.github.io/bigmetadata/index.html>`__. Optionally
         persist the data as a new table.
 
         Example:
-            Get a DataFrame with Data Observatory measures based on the
-            geometries in a CARTO table.
+            Get a DataFrame with Data Observatory v1 measures (deprecated) based
+            on the geometries in a CARTO table.
 
             >>> do = DataObsClient(credentials)
             >>> median_income = do.discovery(
@@ -479,7 +485,7 @@ class DataObsClient:
             ...     time='2011 - 2015')
             >>> ds = do.augment('transaction_events', median_income)
 
-            Pass in cherry-picked measures from the Data Observatory catalog.
+            Pass in cherry-picked measures from the Data Observatory v1 catalog (deprecated).
             The rest of the metadata will be filled in, but it's important to
             specify the geographic level as this will not show up in the column
             name.
@@ -491,7 +497,7 @@ class DataObsClient:
 
         Args:
             table_name (str):
-                Name of table on CARTO account that Data Observatory measures
+                Name of table on CARTO account that Data Observatory v1 measures (deprecated)
                 are to be added to.
             metadata (pandas.DataFrame):
                 List of all measures to add to
@@ -525,7 +531,7 @@ class DataObsClient:
                 If metadata object is invalid or empty, or if the number of
                 requested measures exceeds 50.
             CartoException:
-                If user account consumes all of Data Observatory quota
+                If user account consumes all of Data Observatory v1 (deprecated) quota
         """
 
         if isinstance(metadata, DataFrame):
diff --git a/cartoframes/utils/utils.py b/cartoframes/utils/utils.py
index be917164..492d37a3 100644
--- a/cartoframes/utils/utils.py
+++ b/cartoframes/utils/utils.py
@@ -11,11 +11,14 @@ import decimal
 import hashlib
 import inspect
 import requests
+import warnings
+import functools
 import geopandas
 import numpy as np
 import pkg_resources
 import semantic_version
 
+
 from functools import wraps
 from datetime import datetime, timezone
 from warnings import catch_warnings, filterwarnings
@@ -548,3 +551,23 @@ def get_parameter_from_decorator(parameter_name, decorated_function, *args, **kw
             pass
 
     return parameter
+
+
+def deprecated(message=''):
+    def decorator(func):
+
+        @functools.wraps(func)
+        def decorated(*args, **kwargs):
+            warnings.simplefilter('always', DeprecationWarning)
+            warnings.warn(
+                'The {type_} `{name}` is deprecated. {message}'.format(
+                    type_='class' if inspect.isclass(func) else 'function',
+                    name=func.__name__, message=message),
+                category=DeprecationWarning, stacklevel=2)
+            warnings.simplefilter('default', DeprecationWarning)
+
+            return func(*args, **kwargs)
+
+        return decorated
+
+    return decorator


Test patch:
diff --git a/tests/e2e/data/client/test_data_obs_client.py b/tests/e2e/data/client/test_data_obs_client.py
index fa21dd20..ebe50806 100644
--- a/tests/e2e/data/client/test_data_obs_client.py
+++ b/tests/e2e/data/client/test_data_obs_client.py
@@ -22,6 +22,56 @@ from tests.e2e.helpers import _UserUrlLoader
 warnings.filterwarnings('ignore')
 
 
+class TestDataObsClientDeprecation(unittest.TestCase, _UserUrlLoader):
+    def setUp(self):
+        self.credentials = Credentials(None, None, self.user_url().format(username=None))
+
+    def test_class_deprecation(self):
+        with warnings.catch_warnings(record=True) as w:
+            _ = DataObsClient(self.credentials)
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert 'deprecated' in str(w[-1].message)
+
+    def test_boundaries_deprecation(self):
+        with warnings.catch_warnings(record=True) as w:
+            do = DataObsClient(self.credentials)
+
+        with warnings.catch_warnings(record=True) as w:
+            try:
+                do.boundaries()
+            except Exception:
+                pass
+
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert 'deprecated' in str(w[-1].message)
+
+    def test_discovery_deprecation(self):
+        with warnings.catch_warnings(record=True) as w:
+            do = DataObsClient(self.credentials)
+
+        with warnings.catch_warnings(record=True) as w:
+            try:
+                do.discovery()
+            except Exception:
+                pass
+
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert 'deprecated' in str(w[-1].message)
+
+    def test_augment_deprecation(self):
+        with warnings.catch_warnings(record=True) as w:
+            do = DataObsClient(self.credentials)
+
+        with warnings.catch_warnings(record=True) as w:
+            try:
+                do.augment()
+            except Exception:
+                pass
+
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert 'deprecated' in str(w[-1].message)
+
+
 @pytest.mark.skip()
 class TestDataObsClient(unittest.TestCase, _UserUrlLoader):
     """Tests for cartoframes.client.DataObsClient"""
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
index fc0ba618..c0942688 100644
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -2,6 +2,7 @@
 
 """Unit tests for cartoframes.utils"""
 import unittest
+import warnings
 from collections import OrderedDict
 
 import requests
@@ -9,7 +10,7 @@ import numpy as np
 
 from cartoframes.utils.utils import (camel_dictionary, cssify, debug_print, dict_items,
                                      importify_params, snake_to_camel, dtypes2pg, pg2dtypes,
-                                     encode_row, extract_viz_columns, remove_comments)
+                                     encode_row, extract_viz_columns, remove_comments, deprecated)
 
 
 class TestUtils(unittest.TestCase):
@@ -222,3 +223,27 @@ class TestUtils(unittest.TestCase):
            multiline comment */
         """
         assert remove_comments(viz) == 'color: blue'
+
+    def test_deprecated_decorator_class(self):
+        @deprecated(message='my message')
+        class MyClass:
+            pass
+
+        with warnings.catch_warnings(record=True) as w:
+            _ = MyClass()
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert 'deprecated' in str(w[-1].message)
+            assert 'my message' in str(w[-1].message)
+            assert 'class' in str(w[-1].message)
+
+    def test_deprecated_decorator_function(self):
+        @deprecated(message='my message')
+        def my_function():
+            pass
+
+        with warnings.catch_warnings(record=True) as w:
+            my_function()
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert 'deprecated' in str(w[-1].message)
+            assert 'my message' in str(w[-1].message)
+            assert 'function' in str(w[-1].message)