Repository: CartoDB/cartoframes
Pull request #1635 (2020-05-11 11:02:55)

Add windows section and how to create a conda environment in the installation guide


Patch:
diff --git a/cartoframes/data/clients/auth_api_client.py b/cartoframes/data/clients/auth_api_client.py
index 0c563ebf..21cca196 100644
--- a/cartoframes/data/clients/auth_api_client.py
+++ b/cartoframes/data/clients/auth_api_client.py
@@ -41,7 +41,7 @@ class AuthAPIClient:
             else:
                 raise e
 
-        return api_key.name, api_key.token, tables_names
+        return api_key.token, tables_names
 
 
 def _get_table_dict(schema, name, permissions):
diff --git a/cartoframes/io/carto.py b/cartoframes/io/carto.py
index 0c9a62e1..f0789254 100644
--- a/cartoframes/io/carto.py
+++ b/cartoframes/io/carto.py
@@ -13,6 +13,7 @@ from ..utils.metrics import send_metrics
 
 
 GEOM_COLUMN_NAME = 'the_geom'
+
 IF_EXISTS_OPTIONS = ['fail', 'replace', 'append']
 
 
diff --git a/cartoframes/io/managers/context_manager.py b/cartoframes/io/managers/context_manager.py
index 5272661d..67b28679 100644
--- a/cartoframes/io/managers/context_manager.py
+++ b/cartoframes/io/managers/context_manager.py
@@ -13,7 +13,7 @@ from ...auth.defaults import get_default_credentials
 from ...utils.logger import log
 from ...utils.geom_utils import encode_geometry_ewkb
 from ...utils.utils import is_sql_query, check_credentials, encode_row, map_geom_type, PG_NULL
-from ...utils.columns import get_dataframe_columns_info, get_query_columns_info, obtain_converters, \
+from ...utils.columns import Column, get_dataframe_columns_info, obtain_converters, \
                       date_columns_names, normalize_name
 
 DEFAULT_RETRY_TIMES = 3
@@ -47,21 +47,16 @@ class ContextManager:
         table_name = self.normalize_table_name(table_name)
         columns = get_dataframe_columns_info(gdf)
 
-        if self.has_table(table_name, schema):
-            if if_exists == 'replace':
-                if self._compare_columns(table_name, schema, columns):
-                    self._truncate_table_from_columns(table_name, schema, columns, cartodbfy)
-                else:
-                    self._drop_create_table_from_columns(table_name, schema, columns, cartodbfy)
-            elif if_exists == 'fail':
-                raise Exception('Table "{schema}.{table_name}" already exists in your CARTO account. '
-                                'Please choose a different `table_name` or use '
-                                'if_exists="replace" to overwrite it.'.format(
-                                    table_name=table_name, schema=schema))
-            else:  # 'append'
-                pass
-        else:
-            self._drop_create_table_from_columns(table_name, schema, columns, cartodbfy)
+        if if_exists == 'replace' or not self.has_table(table_name, schema):
+            log.debug('Creating table "{}"'.format(table_name))
+            self._create_table_from_columns(table_name, columns, schema, cartodbfy)
+        elif if_exists == 'fail':
+            raise Exception('Table "{schema}.{table_name}" already exists in your CARTO account. '
+                            'Please choose a different `table_name` or use '
+                            'if_exists="replace" to overwrite it.'.format(
+                                table_name=table_name, schema=schema))
+        else:  # 'append'
+            pass
 
         self._copy_from(gdf, table_name, columns)
         return table_name
@@ -70,18 +65,16 @@ class ContextManager:
         schema = self.get_schema()
         table_name = self.normalize_table_name(table_name)
 
-        if self.has_table(table_name, schema):
-            if if_exists == 'replace':
-                self._drop_create_table_from_query(table_name, schema, query, cartodbfy)
-            elif if_exists == 'fail':
-                raise Exception('Table "{schema}.{table_name}" already exists in your CARTO account. '
-                                'Please choose a different `table_name` or use '
-                                'if_exists="replace" to overwrite it.'.format(
-                                    table_name=table_name, schema=schema))
-            else:  # 'append'
-                pass
-        else:
-            self._drop_create_table_from_query(table_name, schema, query, cartodbfy)
+        if if_exists == 'replace' or not self.has_table(table_name, schema):
+            log.debug('Creating table "{}"'.format(table_name))
+            self._create_table_from_query(query, table_name, schema, cartodbfy)
+        elif if_exists == 'fail':
+            raise Exception('Table "{schema}.{table_name}" already exists in your CARTO account. '
+                            'Please choose a different `table_name` or use '
+                            'if_exists="replace" to overwrite it.'.format(
+                                table_name=table_name, schema=schema))
+        else:  # 'append'
+            pass
 
         return table_name
 
@@ -194,19 +187,7 @@ class ContextManager:
             tables = [table.split('.')[1] if '.' in table else table for table in result['rows'][0]['tables']]
         return tables
 
