Repository: CartoDB/cartoframes
Pull request #1388 (2019-12-18 14:57:50)

Add CLI subscription
Closes: https://github.com/CartoDB/cartoframes/issues/1341

- Unify Credentials check in the Catalog
- Add CLI prompt to subscribe


Patch:
diff --git a/cartoframes/data/observatory/catalog/catalog.py b/cartoframes/data/observatory/catalog/catalog.py
index 3d1ed50a..d344f334 100644
--- a/cartoframes/data/observatory/catalog/catalog.py
+++ b/cartoframes/data/observatory/catalog/catalog.py
@@ -6,7 +6,7 @@ from .geography import Geography
 from .subscriptions import Subscriptions
 from .repository.constants import COUNTRY_FILTER, CATEGORY_FILTER, GEOGRAPHY_FILTER, PROVIDER_FILTER
 
-from ....auth import Credentials, defaults
+from ....utils.utils import get_credentials
 
 
 class Catalog(object):
@@ -280,10 +280,7 @@ class Catalog(object):
         """
 
         _no_filters = {}
-        _credentials = credentials or defaults.get_default_credentials()
-
-        if not isinstance(_credentials, Credentials):
-            raise ValueError('`credentials` must be a Credentials class instance')
+        _credentials = get_credentials(credentials)
 
         return Subscriptions(
             Dataset.get_all(_no_filters, _credentials),
diff --git a/cartoframes/data/observatory/catalog/dataset.py b/cartoframes/data/observatory/catalog/dataset.py
index fd5b931c..83c4a6e0 100644
--- a/cartoframes/data/observatory/catalog/dataset.py
+++ b/cartoframes/data/observatory/catalog/dataset.py
@@ -16,7 +16,7 @@ from . import subscription_info
 from . import subscriptions
 from . import utils
 from ....core.logger import log
-from ....auth import Credentials, defaults
+from ....utils.utils import get_credentials, check_credentials
 
 DATASET_TYPE = 'dataset'
 
@@ -361,6 +361,8 @@ class Dataset(CatalogEntity):
         :raises DiscoveryException: When no datasets are found.
         :raises CartoException: If there's a problem when connecting to the catalog.
         """
+        if credentials is not None:
+            check_credentials(credentials)
 
         return cls._entity_repo.get_all(filters, credentials)
 
@@ -384,10 +386,12 @@ class Dataset(CatalogEntity):
         :raises CartoException: If you have not a valid license for the dataset being downloaded.
         :raises ValueError: If the credentials argument is not valid.
         """
-        if not self._is_subscribed(credentials):
+        _credentials = get_credentials(credentials)
+
+        if not self._is_subscribed(_credentials):
             raise CartoException('You are not subscribed to this Dataset yet. Please, use the subscribe method first.')
 
-        self._download(file_path, credentials)
+        self._download(file_path, _credentials)
 
     @classmethod
     def get_datasets_spatial_filtered(cls, filter_dataset):
@@ -445,8 +449,7 @@ class Dataset(CatalogEntity):
 
         :raises CartoException: If there's a problem when connecting to the catalog.
         """
-
-        _credentials = self._get_credentials(credentials)
+        _credentials = get_credentials(credentials)
         _subscribed_ids = subscriptions.get_subscription_ids(_credentials)
 
         if self.id in _subscribed_ids:
