Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create Maps API key automagically in Kuviz #1082

Merged
merged 42 commits into from
Oct 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3978545
publish use the same credentials
simon-contreras-deel Oct 9, 2019
68634e3
publish structure
simon-contreras-deel Oct 9, 2019
cc94622
instantiate KuvizPublisher with all info
simon-contreras-deel Oct 9, 2019
5165782
auth api client
simon-contreras-deel Oct 9, 2019
d218ce3
kuviz refactor
simon-contreras-deel Oct 9, 2019
8728491
easy publish
simon-contreras-deel Oct 9, 2019
acc1e94
kuviz easy get and set layers
simon-contreras-deel Oct 9, 2019
3cc0d76
updating kuviz delete and creation layers
simon-contreras-deel Oct 9, 2019
93f58de
update kuviz
simon-contreras-deel Oct 9, 2019
8075113
using new update
simon-contreras-deel Oct 9, 2019
2ac70dc
fix tables usage
simon-contreras-deel Oct 9, 2019
22db554
setting api key naming
simon-contreras-deel Oct 9, 2019
149875f
test auth api client
simon-contreras-deel Oct 10, 2019
25ff832
udating kuviz test
simon-contreras-deel Oct 10, 2019
3afa59a
table_name is optional
simon-contreras-deel Oct 10, 2019
dedf194
remove tests testing mocks
simon-contreras-deel Oct 10, 2019
a4e3f3e
test naming
simon-contreras-deel Oct 10, 2019
def0bc3
remove needless mocks
simon-contreras-deel Oct 10, 2019
c38d945
test publish method
simon-contreras-deel Oct 10, 2019
fcbb38e
test update and delete
simon-contreras-deel Oct 10, 2019
3676ff1
ensuring working with layer copy
simon-contreras-deel Oct 11, 2019
2f0d06a
unique name for api key
simon-contreras-deel Oct 11, 2019
01d228c
ensure table name
simon-contreras-deel Oct 11, 2019
7eeeb0e
saving and changing branch
simon-contreras-deel Oct 11, 2019
3837ce8
Merge branch 'develop' into feature/magic-apikey-kuviz
simon-contreras-deel Oct 11, 2019
96be37a
tests several geoms columns and geodataframes
simon-contreras-deel Oct 11, 2019
0bc118b
final notebook
simon-contreras-deel Oct 11, 2019
f264a8c
updating map test behaviour
simon-contreras-deel Oct 11, 2019
dbc9b69
updating doc
simon-contreras-deel Oct 11, 2019
e16663a
hound
simon-contreras-deel Oct 11, 2019
d477c00
removeing forgotten line
simon-contreras-deel Oct 11, 2019
4343a16
more hound happiness
simon-contreras-deel Oct 11, 2019
89619f4
updating guides
simon-contreras-deel Oct 11, 2019
d31e7f7
needless import
simon-contreras-deel Oct 13, 2019
98c087d
messaging in Kuviz publication and update
simon-contreras-deel Oct 13, 2019
a57fa18
improve message
simon-contreras-deel Oct 13, 2019
884ad08
Merge branch 'feature/magic-apikey-kuviz' of github.com:CartoDB/carto…
simon-contreras-deel Oct 13, 2019
82199f8
Improve message 2
simon-contreras-deel Oct 13, 2019
9e271b9
Merge branch 'feature/magic-apikey-kuviz' of github.com:CartoDB/carto…
simon-contreras-deel Oct 13, 2019
43adc4f
Improve message 3
simon-contreras-deel Oct 13, 2019
b42afd9
Merge branch 'develop' into feature/magic-apikey-kuviz
simon-contreras-deel Oct 14, 2019
aa7784b
Update docs/guides/kuviz.rst
alrocar Oct 14, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions cartoframes/data/clients/auth_api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import absolute_import

from carto.api_keys import APIKeyManager

from ...auth import get_default_credentials