-    def _compare_columns(self, table_name, schema, columns):
-        GEOM_COL = 'the_geom_webmercator'
-
-        query = self._compute_query_from_table(table_name, schema)
-        existing_columns = [c for c in self._get_query_columns_info(query) if (c.name != GEOM_COL)]
-
-        existing_columns.sort()
-        columns.sort()
-
-        return existing_columns == columns
-
-    def _drop_create_table_from_query(self, table_name, schema, query, cartodbfy):
-        log.debug('DROP + CREATE table "{}"'.format(table_name))
+    def _create_table_from_query(self, query, table_name, schema, cartodbfy=True):
         query = 'BEGIN; {drop}; {create}; {cartodbfy}; COMMIT;'.format(
             drop=_drop_table_query(table_name),
             create=_create_table_from_query_query(table_name, query),
@@ -214,8 +195,7 @@ class ContextManager:
         )
         self.execute_long_running_query(query)
 
-    def _drop_create_table_from_columns(self, table_name, schema, columns, cartodbfy):
-        log.debug('DROP + CREATE table "{}"'.format(table_name))
+    def _create_table_from_columns(self, table_name, columns, schema, cartodbfy=True):
         query = 'BEGIN; {drop}; {create}; {cartodbfy}; COMMIT;'.format(
             drop=_drop_table_query(table_name),
             create=_create_table_from_columns_query(table_name, columns),
@@ -223,14 +203,6 @@ class ContextManager:
         )
         self.execute_long_running_query(query)
 
-    def _truncate_table_from_columns(self, table_name, schema, columns, cartodbfy):
-        log.debug('TRUNCATE table "{}"'.format(table_name))
-        query = 'BEGIN; {truncate}; {cartodbfy}; COMMIT;'.format(
-            truncate=_truncate_table_query(table_name),
-            cartodbfy=_cartodbfy_query(table_name, schema) if cartodbfy else ''
-        )
-        self.execute_long_running_query(query)
-
     def compute_query(self, source, schema=None):
         if is_sql_query(source):
             return source
@@ -254,7 +226,7 @@ class ContextManager:
     def _get_query_columns_info(self, query):
         query = 'SELECT * FROM ({}) _q LIMIT 0'.format(query)
         table_info = self.execute_query(query)