@@ -469,22 +472,16 @@ class Dataset(CatalogEntity):
 
         :raises CartoException: If there's a problem when connecting to the catalog.
         """
-
-        _credentials = self._get_credentials(credentials)
+        _credentials = get_credentials(credentials)
 
         return subscription_info.SubscriptionInfo(
             subscription_info.fetch_subscription_info(self.id, DATASET_TYPE, _credentials))
 
-    def _is_subscribed(self, credentials=None):
+    def _is_subscribed(self, credentials):
         if self.is_public_data:
             return True
 
-        _credentials = credentials or defaults.get_default_credentials()
-
-        if not isinstance(_credentials, Credentials):
-            raise ValueError('`credentials` must be a Credentials class instance')
-
-        datasets = Dataset.get_all({}, _credentials)
+        datasets = Dataset.get_all({}, credentials)
 
         return datasets is not None and self in datasets
 
diff --git a/cartoframes/data/observatory/catalog/entity.py b/cartoframes/data/observatory/catalog/entity.py
index c5ac212d..87072426 100644
--- a/cartoframes/data/observatory/catalog/entity.py
+++ b/cartoframes/data/observatory/catalog/entity.py
@@ -6,7 +6,6 @@ from google.api_core.exceptions import NotFound
 from carto.exceptions import CartoException
 
 from ...clients.bigquery_client import BigQueryClient
-from ....auth import Credentials, defaults
 from ....core.logger import log
 
 
@@ -110,11 +109,10 @@ class CatalogEntity(ABC):
 
         return self.id
 
-    def _download(self, file_path, credentials=None):
+    def _download(self, file_path, credentials):
         if not self._is_available_in('bq'):
             raise CartoException('{} is not ready for Download. Please, contact us for more information.'.format(self))
 
-        credentials = self._get_credentials(credentials)
         bq_client = _get_bigquery_client(credentials)
 
         full_remote_table_name = self._get_remote_full_table_name(
@@ -140,14 +138,6 @@ class CatalogEntity(ABC):
     def _is_available_in(self, platform=_PLATFORM_BQ):
         return self.data['available_in'] and platform in self.data['available_in']
 
-    def _get_credentials(self, credentials=None):
-        _credentials = credentials or defaults.get_default_credentials()
-
-        if not isinstance(_credentials, Credentials):
-            raise ValueError('`credentials` must be a Credentials class instance')
-
-        return _credentials
-
     def _get_remote_full_table_name(self, user_project, user_dataset, public_project):
         project, dataset, table = self.id.split('.')
 
diff --git a/cartoframes/data/observatory/catalog/geography.py b/cartoframes/data/observatory/catalog/geography.py
index 533bd390..7530e1d7 100644
--- a/cartoframes/data/observatory/catalog/geography.py
+++ b/cartoframes/data/observatory/catalog/geography.py
@@ -7,7 +7,7 @@ from .repository.constants import GEOGRAPHY_FILTER
 from . import subscription_info
 from . import subscriptions
 from . import utils
-from ....auth import Credentials, defaults
+from ....utils.utils import get_credentials, check_credentials
 
 GEOGRAPHY_TYPE = 'geography'
 
@@ -195,6 +195,8 @@ class Geography(CatalogEntity):
         :raises DiscoveryException: When no geographies are found.
         :raises CartoException: If there's a problem when connecting to the catalog.
         """
+        if credentials is not None:
+            check_credentials(credentials)
 
         return cls._entity_repo.get_all(filters, credentials)
 
@@ -218,11 +220,13 @@ class Geography(CatalogEntity):
         :raises CartoException: If you have not a valid license for the dataset being downloaded.
         :raises ValueError: If the credentials argument is not valud.
         """
-        if not self._is_subscribed(credentials):
+        _credentials = get_credentials(credentials)
+
+        if not self._is_subscribed(_credentials):
             raise CartoException('You are not subscribed to this Geography yet. Please, use the subscribe method '
                                  'first.')
 
-        return self._download(file_path, credentials)
+        self._download(file_path, _credentials)
 
     def subscribe(self, credentials=None):
         """Subscribe to a Geography. You need Data Observatory enabled in your CARTO account, please contact us at
@@ -250,8 +254,7 @@ class Geography(CatalogEntity):
 
         :raises CartoException: If there's a problem when connecting to the catalog.
         """
-
-        _credentials = self._get_credentials(credentials)
+        _credentials = get_credentials(credentials)
         _subscribed_ids = subscriptions.get_subscription_ids(_credentials)
 
         if self.id in _subscribed_ids:
@@ -274,22 +277,18 @@ class Geography(CatalogEntity):
 
         :raises CartoException: If there's a problem when connecting to the catalog.
         """