class AuthAPIClient(object):
"""AuthAPIClient class is a client of the CARTO Auth API.
More info: https://carto.com/developers/auth-api/

Args:
credentials (:py:class:`Credentials <cartoframes.auth.Credentials>`, optional):
credentials of user account to send Dataset to. If not provided,
a default credentials (if set with :py:meth:`set_default_credentials
<cartoframes.auth.set_default_credentials>`) will attempted to be
used.
"""
def __init__(self, credentials=None):
credentials = credentials or get_default_credentials()
self._api_key_manager = _get_api_key_manager(credentials)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should raise an error if we are not receiving the master API key here, because otherwise it won't work the API key creation. It could be responsibility of carto-python.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering the API keys with the one provided by the user to check if it is the master one? This also applies to the Kuviz creation, the user should use the master one.

I am not sure about it. Maybe we can catch Access denied exceptions and remembering the user about using the master key?

Copy link
Contributor Author

@simon-contreras-deel simon-contreras-deel Oct 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, the validation of the credentials is something to work on because we are not validating if the credentials are defined or not (we are doing the classical credentials or get_default_credentials(), but not checking if we have credentials or not after that). Maybe we can create a task with both objectives

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def create_api_key(self, datasets, name, apis=['sql', 'maps'], permissions=['select']):
tables = []
for dataset in datasets:
table_names = dataset.get_table_names()
for table_name in table_names:
tables.append(_get_table_dict(dataset.schema, table_name, permissions))

api_key = self._api_key_manager.create(
name=name,
apis=apis,
tables=tables)

return api_key.token


def _get_table_dict(schema, table, permissions):
return {
"schema": schema,
"name": table,
"permissions": permissions
}


def _get_api_key_manager(credentials):
auth_client = credentials.get_api_key_auth_client()
return APIKeyManager(auth_client)
108 changes: 71 additions & 37 deletions cartoframes/viz/kuviz.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
from copy import deepcopy
from warnings import warn
import time

from carto.kuvizs import KuvizManager
from carto.exceptions import CartoException

from ..auth import get_default_credentials
from .source import Source
from ..utils.columns import normalize_name
from ..data.clients.auth_api_client import AuthAPIClient

from warnings import filterwarnings
filterwarnings("ignore", category=FutureWarning, module="carto")

DEFAULT_PUBLIC = 'default_public'


class KuvizPublisher(object):
def __init__(self, layers):
self._layers = deepcopy(layers)
def __init__(self, credentials=None):
self.kuviz = None
self._maps_api_key = DEFAULT_PUBLIC

self._credentials = credentials or get_default_credentials()
self._auth_client = _create_auth_client(self._credentials)
self._layers = []

@staticmethod
def all(credentials=None):
Expand All @@ -22,48 +32,72 @@ def all(credentials=None):
kuvizs = km.all()
return [kuviz_to_dict(kuviz) for kuviz in kuvizs]

def set_credentials(self, credentials=None):
self._credentials = credentials or get_default_credentials()
self._auth_client = _create_auth_client(self._credentials)

def publish(self, html, name, password=None):
return _create_kuviz(html=html, name=name, auth_client=self._auth_client, password=password)
def get_layers(self):
return self._layers

def is_sync(self):
return all(layer.source.dataset.is_remote() for layer in self._layers)
def set_layers(self, layers, name, table_name=None):
table_name = table_name or '{}_{}_table'.format(name, int(time.time() * 1000))

def is_public(self):
return all(layer.source.dataset.is_public() for layer in self._layers)
self._sync_layers(layers, table_name)
self._manage_maps_api_key(name)
self._add_layers_credentials()

def get_layers(self, maps_api_key='default_public'):
def publish(self, html, name, password=None):
self.kuviz = _create_kuviz(html=html, name=name, auth_client=self._auth_client, password=password)
return kuviz_to_dict(self.kuviz)

def update(self, data, name, password):
if not self.kuviz:
raise CartoException('The map has not been published yet. Use the `publish` method instead.')

self.kuviz.data = data
self.kuviz.name = name
self.kuviz.password = password
self.kuviz.save()

return kuviz_to_dict(self.kuviz)

def delete(self):
if self.kuviz:
self.kuviz.delete()
warn("Publication '{n}' ({id}) deleted".format(n=self.kuviz.name, id=self.kuviz.id))
self.kuviz = None
return True
return False