-        return get_query_columns_info(table_info['fields'])
+        return Column.from_sql_api_fields(table_info['fields'])
 
     def _get_copy_query(self, query, columns, limit):
         query_columns = [
@@ -273,7 +245,6 @@ class ContextManager:
         return query
 
     def _copy_to(self, query, columns, retry_times):
-        log.debug('COPY TO')
         copy_query = 'COPY ({0}) TO stdout WITH (FORMAT csv, HEADER true, NULL \'{1}\')'.format(query, PG_NULL)
 
         try:
@@ -301,7 +272,6 @@ class ContextManager:
         return df
 
     def _copy_from(self, dataframe, table_name, columns):
-        log.debug('COPY FROM')
         query = """
             COPY {table_name}({columns}) FROM stdin WITH (FORMAT csv, DELIMITER '|', NULL '{null}');
         """.format(
@@ -327,11 +297,6 @@ def _drop_table_query(table_name, if_exists=True):
         if_exists='IF EXISTS' if if_exists else '')
 
 
-def _truncate_table_query(table_name):
-    return '''TRUNCATE TABLE {table_name}'''.format(
-        table_name=table_name)
-
-
 def _create_table_from_columns_query(table_name, columns):
     columns = ['{name} {type}'.format(name=column.dbname, type=column.dbtype) for column in columns]
 
diff --git a/cartoframes/utils/columns.py b/cartoframes/utils/columns.py
index 6d6df518..f20429e7 100644
--- a/cartoframes/utils/columns.py
+++ b/cartoframes/utils/columns.py
@@ -3,108 +3,140 @@
 import re
 
 from unidecode import unidecode
+from collections import namedtuple
 
 from .utils import dtypes2pg, pg2dtypes, PG_NULL
-
-BOOL_DBTYPES = ['bool', 'boolean']
-OBJECT_DBTYPES = ['text']
-INT_DBTYPES = ['int2', 'int4', 'int2', 'int', 'int8', 'smallint', 'integer', 'bigint']
-FLOAT_DBTYPES = ['float4', 'float8', 'real', 'double precision', 'numeric', 'decimal']
-DATETIME_DBTYPES = ['date', 'timestamp', 'timestampz']
-FORBIDDEN_COLUMN_NAMES = ['the_geom_webmercator']
-MAX_LENGTH = 63
-MAX_COLLISION_LENGTH = MAX_LENGTH - 4
-RESERVED_WORDS = ('ALL', 'ANALYSE', 'ANALYZE', 'AND', 'ANY', 'ARRAY', 'AS', 'ASC', 'ASYMMETRIC', 'AUTHORIZATION',
-                  'BETWEEN', 'BINARY', 'BOTH', 'CASE', 'CAST', 'CHECK', 'COLLATE', 'COLUMN', 'CONSTRAINT',
-                  'CREATE', 'CROSS', 'CURRENT_DATE', 'CURRENT_ROLE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
-                  'CURRENT_USER', 'DEFAULT', 'DEFERRABLE', 'DESC', 'DISTINCT', 'DO', 'ELSE', 'END', 'EXCEPT',
-                  'FALSE', 'FOR', 'FOREIGN', 'FREEZE', 'FROM', 'FULL', 'GRANT', 'GROUP', 'HAVING', 'ILIKE', 'IN',
-                  'INITIALLY', 'INNER', 'INTERSECT', 'INTO', 'IS', 'ISNULL', 'JOIN', 'LEADING', 'LEFT', 'LIKE',
-                  'LIMIT', 'LOCALTIME', 'LOCALTIMESTAMP', 'NATURAL', 'NEW', 'NOT', 'NOTNULL', 'NULL', 'OFF',
-                  'OFFSET', 'OLD', 'ON', 'ONLY', 'OR', 'ORDER', 'OUTER', 'OVERLAPS', 'PLACING', 'PRIMARY',
-                  'REFERENCES', 'RIGHT', 'SELECT', 'SESSION_USER', 'SIMILAR', 'SOME', 'SYMMETRIC', 'TABLE', 'THEN',
-                  'TO', 'TRAILING', 'TRUE', 'UNION', 'UNIQUE', 'USER', 'USING', 'VERBOSE', 'WHEN', 'WHERE',
-                  'XMIN', 'XMAX', 'FORMAT', 'CONTROLLER', 'ACTION')
-
-
-class ColumnInfo:
-
-    def __init__(self, name, dbname, dbtype, is_geom):
-        self.name = name
-        self.dbname = dbname
-        self.dbtype = dbtype
-        self.is_geom = is_geom
-
-    def __repr__(self):
-        params = ', '.join([self.name, self.dbname, self.dbtype, str(self.is_geom)])
-        return 'ColumnInfo({})'.format(params)
-
-    def __eq__(self, other):
-        if self.name == 'cartodb_id':
-            # Skip cartodb_id comparison because cartodbfy converts bigint to integer
-            return True
-        else:
-            return self.name == other.name and \
-                   self.dbname == other.dbname and \
-                   self.dbtype == other.dbtype and \
-                   self.is_geom == other.is_geom
-
-    def __gt__(self, other):
-        return self.name > other.name
-
-    def __ge__(self, other):
-        return self.name >= other.name
-
-    def __lt__(self, other):
-        return self.name < other.name
-
-    def __le__(self, other):
-        return self.name <= other.name
+from .geom_utils import decode_geometry_item, detect_encoding_type
+
+
+class Column(object):
+    BOOL_DTYPE = 'bool'
+    OBJECT_DTYPE = 'object'
+    INT_DTYPES = ['int16', 'int32', 'int64']
+    FLOAT_DTYPES = ['float32', 'float64']
+    DATETIME_DTYPES = ['datetime64[D]', 'datetime64[ns]', 'datetime64[ns, UTC]']
+    INDEX_COLUMN_NAME = 'cartodb_id'
+    FORBIDDEN_COLUMN_NAMES = ['the_geom_webmercator']
+    MAX_LENGTH = 63
+    MAX_COLLISION_LENGTH = MAX_LENGTH - 4
+    RESERVED_WORDS = ('ALL', 'ANALYSE', 'ANALYZE', 'AND', 'ANY', 'ARRAY', 'AS', 'ASC', 'ASYMMETRIC', 'AUTHORIZATION',
+                      'BETWEEN', 'BINARY', 'BOTH', 'CASE', 'CAST', 'CHECK', 'COLLATE', 'COLUMN', 'CONSTRAINT',
+                      'CREATE', 'CROSS', 'CURRENT_DATE', 'CURRENT_ROLE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
+                      'CURRENT_USER', 'DEFAULT', 'DEFERRABLE', 'DESC', 'DISTINCT', 'DO', 'ELSE', 'END', 'EXCEPT',
+                      'FALSE', 'FOR', 'FOREIGN', 'FREEZE', 'FROM', 'FULL', 'GRANT', 'GROUP', 'HAVING', 'ILIKE', 'IN',
+                      'INITIALLY', 'INNER', 'INTERSECT', 'INTO', 'IS', 'ISNULL', 'JOIN', 'LEADING', 'LEFT', 'LIKE',
+                      'LIMIT', 'LOCALTIME', 'LOCALTIMESTAMP', 'NATURAL', 'NEW', 'NOT', 'NOTNULL', 'NULL', 'OFF',
+                      'OFFSET', 'OLD', 'ON', 'ONLY', 'OR', 'ORDER', 'OUTER', 'OVERLAPS', 'PLACING', 'PRIMARY',
+                      'REFERENCES', 'RIGHT', 'SELECT', 'SESSION_USER', 'SIMILAR', 'SOME', 'SYMMETRIC', 'TABLE', 'THEN',
+                      'TO', 'TRAILING', 'TRUE', 'UNION', 'UNIQUE', 'USER', 'USING', 'VERBOSE', 'WHEN', 'WHERE',
+                      'XMIN', 'XMAX', 'FORMAT', 'CONTROLLER', 'ACTION', )
+    NORMALIZED_GEOM_COL_NAME = 'the_geom'
+
+    @staticmethod
+    def from_sql_api_fields(fields):
+        return [Column(column, normalize=False, pgtype=_extract_pgtype(fields[column])) for column in fields]
+
+    def __init__(self, name, normalize=True, pgtype=None):
+        if not name:
+            raise ValueError('Column name cannot be null or empty')
+
+        self.name = str(name)
+        self.pgtype = pgtype
+        self.dtype = pg2dtypes(pgtype)
+        if normalize:
+            self.normalize()
+
+    def normalize(self, forbidden_column_names=None):
+        self._sanitize()
+        self.name = self._truncate()
+
+        if forbidden_column_names:
+            i = 1
+            while self.name in forbidden_column_names:
+                self.name = '{}_{}'.format(self._truncate(length=Column.MAX_COLLISION_LENGTH), str(i))
+                i += 1
+
+        return self
+
+    def _sanitize(self):
+        self.name = self._slugify(self.name)
+
+        if self._is_reserved() or self._is_unsupported():
+            self.name = '_{}'.format(self.name)
+
+    def _is_reserved(self):
+        return self.name.upper() in Column.RESERVED_WORDS
+
+    def _is_unsupported(self):
+        return not re.match(r'^[a-z_]+[a-z_0-9]*$', self.name)
+
+    def _truncate(self, length=MAX_LENGTH):
+        return self.name[:length]
+
+    def _slugify(self, value):
+        value = unidecode(str(value).lower())
+
+        value = re.sub(r'<[^>]+>', '', value)
+        value = re.sub(r'&.+?;', '-', value)
+        value = re.sub(r'[^a-z0-9 _-]', '-', value).strip().lower()
+        value = re.sub(r'\s+', '-', value)
+        value = re.sub(r' ', '-', value)
+        value = re.sub(r'-+', '-', value)
+        value = re.sub(r'-', '_', value)
+
+        return value
+
+
+ColumnInfo = namedtuple('ColumnInfo', ['name', 'dbname', 'dbtype', 'is_geom'])
+
+
+def _extract_pgtype(fields):
+    if 'pgtype' in fields:
+        return fields['pgtype']
+    return None
 
 
 def get_dataframe_columns_info(df):
     columns = []
+    geom_type = _get_geometry_type(df)
+    df_columns = [(name, df.dtypes[name]) for name in df.columns]
 
-    for name in df.columns:
+    for name, dtype in df_columns:
         if _is_valid_column(name):
-            dbtype = dtypes2pg(str(df.dtypes[name]))
-            columns.append(_create_column_info(name, dbtype))
+            columns.append(_compute_column_info(name, dtype, geom_type))
 
     return columns
 
 
-def get_query_columns_info(fields):
-    columns = []
+def _get_geom_col_name(df):
+    return getattr(df, '_geometry_column_name', None)
 
-    for name in fields:
-        field = fields[name]
-        pgtype = field.get('pgtype')
-        dbtype = dtypes2pg(pg2dtypes(pgtype)) if pgtype else field.get('type')
-        columns.append(_create_column_info(name, dbtype))
 
-    return columns
+def _get_geometry_type(df):
+    geom_column = _get_geom_col_name(df)
+    if geom_column in df:
+        first_geom = _first_value(df[geom_column])
+        if first_geom:
+            enc_type = detect_encoding_type(first_geom)
+            return decode_geometry_item(first_geom, enc_type).geom_type
 
 
 def _is_valid_column(name):
-    return name.lower() not in FORBIDDEN_COLUMN_NAMES
+    return name.lower() not in Column.FORBIDDEN_COLUMN_NAMES
 
 
-def _create_column_info(name, dbtype=None):
-    is_geom = False
+def _compute_column_info(name, dtype=None, geom_type=None):
+    name = name
     dbname = normalize_name(name)
-    if dbtype == 'geometry':
-        dbtype = 'geometry(Geometry, 4326)'
+    if str(dtype) == 'geometry':
+        dbtype = 'geometry({}, 4326)'.format(geom_type or 'Point')
         is_geom = True
+    else:
+        dbtype = dtypes2pg(dtype)
+        is_geom = False
     return ColumnInfo(name, dbname, dbtype, is_geom)
 
 
-def normalize_name(column_name):
-    if column_name is None:
-        return None
-
-    return normalize_names([column_name])[0]
-
-
 def normalize_names(column_names):
     """Given an arbitrary column name, translate to a SQL-normalized column
         name a la CARTO's Import API will translate to
@@ -124,9 +156,9 @@ def normalize_names(column_names):
             * 'SELECT' -> '_select',
             * 'à' -> 'a',
             * 'longcolumnshouldbesplittedsomehowanditellyouwhereitsgonnabesplittedrightnow' -> \
-              'longcolumnshouldbesplittedsomehowanditellyouwhereitsgonnabespli',
+             'longcolumnshouldbesplittedsomehowanditellyouwhereitsgonnabespli',
             * 'longcolumnshouldbesplittedsomehowanditellyouwhereitsgonnabesplittedrightnow' -> \
-              'longcolumnshouldbesplittedsomehowanditellyouwhereitsgonnabe_1',
+             'longcolumnshouldbesplittedsomehowanditellyouwhereitsgonnabe_1',
             * 'all' -> '_all'
 
         Args:
@@ -135,77 +167,56 @@ def normalize_names(column_names):
             list: List of SQL-normalized column names
     """
     result = []
-
     for column_name in column_names:
-        result.append(_normalize(column_name, forbidden_column_names=result))
+        column = Column(column_name).normalize(forbidden_column_names=result)
+        result.append(column.name)
 
     return result
 
 
-def _normalize(column_name, forbidden_column_names=None):
-    column_name = _truncate(_sanitize(_slugify(column_name)))
-
-    if forbidden_column_names:
-        i = 1
-        while column_name in forbidden_column_names:
-            column_name = '{}_{}'.format(_truncate(column_name, length=MAX_COLLISION_LENGTH), i)
-            i += 1
-
-    return column_name
-
-
-def _slugify(value):
-    value = unidecode(str(value).lower())
-    value = re.sub(r'<[^>]+>', '', value)
-    value = re.sub(r'&.+?;', '-', value)
-    value = re.sub(r'[^a-z0-9 _-]', '-', value).strip().lower()
-    value = re.sub(r'\s+', '-', value)
-    value = re.sub(r' ', '-', value)
-    value = re.sub(r'-+', '-', value)
-    value = re.sub(r'-', '_', value)
-    return value
-
-
-def _sanitize(value):
-    return '_{}'.format(value) if _is_reserved(value) or _is_unsupported(value) else value
-
-
-def _truncate(value, length=MAX_LENGTH):
-    return value[:length]
-
-
-def _is_reserved(value):
-    return value.upper() in RESERVED_WORDS
-
+def normalize_name(column_name):
+    if column_name is None:
+        return None
 
-def _is_unsupported(value):
-    return not re.match(r'^[a-z_]+[a-z_0-9]*$', value)
+    return normalize_names([column_name])[0]
 
 
 def obtain_converters(columns):
     converters = {}
 
-    for int_column_name in type_columns_names(columns, INT_DBTYPES):
+    for int_column_name in int_columns_names(columns):
         converters[int_column_name] = _convert_int
 
-    for float_column_name in type_columns_names(columns, FLOAT_DBTYPES):
+    for float_column_name in float_columns_names(columns):
         converters[float_column_name] = _convert_float
 
-    for bool_column_name in type_columns_names(columns, BOOL_DBTYPES):
+    for bool_column_name in bool_columns_names(columns):
         converters[bool_column_name] = _convert_bool
 
-    for object_column_name in type_columns_names(columns, OBJECT_DBTYPES):
+    for object_column_name in object_columns_names(columns):
         converters[object_column_name] = _convert_object
 
     return converters
 
 
 def date_columns_names(columns):
-    return type_columns_names(columns, DATETIME_DBTYPES)
+    return [x.name for x in columns if x.dtype in Column.DATETIME_DTYPES]
+
+
+def int_columns_names(columns):
+    return [x.name for x in columns if x.dtype in Column.INT_DTYPES]
+
+
+def float_columns_names(columns):
+    return [x.name for x in columns if x.dtype in Column.FLOAT_DTYPES]
+
+
+def bool_columns_names(columns):
+    return [x.name for x in columns if x.dtype == Column.BOOL_DTYPE]
 
 
-def type_columns_names(columns, dbtypes):
-    return [x.name for x in columns if x.dbtype in dbtypes]
+def object_columns_names(columns):
+    return [x.name for x in columns if x.dtype == Column.OBJECT_DTYPE]
 
 
 def _convert_int(x):
diff --git a/cartoframes/utils/utils.py b/cartoframes/utils/utils.py
index 1506c500..b65188a7 100644
--- a/cartoframes/utils/utils.py
+++ b/cartoframes/utils/utils.py
@@ -121,7 +121,6 @@ def dtypes2pg(dtype):
         'bool': 'boolean',
         'datetime64[ns]': 'timestamp',
         'datetime64[ns, UTC]': 'timestamp',
-        'geometry': 'geometry'
     }
     return mapping.get(str(dtype), 'text')
 