-
-        _credentials = self._get_credentials(credentials)
+        _credentials = get_credentials(credentials)
 
         return subscription_info.SubscriptionInfo(
             subscription_info.fetch_subscription_info(self.id, GEOGRAPHY_TYPE, _credentials))
 
-    def _is_subscribed(self, credentials=None):
-
+    def _is_subscribed(self, credentials):
         if self.is_public_data:
             return True
 
-        _credentials = credentials or defaults.get_default_credentials()
-
-        if not isinstance(_credentials, Credentials):
-            raise ValueError('`credentials` must be a Credentials class instance')
-
-        geographies = Geography.get_all({}, _credentials)
+        geographies = Geography.get_all({}, credentials)
 
         return geographies is not None and self in geographies
+
+    def __str__(self):
+        return "<Geography.get('{}')>".format(self._get_print_id())
diff --git a/cartoframes/data/observatory/catalog/utils.py b/cartoframes/data/observatory/catalog/utils.py
index 4e7076e8..d960164a 100644
--- a/cartoframes/data/observatory/catalog/utils.py
+++ b/cartoframes/data/observatory/catalog/utils.py
@@ -1,8 +1,6 @@
 from .subscriptions import trigger_subscription
 from .subscription_info import fetch_subscription_info
 
-from ....core.logger import log
-
 
 def is_ipython_notebook():
     """
@@ -23,58 +21,71 @@ if is_ipython_notebook():
     from ipywidgets.widgets import HTML, Layout, Button, GridspecLayout
 
 
-def display_existing_subscription_message(id, type):
+def display_existing_subscription_message(entity_id, entity_type):
     if is_ipython_notebook():
-        message = '''
-        <h3>Subscription already purchased</h3>
-        The {0} <b>{1}</b> has already been purchased.
-        '''.format(type, id)
-        text = HTML(message)
-        display(text)
-
-
-def display_subscription_form(id, type, credentials):
-    info = fetch_subscription_info(id, type, credentials)
+        _display_existing_subscription_message_notebook(entity_id, entity_type)
+    else:
+        _display_existing_subscription_message_cli(entity_id, entity_type)
 
-    if info.get('type') != type:
-        raise Exception('Incorrect type returned.')
 
-    if info.get('subscription_list_price') is None:
-        raise Exception('This {} has incomplete information. Please contact to support@carto.com.'.format(type))
+def display_subscription_form(entity_id, entity_type, credentials):
+    info = fetch_subscription_info(entity_id, entity_type, credentials)
 
     if is_ipython_notebook():
-        display_subscription_form_notebook(id, info, credentials)
+        _display_subscription_form_notebook(entity_id, entity_type, info, credentials)
     else:
-        display_subscription_form_cli()
+        _display_subscription_form_cli(entity_id, entity_type, info, credentials)
+
+
+def _display_existing_subscription_message_notebook(entity_id, entity_type):
+    message = '''
+        <h3>Subscription already purchased</h3>
+        The {0} <b>{1}</b> has already been purchased.
+        '''.format(entity_type, entity_id)
+    display(HTML(message))
+
 
+def _display_existing_subscription_message_cli(entity_id, entity_type):
+    message = ('Subscription already purchased:\n' +
+               '- The {0} "{1}" has already been purchased.'.format(entity_type, entity_id))
+    print(message)
 
-def display_subscription_form_notebook(id, info, credentials):
+
+def _display_subscription_form_notebook(entity_id, entity_type, info, credentials):
     message = '''
     <h3>Subscription contract</h3>
     You are about to subscribe to <b>{id}</b>.
-    The cost of this {type} is <b>${subscription_list_price}</b>.
+    The cost of this {type} is <b>${price}</b>.
     If you want to proceed, a Request will be sent to CARTO who will
     order the data and load it into your account.
     This {type} is available for Instant Order for your organization,
     so it will automatically process the order and you will get immediate access to the {type}.
     In order to proceed we need you to agree to the License of the {type}
-    available at <b><a href="{tos_link}" target="_blank">this link</a></b>.
+    available at <b><a href="{link}" target="_blank">this link</a></b>.
     <br>Do you want to proceed?
-    '''.format(**info)
-
-    ok_response = '''
-    <b>Congrats!</b><br>The {type} <b>{id}</b> has been requested and it will be available in your account soon.
-    '''.format(**info)
-    cancel_message = '''
-    The {type} <b>{id}</b> has not been purchased.
-    '''.format(**info)
-
-    text, buttons = _create_notebook_form(id, info.get('type'), message, ok_response, cancel_message, credentials)
+    '''.format(
+        id=entity_id,
+        type=entity_type,
+        price=info.get('subscription_list_price'),
+        link=info.get('tos_link'))
+
+    responses = {
+        'ok': '''
+        <b>Congrats!</b><br>The {type} <b>{id}</b> has been requested and it will be available in your account soon.
+        '''.format(id=entity_id, type=entity_type),
+        'cancel': '''
+        The {type} <b>{id}</b> has not been purchased.
+        '''.format(id=entity_id, type=entity_type),
+        'error': '''
+        Subscription error. Please contact to support@carto.com.
+        '''}
+
+    text, buttons = _create_notebook_form(entity_id, entity_type, message, responses, credentials)
 
     display(text, buttons)
 
 
-def _create_notebook_form(id, type, message, ok_response, cancel_message, credentials):
+def _create_notebook_form(entity_id, entity_type, message, responses, credentials):
     text = HTML(message)
 
     button_yes = Button(
@@ -92,15 +103,15 @@ def _create_notebook_form(id, type, message, ok_response, cancel_message, creden
 
     def on_button_yes_clicked(b):
         disable_buttons()
-        response = trigger_subscription(id, type, credentials)
+        response = trigger_subscription(entity_id, entity_type, credentials)
         if response:
-            display(HTML(ok_response))
+            display(HTML(responses.get('ok')))
         else:
-            display(HTML('Error'))
+            display(HTML(responses.get('error')))
 
     def on_button_no_clicked(b):
         disable_buttons()
-        display(HTML(cancel_message))
+        display(HTML(responses.get('cancel')))
 
     button_yes.on_click(on_button_yes_clicked)
     button_no.on_click(on_button_no_clicked)
@@ -108,5 +119,42 @@ def _create_notebook_form(id, type, message, ok_response, cancel_message, creden
     return (text, buttons)
 
 
-def display_subscription_form_cli():
-    log.info('This method is not yet implemented in CLI')
+def _display_subscription_form_cli(entity_id, entity_type, info, credentials):
+    message = (
+        'Subscription contract:\n'
+        'You are about to subscribe to "{id}". '
+        'The cost of this {type} is ${price}. '
+        'If you want to proceed, a Request will be sent to CARTO who will '
+        'order the data and load it into your account. '
+        'This {type} is available for Instant Order for your organization, '
+        'so it will automatically process the order and you will get immediate access to the {type}. '
+        'In order to proceed we need you to agree to the License of the {type} '
+        'available at this link: {link}.\n'
+        'Do you want to proceed?').format(
+            id=entity_id,
+            type=entity_type,
+            price=info.get('subscription_list_price'),
+            link=info.get('tos_link'))
+
+    responses = {
+        'ok': ('Congrats! The {type} "{id}" has been requested and '
+               'it will be available in your account soon.').format(id=entity_id, type=entity_type),
+        'cancel': 'The {type} "{id}" has not been purchased'.format(id=entity_id, type=entity_type),
+        'error': 'Subscription error. Please contact to support@carto.com.'}
+
+    response = _create_cli_form(entity_id, entity_type, message, responses, credentials)
+
+    print(response)
+
+
+def _create_cli_form(entity_id, entity_type, message, responses, credentials):
+    answer_yes = input('{} (y/N): '.format(message)).lower() == 'y'
+
+    if answer_yes:
+        response = trigger_subscription(entity_id, entity_type, credentials)
+        if response:
+            return responses.get('ok')
+        else:
+            return responses.get('error')
+    else:
+        return responses.get('cancel')
diff --git a/cartoframes/data/services/geocoding.py b/cartoframes/data/services/geocoding.py
index b750740f..aeceee39 100644
--- a/cartoframes/data/services/geocoding.py
+++ b/cartoframes/data/services/geocoding.py
@@ -2,8 +2,6 @@
 
 import re
 
-from carto.exceptions import CartoException
-
 from .utils import geocoding_utils
 from .utils import geocoding_constants
 from .utils import TableGeocodingLock
@@ -13,8 +11,6 @@ from ...core.logger import log
 from ...core.managers.source_manager import SourceManager
 from ...io.carto import read_carto, to_carto, has_table, delete_table, update_table, copy_table, create_table_from_query
 
-CARTO_INDEX_KEY = 'cartodb_id'
-
 
 class Geocoding(Service):
     """Geocoding using CARTO data services.