def _sync_layers(self, layers, table_name=None):
for idx, layer in enumerate(layers):
if layer.source.dataset.is_local():
table_name = normalize_name("{name}_{idx}".format(name=table_name, idx=idx))
layer = self._sync_layer(layer, table_name)
else:
layer = deepcopy(layer)
self._layers.append(layer)

def _sync_layer(self, layer, table_name):
layer.source.dataset.upload(table_name=table_name, credentials=self._credentials)
simon-contreras-deel marked this conversation as resolved.
Show resolved Hide resolved
layer.source = Source(table_name, credentials=self._credentials)

return layer

def _manage_maps_api_key(self, name):
non_public_datasets = [layer.source.dataset
for layer in self._layers
if not layer.source.dataset.is_public()]

if len(non_public_datasets) > 0:
api_key_name = '{}_{}_api_key'.format(name, int(time.time() * 1000))
auth_api_client = AuthAPIClient(self._credentials)
self._maps_api_key = auth_api_client.create_api_key(non_public_datasets, api_key_name, ['maps'])

def _add_layers_credentials(self):
for layer in self._layers:
layer.source.dataset.credentials = self._credentials

layer.credentials = {
'username': self._credentials.username,
'api_key': maps_api_key,
'base_url': self._credentials.base_url
'username': layer.source.dataset.credentials.username,
'api_key': self._maps_api_key,
'base_url': layer.source.dataset.credentials.base_url
}

return self._layers

def sync_layers(self, table_name, credentials=None):
for idx, layer in enumerate(self._layers):
table_name = normalize_name("{name}_{idx}".format(name=table_name, idx=idx + 1))

dataset_credentials = credentials or layer.source.dataset.credentials or get_default_credentials()

self._sync_layer(layer, table_name, dataset_credentials)

def _sync_layer(self, layer, table_name, credentials):
if layer.source.dataset.is_local():
layer.source.dataset.upload(table_name=table_name, credentials=credentials)
layer.source = Source(table_name, credentials=credentials)
warn('Table `{}` created. In order to publish the map, you will need to create a new Regular API '
'key with permissions to Maps API and the table `{}`. You can do it from your CARTO dashboard or '
'using the Auth API. You can get more info at '
'https://carto.com/developers/auth-api/guides/types-of-API-Keys/'.format(table_name, table_name))


def _create_kuviz(html, name, auth_client, password=None):
km = _get_kuviz_manager(auth_client)
Expand Down
85 changes: 23 additions & 62 deletions cartoframes/viz/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from . import constants
from .basemaps import Basemaps
from .kuviz import KuvizPublisher, kuviz_to_dict
from .kuviz import KuvizPublisher
from .html.HTMLMap import HTMLMap
from ..utils.utils import get_center

Expand Down Expand Up @@ -212,7 +212,8 @@ def __init__(self,

self._carto_vl_path = kwargs.get('_carto_vl_path', None)
self._airship_path = kwargs.get('_airship_path', None)
self._publisher = self._get_publisher()

self._publisher = None
self._kuviz = None

self.camera = None
Expand Down Expand Up @@ -270,16 +271,20 @@ def get_content(self):
'_airship_path': self._airship_path
}

def publish(self, name, maps_api_key='default_public', credentials=None, password=None):
def publish(self, name, table_name=None, credentials=None, password=None):
"""Publish the map visualization as a CARTO custom visualization (aka Kuviz).

Args:
name (str): The Kuviz name on CARTO
maps_api_key (str, optional): A Regular API key with permissions
to Maps API and datasets used by the map
table_name (str, optional): Desired table name for the dataset on CARTO.
It is required working with local data (we need to upload it to CARTO)
If name does not conform to SQL naming conventions, it will be
'normalized' (e.g., all lower case, adding `_` in place of spaces
and other special characters.
credentials (:py:class:`Credentials <cartoframes.auth.Credentials>`, optional):
A Credentials instance. If not provided, the credentials will be automatically
obtained from the default credentials if available.
obtained from the default credentials if available. It is used to create the
publication and also to save local data (if exists) into your CARTO account
password (str, optional): setting it your Kuviz will be protected by
password. When someone will try to show the Kuviz, the password
will be requested
Expand All @@ -296,65 +301,27 @@ def publish(self, name, maps_api_key='default_public', credentials=None, passwor
tmap.publish('Custom Map Title')

"""
if not self._publisher.is_sync():
raise CartoException('The map layers are not synchronized with CARTO. '
'Please, use the `sync_data` method before publishing the map')
self._publisher = _get_publisher(table_name, credentials)
self._publisher.set_layers(self.layers, name, table_name)