diff --git a/cartoframes/viz/kuviz.py b/cartoframes/viz/kuviz.py
index ec627693..bd2c0bb9 100644
--- a/cartoframes/viz/kuviz.py
+++ b/cartoframes/viz/kuviz.py
@@ -78,14 +78,12 @@ class KuvizPublisher:
         private_sources = [layer.source for layer in layers if not layer.source.is_public()]
 
         if len(private_sources) > 0:
-            key_name, key_value, private_tables_names = self._auth_api_client.create_api_key(
-                private_sources, ['maps']
-            )
+            maps_api_key, private_tables_names = self._auth_api_client.create_api_key(private_sources, ['maps'])
             log.info(
                 'The map has been published. '
-                'The "{0}" Maps API key with value "{1}" is being used for these datasets {2}. '
-                'You can manage your API keys on your account.'.format(key_name, key_value, private_tables_names))
-            return key_value
+                'The "{0}" Maps API key is being used for these datasets {1}. '
+                'You can manage your API keys on your account'.format(maps_api_key, private_tables_names))
+            return maps_api_key
 
         return DEFAULT_PUBLIC
 
diff --git a/examples/publish_and_share/publish_visualization_gdf.ipynb b/examples/publish_and_share/publish_visualization_gdf.ipynb
index 0e896f46..2b5e382e 100644
--- a/examples/publish_and_share/publish_visualization_gdf.ipynb
+++ b/examples/publish_and_share/publish_visualization_gdf.ipynb
@@ -92,16 +92,6 @@
      "text": [
       "Success! Data uploaded to table \"table_name\" correctly\n"
      ]