@@ -242,8 +238,8 @@ class Geocoding(Service):
 
         cdf = read_carto(input_table_name, self._credentials)
 
-        if self._source_manager.is_dataframe() and CARTO_INDEX_KEY in cdf:
-            del cdf[CARTO_INDEX_KEY]
+        if self._source_manager.is_dataframe():
+            del cdf['cartodb_id']
 
         if is_temporary:
             delete_table(input_table_name, self._credentials, log_enabled=False)
@@ -381,66 +377,56 @@ class Geocoding(Service):
 
         aborted = False
 
-        if not dry_run:
-            available_quota = self.available_quota()
-            if output['required_quota'] > available_quota:
-                raise CartoException('Your CARTO account does not have enough Geocoding quota: {}/{}'.format(
-                    output['required_quota'],
-                    available_quota
-                ))
-
-            if output['required_quota'] > 0:
-                with TableGeocodingLock(self._execute_query, table_name) as locked:
-                    if not locked:
-                        output['error'] = 'The table is already being geocoded'
-                        output['aborted'] = aborted = True
-                    else:
-                        schema = self._schema()
-                        sql, add_columns = geocoding_utils.geocode_query(
-                            table_name, schema, street, city, state, country, status)
-
-                        add_columns += [(geocoding_constants.HASH_COLUMN, 'text')]
-
-                        log.debug("Adding columns %s if needed", ', '.join([c[0] for c in add_columns]))
-                        alter_sql = "ALTER TABLE {table} {add_columns};".format(
-                            table=table_name,
-                            add_columns=','.join([
-                                'ADD COLUMN IF NOT EXISTS {} {}'.format(name, type) for name, type in add_columns]))
-                        self._execute_query(alter_sql)
-
-                        log.debug("Executing query: %s", sql)
-                        result = None
-                        try:
-                            result = self._execute_long_running_query(sql)
-                        except Exception as err:
-                            log.error(err)
-                            msg = str(err)
-                            output['error'] = msg
-                            # FIXME: Python SDK should return proper exceptions
-                            # see: https://github.com/CartoDB/cartoframes/issues/751
-                            match = re.search(
-                                r'Remaining quota:\s+(\d+)\.\s+Estimated cost:\s+(\d+)',
-                                msg, re.MULTILINE | re.IGNORECASE
-                            )
-                            if match:
-                                output['remaining_quota'] = int(match.group(1))
-                                output['estimated_cost'] = int(match.group(2))
-                            aborted = True
-                            # Don't rollback to avoid losing any partial geocodification:
-                            # TODO
-                            # transaction.commit()
-
-                        if result and not aborted:
-                            # Number of updated rows not available for batch queries
-                            # output['updated_rows'] = result.rowcount
-                            # log.debug('Number of rows updated: %d', output['updated_rows'])
-                            pass
-
-                if not aborted:
-                    sql = geocoding_utils.posterior_summary_query(table_name)
-                    log.debug("Executing result summary query: %s", sql)
-                    result = self._execute_query(sql)
-                    geocoding_utils.set_post_summary_info(summary, result, output)
+        if output['required_quota'] > 0 and not dry_run:
+            with TableGeocodingLock(self._execute_query, table_name) as locked:
+                if not locked:
+                    output['error'] = 'The table is already being geocoded'
+                    output['aborted'] = aborted = True
+                else:
+                    sql, add_columns = geocoding_utils.geocode_query(table_name, street, city, state, country, status)
+
+                    add_columns += [(geocoding_constants.HASH_COLUMN, 'text')]
+
+                    log.debug("Adding columns %s if needed", ', '.join([c[0] for c in add_columns]))
+                    alter_sql = "ALTER TABLE {table} {add_columns};".format(
+                        table=table_name,
+                        add_columns=','.join([
+                            'ADD COLUMN IF NOT EXISTS {} {}'.format(name, type) for name, type in add_columns]))
+                    self._execute_query(alter_sql)
+
+                    log.debug("Executing query: %s", sql)
+                    result = None
+                    try:
+                        result = self._execute_long_running_query(sql)
+                    except Exception as err:
+                        log.error(err)
+                        msg = str(err)
+                        output['error'] = msg
+                        # FIXME: Python SDK should return proper exceptions
+                        # see: https://github.com/CartoDB/cartoframes/issues/751
+                        match = re.search(
+                            r'Remaining quota:\s+(\d+)\.\s+Estimated cost:\s+(\d+)',
+                            msg, re.MULTILINE | re.IGNORECASE
+                        )
+                        if match:
+                            output['remaining_quota'] = int(match.group(1))
+                            output['estimated_cost'] = int(match.group(2))
+                        aborted = True
+                        # Don't rollback to avoid losing any partial geocodification:
+                        # TODO
+                        # transaction.commit()
+
+                    if result and not aborted:
+                        # Number of updated rows not available for batch queries
+                        # output['updated_rows'] = result.rowcount
+                        # log.debug('Number of rows updated: %d', output['updated_rows'])
+                        pass
+
+            if not aborted:
+                sql = geocoding_utils.posterior_summary_query(table_name)
+                log.debug("Executing result summary query: %s", sql)
+                result = self._execute_query(sql)
+                geocoding_utils.set_post_summary_info(summary, result, output)
 
         if not aborted:
             # TODO