if maps_api_key == 'default_public':
self._validate_public_publication()

self._publisher.set_credentials(credentials)
html = self._get_publication_html(name, maps_api_key)
self._kuviz = self._publisher.publish(html, name, password)
return kuviz_to_dict(self._kuviz)

def sync_data(self, table_name, credentials=None):
"""Synchronize datasets used by the map with CARTO.

Args:
table_name (str): Desired table name for the dataset on CARTO. If
name does not conform to SQL naming conventions, it will be
'normalized' (e.g., all lower case, adding `_` in place of spaces
and other special characters.
credentials (:py:class:`Credentials <cartoframes.auth.Credentials>`, optional):
A Credentials instance. If not provided, the credentials will be automatically
obtained from the default credentials if available.
"""
if not self._publisher.is_sync():
self._publisher.sync_layers(table_name, credentials)
html = self._get_publication_html(name)
return self._publisher.publish(html, name, password)

def delete_publication(self):
"""Delete the published map Kuviz."""
if self._kuviz:
self._kuviz.delete()
print("Publication '{n}' ({id}) deleted".format(n=self._kuviz.name, id=self._kuviz.id))
self._kuviz = None
return self._publisher.delete()

def update_publication(self, name, password, maps_api_key='default_public'):
def update_publication(self, name, password):
"""Update the published map Kuviz.

Args:
name (str): The Kuviz name on CARTO
password (str): setting it your Kuviz will be protected by
password and using `None` the Kuviz will be public
maps_api_key (str, optional): A Regular API key with permissions
to Maps API and datasets used by the map
"""
if not self._kuviz:
raise CartoException('The map has not been published. Use the `publish` method.')

if not self._publisher.is_sync():
raise CartoException('The map layers are not synchronized with CARTO. '
'Please, use the `sync_data` method before publishing the map')

if maps_api_key == 'default_public':
self._validate_public_publication()

self._kuviz.data = self._get_publication_html(name, maps_api_key)
self._kuviz.name = name
self._kuviz.password = password
self._kuviz.save()
return kuviz_to_dict(self._kuviz)
html = self._get_publication_html(name)
return self._publisher.update(html, name, password)

@staticmethod
def all_publications(credentials=None):
Expand All @@ -367,10 +334,10 @@ def all_publications(credentials=None):
"""
return KuvizPublisher.all(credentials)

def _get_publication_html(self, name, maps_api_key):
def _get_publication_html(self, name):
html_map = HTMLMap('templates/viz/main.html.j2')
html_map.set_content(
layers=_get_layer_defs(self._publisher.get_layers(maps_api_key)),
layers=_get_layer_defs(self._publisher.get_layers()),
bounds=self.bounds,
size=None,
viewport=self.viewport,
Expand All @@ -391,15 +358,9 @@ def _validate_default_legend(self, default_legend, title):
if default_legend and not title:
raise CartoException('The default legend needs a map title to be displayed')

def _get_publisher(self):
return KuvizPublisher(self.layers)

def _validate_public_publication(self):
if not self._publisher.is_public():
raise CartoException('The datasets used in your map are not public. '
'You need add new Regular API key with permissions to Maps API and the datasets. '
'You can do it from your CARTO dashboard or using the Auth API. You can get more '
'info at https://carto.com/developers/auth-api/guides/types-of-API-Keys/')
def _get_publisher(self, credentials):
return KuvizPublisher(credentials)


def _get_bounds(bounds, layers):
Expand Down
Loading