-    },
-    {
-     "data": {
-      "text/plain": [
-       "'table_name'"
-      ]
-     },
-     "execution_count": 5,
-     "metadata": {},
-     "output_type": "execute_result"
     }
    ],
    "source": [
@@ -128,7 +118,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "The map has been published. The \"cartoframes_0a8a065b2cc026c5c33ee3dc269afcf1\" Maps API key with value \"YTAEiju-Utii8mgtERiEkA\" is being used for these datasets ['table_name']. You can manage your API keys on your account.\n"
+      "The map has been published. The \"YTAEiju-Utii8mgtERiEkA\" Maps API key is being used for these datasets ['table_name']. You can manage your API keys on your account\n"
      ]
     },
     {
diff --git a/examples/publish_and_share/publish_visualization_layout.ipynb b/examples/publish_and_share/publish_visualization_layout.ipynb
index 385d7490..aaf619e3 100644
--- a/examples/publish_and_share/publish_visualization_layout.ipynb
+++ b/examples/publish_and_share/publish_visualization_layout.ipynb
@@ -34,7 +34,7 @@
     "layout_viz = Layout([\n",
     "    Map(Layer('public_table')),\n",
     "    Map(Layer('private_table'))\n",
-    "])"
+    "], is_static=True)"
    ]
   },
   {
@@ -46,7 +46,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "The map has been published. The \"cartoframes_997c05771fd4e0916de49826722e51cd\" Maps API key with value \"7rj9ftFsOKUjSotnygh2jg\" is being used for these datasets ['private_table']. You can manage your API keys on your account.\n"
+      "The map has been published. The \"7rj9ftFsOKUjSotnygh2jg\" Maps API key is being used for these datasets ['private_table']. You can manage your API keys on your account\n"
      ]
     },
     {
diff --git a/examples/publish_and_share/publish_visualization_private_table.ipynb b/examples/publish_and_share/publish_visualization_private_table.ipynb
index 84c5ae9b..47097f00 100644
--- a/examples/publish_and_share/publish_visualization_private_table.ipynb
+++ b/examples/publish_and_share/publish_visualization_private_table.ipynb
@@ -44,7 +44,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "The map has been published. The \"cartoframes_997c05771fd4e0916de49826722e51cd\" Maps API key with value \"7rj9ftFsOKUjSotnygh2jg\" is being used for these datasets ['private_table']. You can manage your API keys on your account.\n"
+      "The map has been published. The \"7rj9ftFsOKUjSotnygh2jg\" Maps API key is being used for these datasets ['private_table']. You can manage your API keys on your account\n"
      ]
     },
     {
diff --git a/guides/01-Installation.ipynb b/guides/01-Installation.ipynb
index 216ab742..2db8b6eb 100644
--- a/guides/01-Installation.ipynb
+++ b/guides/01-Installation.ipynb
@@ -6,14 +6,40 @@
    "source": [
     "## Installation\n",
     "\n",
-    "This guide is intended for those who are using CARTOframes for the first time and provides the steps to install it **locally**, in a **Jupyter Notebook**, or using a **Virtual Environment**. You need **Python 3** to use CARTOframes."
+    "This guide is intended for those who are using CARTOframes for the first time. It covers:\n",
+    "- The **Python version** required\n",
+    "- How to install it using **pip**\n",
+    "- How to install it in a **Jupyter Notebook**\n",
+    "- How to install it in **Windows**\n",
+    "- How to create a **Virtual Enviroment**\n",
+    "- Considerations in **Jupyter Lab**\n",
+    "- Metrics\n",
+    "\n",
+    "> We recommend installing CARTOframes in a [Virtual Environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/), since they are very useful when working with Python locally.\n"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Install CARTOframes using `pip`\n",
+    "### Python version required\n",
+    "\n",
+    "CARTOframes needs Python 3 to work properly. You can check your version by running:\n",
+    "\n",
+    "```\n",
+    "python --version\n",
+    "```\n",
+    "\n",
+    "You should get some output like Python 3.6.3. If you do not have Python, please install the latest 3.x version from [python.org](https://python.org/) or refer to the [Installing Python](http://docs.python-guide.org/en/latest/starting/installation/) section of the Hitchhiker’s Guide to Python."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "heading_collapsed": true
+   },
+   "source": [
+    "### Install it using `pip`\n",
     "\n",
     "CARTOframes can be installed with [`pip`](https://pypi.org/project/pip/) by simply typing one of the following commands to do a system install:\n",
     "\n",
@@ -35,22 +61,37 @@
     "pip install cartoframes==1.x.y\n",
     "```\n",
     "\n",
-    "#### Optional dependencies\n",
+    "#### Optional dependency\n",
     "\n",
-    "- [RTree](https://github.com/Toblerity/rtree) is a spatial index package to improve performance and required for overlay operations. It provides an interface to [libspatialindex](https://github.com/libspatialindex/libspatialindex)."
+    "[RTree](https://github.com/Toblerity/rtree) is a spatial index package to improve performance and required for overlay operations. It provides an interface to [libspatialindex](https://github.com/libspatialindex/libspatialindex)."
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Use a Jupyter Notebook\n",
+    "### Install it in a Jupyter Notebook\n",
     "\n",
     "In the CARTOframes Developer Center, all of the examples are created with [Jupyter Notebooks](https://jupyter.org/). If you aren't familiar with Jupyter Notebooks, we recommend reading the [beginner documentation](https://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/what_is_jupyter.html) to get familiar with the environment.\n",
     "\n",
     "To install CARTOframes through a Jupyter Notebook, run this command:\n",
     "\n",
     "```bash\n",
+    "!pip install cartoframes\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Install it in Windows\n",
+    "\n",
+    "In order to work properly, CARTOframes needs that [Shapely](https://github.com/Toblerity/Shapely) and [Fiona](https://github.com/Toblerity/Fiona) libraries are already installed in your system. Execute these commands to install CARTOframes correctly:\n",
+    "\n",
+    "```bash\n",
+    "pip install shapely\n",
+    "conda install -c conda-forge fiona\n",
     "pip install cartoframes\n",
     "```"
    ]
@@ -61,7 +102,7 @@
    "source": [
     "### Use a Virtual Environment\n",
     "\n",
-    "We recommend installing CARTOframes in a [Virtual Environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/), since they are very useful when working with Python locally. This section provides the necessary information to create a simple Virtual Environment and install CARTOframes inside of it:\n",
+    "This section provides the necessary information to create a simple Virtual Environment with virtualenv and conda and also how to install CARTOframes inside of it.\n",
     "\n",
     "```bash\n",
     "$ virtualenv cartoframes_env\n",
@@ -73,7 +114,25 @@
     "\n",
     "```bash\n",
     "(cartoframes_env) $ deactivate\n",
-    "```\n"
+    "```\n",
+    "\n",
+    "#### Conda users\n",
+    "\n",
+    "Let's see how to achieve it with conda.\n",
+    "\n",
+    "```bash\n",
+    "$ conda create --name cartoframes_env\n",
+    "$ conda activate cartoframes_env\n",
+    "(cartoframes_env) $ pip install shapely\n",
+    "(cartoframes_env) $ conda install -c conda-forge fiona\n",
+    "(cartoframes_env) $ pip install cartoframes\n",
+    "```\n",
+    "\n",
+    "When the virtual environment is activated, it is visible in the command line prompt, in this case: `(cartoframes_env)`. It can be deactivated by typing `deactivate` to exit the virtualenv:\n",
+    "\n",
+    "```bash\n",
+    "(cartoframes_env) $ conda deactivate\n",
+    "```"
    ]
   },
   {


Test patch:
diff --git a/tests/unit/auth/test_auth_api_client.py b/tests/unit/auth/test_auth_api_client.py
index 3e245bcb..557ebac8 100644
--- a/tests/unit/auth/test_auth_api_client.py
+++ b/tests/unit/auth/test_auth_api_client.py
@@ -33,9 +33,8 @@ class TestAuthAPIClient(object):
         api_key_name = 'fake_name'
 
         auth_api_client = AuthAPIClient()
-        name, token, tables = auth_api_client.create_api_key([source], name=api_key_name)
+        token, tables = auth_api_client.create_api_key([source], name=api_key_name)
 
-        assert name == api_key_name
         assert token == TOKEN_MOCK
 
     def test_create_api_key_several_sources(self, mocker):
@@ -45,7 +44,6 @@ class TestAuthAPIClient(object):
         api_key_name = 'fake_name'
 
         auth_api_client = AuthAPIClient()
-        name, token, tables = auth_api_client.create_api_key([source, source, source], name=api_key_name)
+        token, tables = auth_api_client.create_api_key([source, source, source], name=api_key_name)
 
-        assert name == api_key_name
         assert token == TOKEN_MOCK
diff --git a/tests/unit/utils/managers/test_context_manager.py b/tests/unit/utils/managers/test_context_manager.py
index 477f19ec..159b2321 100644
--- a/tests/unit/utils/managers/test_context_manager.py
+++ b/tests/unit/utils/managers/test_context_manager.py
@@ -70,12 +70,12 @@ class TestContextManager(object):
                                 'Please choose a different `table_name` or use '
                                 'if_exists="replace" to overwrite it.')
 