diff --git a/cartoframes/data/services/isolines.py b/cartoframes/data/services/isolines.py
index ef376677..b0177f69 100644
--- a/cartoframes/data/services/isolines.py
+++ b/cartoframes/data/services/isolines.py
@@ -1,5 +1,3 @@
-from carto.exceptions import CartoException
-
 from .service import Service
 from ...core.logger import log
 from ...core.managers.source_manager import SourceManager
@@ -141,13 +139,6 @@ class Isolines(Service):
 
         if dry_run:
             return self.result(data=None, metadata=metadata)
-        else:
-            available_quota = self.available_quota()
-            if metadata['required_quota'] > available_quota:
-                raise CartoException('Your CARTO account does not have enough Isolines quota: {}/{}'.format(
-                    metadata['required_quota'],
-                    available_quota
-                ))
 
         if source_manager.is_remote():
             temporary_table_name = False
@@ -196,14 +187,13 @@ class Isolines(Service):
 
         if exclusive:
             # Add range label column
-            if len(cdf) > 0:
-                cdf[RANGE_LABEL_KEY] = cdf.apply(lambda r: '%.0f min.' % (r[DATA_RANGE_KEY]/60), axis=1)
+            cdf[RANGE_LABEL_KEY] = cdf.apply(lambda r: '%.0f min.' % (r[DATA_RANGE_KEY]/60), axis=1)
 
         if table_name:
             # save result in a table
             to_carto(cdf, table_name, self._credentials, if_exists, log_enabled=dry_run)
 
