Skip to content

Commit

Permalink
Merge pull request #1481 from CartoDB/1457-add-metrics
Browse files Browse the repository at this point in the history
Add metrics
  • Loading branch information
Jesus89 committed Jan 14, 2020
2 parents b4b07a9 + a572301 commit 6df37ea
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 32 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Add metrics and utils.setup_metrics function (#1457)

## [1.0rc1] - 2020-01-10

[RC1 Migration Guide](/docs/developers/migrations/rc1.md)
Expand Down
2 changes: 1 addition & 1 deletion cartoframes/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0rc1'
__version__ = '1.0.0dev0'
36 changes: 18 additions & 18 deletions cartoframes/auth/credentials.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
"""Credentials management for CARTOframes usage."""

import os
import json
import appdirs

from urllib.parse import urlparse
from carto.auth import APIKeyAuthClient
from carto.do_token import DoTokenManager

from .. import __version__
from ..utils.logger import log
from ..utils.utils import is_valid_str, check_do_enabled
from ..utils.utils import is_valid_str, check_do_enabled, save_in_config, \
read_from_config, default_config_path

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

_USER_CONFIG_DIR = appdirs.user_config_dir('cartoframes')
_DEFAULT_PATH = os.path.join(_USER_CONFIG_DIR, 'cartocreds.json')
DEFAULT_CREDS_FILENAME = 'creds.json'


class Credentials:
Expand Down Expand Up @@ -121,8 +119,10 @@ def from_file(cls, config_file=None, session=None):
"""
try:
with open(config_file or _DEFAULT_PATH, 'r') as f:
credentials = json.load(f)
if config_file is None:
credentials = read_from_config(filename=DEFAULT_CREDS_FILENAME)
else:
credentials = read_from_config(filepath=config_file)
return cls(
credentials.get('username'),
credentials.get('api_key'),
Expand Down Expand Up @@ -171,18 +171,18 @@ def save(self, config_file=None):
User credentials for `johnsmith` were successfully saved to `creds.json`
"""
if config_file is None:
config_file = _DEFAULT_PATH
content = {
'username': self._username,
'api_key': self._api_key,
'base_url': self._base_url
}

# create directory if not exists
if not os.path.exists(_USER_CONFIG_DIR):
os.makedirs(_USER_CONFIG_DIR)
if config_file is None:
config_file = save_in_config(content, filename=DEFAULT_CREDS_FILENAME)
else:
config_file = save_in_config(content, filepath=config_file)

with open(config_file, 'w') as _file:
json.dump({
'username': self._username,
'api_key': self._api_key,
'base_url': self._base_url}, _file)
if config_file is not None:
log.info('User credentials for `{0}` were successfully saved to `{1}`'.format(
self._username or self._base_url, config_file))

Expand All @@ -206,7 +206,7 @@ def delete(cls, config_file=None):
base_url='https://johnsmith.carto.com/')
"""
path_to_remove = config_file or _DEFAULT_PATH
path_to_remove = config_file or default_config_path(DEFAULT_CREDS_FILENAME)

try:
os.remove(path_to_remove)
Expand Down
3 changes: 3 additions & 0 deletions cartoframes/io/carto.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
from ..utils.geom_utils import set_geometry, has_geometry
from ..utils.logger import log
from ..utils.utils import is_valid_str, is_sql_query
from ..utils.metrics import send_metrics


GEOM_COLUMN_NAME = 'the_geom'

IF_EXISTS_OPTIONS = ['fail', 'replace', 'append']


@send_metrics('data_downloaded')
def read_carto(source, credentials=None, limit=None, retry_times=3, schema=None, index_col=None, decode_geom=True):
"""Read a table or a SQL query from the CARTO account.
Expand Down Expand Up @@ -61,6 +63,7 @@ def read_carto(source, credentials=None, limit=None, retry_times=3, schema=None,
return gdf


@send_metrics('data_uploaded')
def to_carto(dataframe, table_name, credentials=None, if_exists='fail', geom_col=None, index=False, index_label=None,
cartodbfy=True, log_enabled=True):
"""Upload a Dataframe to CARTO.
Expand Down
2 changes: 2 additions & 0 deletions cartoframes/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from .logger import set_log_level
from .geom_utils import decode_geometry
from .metrics import setup_metrics

__all__ = [
'setup_metrics',
'set_log_level',
'decode_geometry'
]
101 changes: 101 additions & 0 deletions cartoframes/utils/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import os
import uuid
import requests

from .logger import log
from .utils import default_config_path, read_from_config, save_in_config, \
is_uuid, get_local_time, silent_fail, get_runtime_env
from .. import __version__

EVENT_VERSION = '1'
EVENT_SOURCE = 'cartoframes'

UUID_KEY = 'uuid'
ENABLED_KEY = 'enabled'
METRICS_FILENAME = 'metrics.json'

_metrics_config = None


def setup_metrics(enabled):
'''Update the metrics configuration.
Args:
enabled (bool): flag to enable/disable metrics.
'''
global _metrics_config

_metrics_config[ENABLED_KEY] = enabled

save_in_config(_metrics_config, filename=METRICS_FILENAME)


@silent_fail
def init_metrics_config():
global _metrics_config

filepath = default_config_path(METRICS_FILENAME)

if _metrics_config is None:
if os.path.exists(filepath):
_metrics_config = read_from_config(filepath=filepath)

if not check_valid_metrics_uuid(_metrics_config):
_metrics_config = create_metrics_config()
save_in_config(_metrics_config, filename=METRICS_FILENAME)


def create_metrics_config():
return {
UUID_KEY: str(uuid.uuid4()),
ENABLED_KEY: True
}


def get_metrics_uuid():
if _metrics_config is not None:
return _metrics_config.get(UUID_KEY)


def get_metrics_enabled():
if _metrics_config is not None:
return _metrics_config.get(ENABLED_KEY)


def check_valid_metrics_uuid(metrics_config):
return metrics_config is not None and is_uuid(metrics_config.get(UUID_KEY))


def build_metrics_data(event_name):
return {
'event_version': EVENT_VERSION,
'event_time': get_local_time(),
'event_source': EVENT_SOURCE,
'event_name': event_name,
'source_version': __version__,
'installation_id': get_metrics_uuid(),
'runtime_env': get_runtime_env()
}


@silent_fail
def post_metrics(event_name):
if get_metrics_enabled():
json_data = build_metrics_data(event_name)
result = requests.post('https://carto.com/api/metrics', json=json_data, timeout=2)
log.debug('Metrics sent! {0} {1}'.format(result.status_code, json_data))


def send_metrics(event_name):
def decorator_func(func):
def wrapper_func(*args, **kwargs):
result = func(*args, **kwargs)
post_metrics(event_name)
return result
return wrapper_func
return decorator_func


# Run this once
init_metrics_config()
59 changes: 58 additions & 1 deletion cartoframes/utils/utils.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
"""general utility functions"""

import os
import re
import gzip
import json
import time
import base64
import appdirs
import decimal
import hashlib
import requests
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
from pyrestcli.exceptions import ServerErrorException
from pandas.api.types import is_datetime64_any_dtype as is_datetime
Expand All @@ -28,6 +30,8 @@

PG_NULL = '__null'

USER_CONFIG_DIR = appdirs.user_config_dir('cartoframes')


def map_geom_type(geom_type):
return {
Expand Down Expand Up @@ -322,6 +326,11 @@ def is_json_filepath(text):
return re.match(r'^.*\.json\s*$', text, re.IGNORECASE)


def is_uuid(text):
if text is not None:
return re.match(r'^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$', text)


def get_credentials(credentials=None):
from ..auth import defaults
_credentials = credentials or defaults.get_default_credentials()
Expand Down Expand Up @@ -401,6 +410,11 @@ def replacer(match):
return re.sub(pattern, replacer, text).strip()


def get_local_time():
local_time = datetime.now(timezone.utc).astimezone()
return local_time.isoformat()


def timelogger(method):
def fn(*args, **kw):
start = time.time()
Expand Down Expand Up @@ -463,3 +477,46 @@ def is_ipython_notebook():
return False
except NameError:
return False


def get_runtime_env():
if is_ipython_notebook():
kernel_class = get_ipython().config['IPKernelApp'].get('kernel_class', '') # noqa: F821
if kernel_class.startswith('google.colab'):
return 'google.colab'
else:
return 'notebook'
else:
return 'cli'


def save_in_config(content, filename=None, filepath=None):
if filepath is None:
if not os.path.exists(USER_CONFIG_DIR):
os.makedirs(USER_CONFIG_DIR)
filepath = default_config_path(filename)

with open(filepath, 'w') as f:
json.dump(content, f)
return filepath


def read_from_config(filename=None, filepath=None):
if filepath is None:
filepath = default_config_path(filename)

with open(filepath, 'r') as f:
return json.load(f)


def default_config_path(filename):
return os.path.join(USER_CONFIG_DIR, filename)


def silent_fail(method):
def fn(*args, **kw):
try:
return method(*args, **kw)
except Exception:
pass
return fn
3 changes: 3 additions & 0 deletions cartoframes/viz/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .basemaps import Basemaps
from .kuviz import KuvizPublisher
from ..utils.utils import get_center
from ..utils.metrics import send_metrics

WORLD_BOUNDS = [[-180, -90], [180, 90]]

Expand Down Expand Up @@ -136,6 +137,7 @@ def __init__(self,
'pitch': viewport.get('pitch')
}

@send_metrics('map_created')
def _repr_html_(self):
self._html_map = HTMLMap()

Expand Down Expand Up @@ -179,6 +181,7 @@ def get_content(self):
'_airship_path': self._airship_path
}

@send_metrics('map_published')
def publish(self, name, password, table_name=None, credentials=None):
"""Publish the map visualization as a CARTO custom visualization.
Expand Down
31 changes: 31 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from cartoframes.utils import setup_metrics


def pytest_configure(config):
"""
Allows plugins and conftest files to perform initial configuration.
This hook is called for every plugin and initial conftest
file after command line options have been parsed.
"""
setup_metrics(False)


def pytest_sessionstart(session):
"""
Called after the Session object has been created and
before performing collection and entering the run test loop.
"""


def pytest_sessionfinish(session, exitstatus):
"""
Called after whole test run finished, right before
returning the exit status to the system.
"""


def pytest_unconfigure(config):
"""
called before test process is exited.
"""
setup_metrics(True)

0 comments on commit 6df37ea

Please sign in to comment.