-    def test_copy_from_exists_replace_drop_create(self, mocker):
+    def test_copy_from_exists_replace(self, mocker):
         # Given
         mocker.patch('cartoframes.io.managers.context_manager._create_auth_client')
         mocker.patch.object(ContextManager, 'has_table', return_value=True)
         mocker.patch.object(ContextManager, 'get_schema', return_value='schema')
-        mock = mocker.patch.object(ContextManager, '_drop_create_table_from_columns')
+        mock = mocker.patch.object(ContextManager, '_create_table_from_columns')
         df = DataFrame({'A': [1]})
         columns = [ColumnInfo('A', 'a', 'bigint', False)]
 
@@ -84,24 +84,7 @@ class TestContextManager(object):
         cm.copy_from(df, 'TABLE NAME', 'replace')
 
         # Then
-        mock.assert_called_once_with('table_name', 'schema', columns, True)
-
-    def test_copy_from_exists_replace_truncate(self, mocker):
-        # Given
-        mocker.patch('cartoframes.io.managers.context_manager._create_auth_client')
-        mocker.patch.object(ContextManager, 'has_table', return_value=True)
-        mocker.patch.object(ContextManager, 'get_schema', return_value='schema')
-        mocker.patch.object(ContextManager, '_compare_columns', return_value=True)
-        mock = mocker.patch.object(ContextManager, '_truncate_table_from_columns')
-        df = DataFrame({'A': [1]})
-        columns = [ColumnInfo('A', 'a', 'bigint', False)]
-
-        # When
-        cm = ContextManager(self.credentials)
-        cm.copy_from(df, 'TABLE NAME', 'replace')
-
-        # Then
-        mock.assert_called_once_with('table_name', 'schema', columns, True)
+        mock.assert_called_once_with('table_name', columns, 'schema', True)
 
     def test_internal_copy_from(self, mocker):
         # Given