-        if source_manager.is_dataframe() and CARTO_INDEX_KEY in cdf:
+        if source_manager.is_dataframe():
             del cdf[CARTO_INDEX_KEY]
 
         if temporary_table_name:
diff --git a/cartoframes/data/services/service.py b/cartoframes/data/services/service.py
index ac51c2d2..b3bc7afb 100644
--- a/cartoframes/data/services/service.py
+++ b/cartoframes/data/services/service.py
@@ -40,9 +40,6 @@ class Service(object):
     def result(self, data, metadata=None):
         return Result(data=data, metadata=metadata)
 
-    def _schema(self):
-        return self._context_manager.get_schema()
-
     def _new_temporary_table_name(self, base=None):
         return (base or 'table') + '_' + uuid.uuid4().hex[:10]
 
diff --git a/cartoframes/data/services/utils/geocoding_utils.py b/cartoframes/data/services/utils/geocoding_utils.py
index d73ef157..386149b2 100644
--- a/cartoframes/data/services/utils/geocoding_utils.py
+++ b/cartoframes/data/services/utils/geocoding_utils.py
@@ -129,13 +129,12 @@ def posterior_summary_query(table):
     )
 
 
-def geocode_query(table, schema, street, city, state, country, status):
+def geocode_query(table, street, city, state, country, status):
     hash_expression = hash_expr(street, city, state, country)
     query = """
-        SELECT * FROM "{schema}"."{table}" WHERE {needs_geocoding}
+        SELECT * FROM {table} WHERE {needs_geocoding}
     """.format(
         table=table,
-        schema=schema,
         needs_geocoding=needs_geocoding_expr(hash_expression)
     )
     geocode_expression = """
@@ -159,16 +158,15 @@ def geocode_query(table, schema, street, city, state, country, status):
     status_assignment, status_columns = status_assignment_columns(status)
 
     query = """