diff --git a/tests/unit/utils/test_columns.py b/tests/unit/utils/test_columns.py
index 879fa719..183d3fba 100644
--- a/tests/unit/utils/test_columns.py
+++ b/tests/unit/utils/test_columns.py
@@ -6,7 +6,7 @@ from pandas import DataFrame
 from geopandas import GeoDataFrame
 
 from cartoframes.utils.geom_utils import set_geometry
-from cartoframes.utils.columns import ColumnInfo, get_dataframe_columns_info, normalize_names
+from cartoframes.utils.columns import Column, ColumnInfo, get_dataframe_columns_info, normalize_names
 
 
 class TestColumns(object):
@@ -54,6 +54,19 @@ class TestColumns(object):
                          'longcolumnshouldbesplittedsomehowanditellyouwhereitsgonnabe_1',
                          '_all']
 
+    def test_normalize(self):
+        other_cols = []
+        for c, a in zip(self.cols, self.cols_ans):
+            # changed cols should match answers
+            column = Column(c)
+            a_column = Column(a)
+            column.normalize(other_cols)
+            a_column.normalize(other_cols)
+            assert column.name == a
+            # already sql-normed cols should match themselves
+            assert a_column.name == a
+            other_cols.append(column.name)
+
     def test_normalize_names(self):
         assert normalize_names(self.cols) == self.cols_ans
 
@@ -71,7 +84,7 @@ class TestColumns(object):
         assert dataframe_columns_info == [
             ColumnInfo('Address', 'address', 'text', False),
             ColumnInfo('City', 'city', 'text', False),
-            ColumnInfo('the_geom', 'the_geom', 'geometry(Geometry, 4326)', True)
+            ColumnInfo('the_geom', 'the_geom', 'geometry(Point, 4326)', True)
         ]
 
     def test_column_info_without_geom(self):
@@ -97,7 +110,7 @@ class TestColumns(object):
 
         assert dataframe_columns_info == [
             ColumnInfo('cartodb_id', 'cartodb_id', 'bigint', False),
-            ColumnInfo('the_geom', 'the_geom', 'geometry(Geometry, 4326)', True)
+            ColumnInfo('the_geom', 'the_geom', 'geometry(Point, 4326)', True)
         ]
 
     def test_column_info_geometry_troubled_names(self):
@@ -110,6 +123,6 @@ class TestColumns(object):
 
         assert dataframe_columns_info == [
             ColumnInfo('Geom', 'geom', 'text', False),
-            ColumnInfo('the_geom', 'the_geom', 'geometry(Geometry, 4326)', True),
+            ColumnInfo('the_geom', 'the_geom', 'geometry(Point, 4326)', True),
             ColumnInfo('g-e-o-m-e-t-r-y', 'g_e_o_m_e_t_r_y', 'text', False)
         ]