-        UPDATE "{schema}"."{table}"
+        UPDATE {table}
         SET
             the_geom = _g.the_geom,
             {status_assignment}
             {hash_column} = {hash_expression}
         FROM (SELECT * FROM {geocode_expression}) _g
-        WHERE _g.cartodb_id = "{schema}"."{table}".cartodb_id
+        WHERE _g.cartodb_id = {table}.cartodb_id
     """.format(
         table=table,
-        schema=schema,
         hash_column=geocoding_constants.HASH_COLUMN,
         hash_expression=hash_expression,
         geocode_expression=geocode_expression,
diff --git a/cartoframes/utils/utils.py b/cartoframes/utils/utils.py
index 3f34b3f9..ede7be66 100644
--- a/cartoframes/utils/utils.py
+++ b/cartoframes/utils/utils.py
@@ -15,6 +15,7 @@ from functools import wraps
 from warnings import catch_warnings, filterwarnings
 
 from ..auth.credentials import Credentials
+from ..auth import defaults
 from ..core.logger import log
 
 GEOM_TYPE_POINT = 'point'
@@ -301,6 +302,12 @@ def is_table_name(data):
     return isinstance(data, str) and normalize_name(data) == data
 
 
+def get_credentials(credentials=None):
+    _credentials = credentials or defaults.get_default_credentials()
+    check_credentials(_credentials)
+    return _credentials
+
+
 def check_credentials(credentials):
     if not isinstance(credentials, Credentials):
         raise AttributeError('Credentials attribute is required. '


Test patch:
diff --git a/tests/unit/data/observatory/catalog/test_catalog.py b/tests/unit/data/observatory/catalog/test_catalog.py
index c2662ca9..1e3c1939 100644
--- a/tests/unit/data/observatory/catalog/test_catalog.py
+++ b/tests/unit/data/observatory/catalog/test_catalog.py
@@ -194,8 +194,10 @@ class TestCatalog(object):
         catalog = Catalog()
 
         # When
-        with pytest.raises(ValueError) as e:
+        with pytest.raises(AttributeError) as e:
             catalog.subscriptions(wrong_credentials)
 
         # Then
-        assert str(e.value) == '`credentials` must be a Credentials class instance'
+        assert str(e.value) == ('Credentials attribute is required. '
+                                'Please pass a `Credentials` instance '
+                                'or use the `set_default_credentials` function.')
diff --git a/tests/unit/data/observatory/catalog/test_dataset.py b/tests/unit/data/observatory/catalog/test_dataset.py
index 2786f59a..a4bf918d 100644
--- a/tests/unit/data/observatory/catalog/test_dataset.py
+++ b/tests/unit/data/observatory/catalog/test_dataset.py
@@ -461,11 +461,13 @@ class TestDataset(object):
         dataset = Dataset(db_dataset1)
 
         # When
-        with pytest.raises(ValueError) as e:
+        with pytest.raises(AttributeError) as e:
             dataset.subscribe(wrong_credentials)
 
         # Then
-        assert str(e.value) == '`credentials` must be a Credentials class instance'
+        assert str(e.value) == ('Credentials attribute is required. '
+                                'Please pass a `Credentials` instance '
+                                'or use the `set_default_credentials` function.')
 
     @patch('cartoframes.data.observatory.catalog.subscription_info.fetch_subscription_info')
     def test_dataset_subscription_info(self, mock_fetch):
@@ -512,11 +514,13 @@ class TestDataset(object):
         dataset = Dataset(db_dataset1)
 
         # When
-        with pytest.raises(ValueError) as e:
+        with pytest.raises(AttributeError) as e:
             dataset.subscription_info(wrong_credentials)
 
         # Then
-        assert str(e.value) == '`credentials` must be a Credentials class instance'
+        assert str(e.value) == ('Credentials attribute is required. '
+                                'Please pass a `Credentials` instance '
+                                'or use the `set_default_credentials` function.')
 
     def test_dataset_is_available_in(self):
         dataset_in_bq = Dataset(db_dataset1)
diff --git a/tests/unit/data/observatory/catalog/test_geography.py b/tests/unit/data/observatory/catalog/test_geography.py
index bb6f0991..1dcb0803 100644
--- a/tests/unit/data/observatory/catalog/test_geography.py
+++ b/tests/unit/data/observatory/catalog/test_geography.py
@@ -143,7 +143,7 @@ class TestGeography(object):
         geography_str = str(geography)
 
         # Then
-        assert geography_str == 'Geography({dict_str})'.format(dict_str=str(db_geography1))
+        assert geography_str == "<Geography.get('{id}')>".format(id=db_geography1['slug'])
 
     @patch.object(GeographyRepository, 'get_all')
     def test_get_all_geographies(self, mocked_repo):
@@ -384,11 +384,13 @@ class TestGeography(object):
         geography = Geography(db_geography1)
 
         # When
-        with pytest.raises(ValueError) as e:
+        with pytest.raises(AttributeError) as e:
             geography.subscribe(wrong_credentials)
 
         # Then
-        assert str(e.value) == '`credentials` must be a Credentials class instance'
+        assert str(e.value) == ('Credentials attribute is required. '
+                                'Please pass a `Credentials` instance '
+                                'or use the `set_default_credentials` function.')
 
     @patch('cartoframes.data.observatory.catalog.subscription_info.fetch_subscription_info')
     def test_geography_subscription_info(self, mock_fetch):
@@ -435,11 +437,13 @@ class TestGeography(object):
         geography = Geography(db_geography1)
 
         # When
-        with pytest.raises(ValueError) as e:
+        with pytest.raises(AttributeError) as e:
             geography.subscription_info(wrong_credentials)
 
         # Then
-        assert str(e.value) == '`credentials` must be a Credentials class instance'
+        assert str(e.value) == ('Credentials attribute is required. '
+                                'Please pass a `Credentials` instance '
+                                'or use the `set_default_credentials` function.')
 
     def test_geography_is_available_in(self):
         geography_in_bq = Geography(db_geography1)