import re
from datetime import (
date,
datetime,
)
import numpy as np
import pandas as pd
from fireant.dataset.fields import DataType
from fireant.dataset.totals import TOTALS_MARKERS
from fireant.utils import (
filter_kwargs,
)
RAW_VALUE = 'raw'
INF_VALUE = "Inf"
NULL_VALUE = 'null'
BLANK_VALUE = ''
TOTALS_VALUE = '$totals'
TOTALS_LABEL = 'Totals'
epoch = np.datetime64(datetime.utcfromtimestamp(0))
milliseconds = np.timedelta64(1, 'ms')
DATE_FORMATS = {
'iso': '%Y-%m-%dT%H:%M:%S',
'hour': '%Y-%m-%d %H:00',
'day': '%Y-%m-%d',
'week': 'W%W %Y-%m-%d',
'month': '%b %Y',
'quarter': '%Y-%q',
'year': '%Y',
}
@filter_kwargs
def date_as_string(value, interval_key=None):
f = DATE_FORMATS.get(interval_key, '%Y-%m-%d')
return value.strftime(f)
@filter_kwargs
def date_as_millis(value):
if isinstance(value, date) and not isinstance(value, datetime):
value = datetime.combine(value, datetime.min.time())
return int(1000 * value.timestamp())
@filter_kwargs
def return_none(value):
return None
@filter_kwargs
def _format_decimal(value, thousands='', precision=None):
if not isinstance(value, (int, float)):
return value
precision_pattern = '{{:{}.{}f}}'.format(thousands, precision) \
if precision is not None \
else '{{:{}f}}'.format(thousands)
f_value = precision_pattern.format(value)
if precision is None:
f_value = f_value.rstrip('0').rstrip('.')
return f_value
@filter_kwargs
def wrap_styling(value, prefix=None, suffix=None):
if value is None:
return value
return '{prefix}{value}{suffix}' \
.format(prefix=prefix or '',
suffix=suffix or '',
value=value)
@filter_kwargs
def _identity(value):
return value
UNSAFE_CHARS = re.compile(r'[^\w\d\s\-:()]')
[docs]def json_value(value):
"""
This function will return only values safe for JSON
"""
if pd.isnull(value):
return None
if isinstance(value, float) and np.isinf(value):
return 'inf'
if value in TOTALS_MARKERS:
return TOTALS_VALUE
return value
[docs]def safe_value(value):
if value is None or value is '' or pd.isnull(value):
return NULL_VALUE
if isinstance(value, float) and np.isinf(value):
return 'inf'
if value in TOTALS_MARKERS:
return TOTALS_VALUE
str_value = date_as_string(value, interval_key='iso') \
if isinstance(value, date) \
else str(value)
return UNSAFE_CHARS.sub('$', str_value)
@filter_kwargs
def _format_date_field(value, date_as=date_as_string, **kwargs):
return date_as(value, **kwargs)
RAW_FIELD_FORMATTER = {
DataType.date: _format_date_field,
}
[docs]def raw_value(value, field, date_as=date_as_string):
"""
Converts a raw metric value into a safe type. This will change dates into strings, NaNs into Nones, and np types
into their corresponding python types.
:param value:
The raw metric value.
:param field:
The slicer field that the value represents.
:param date_as:
"""
if value is None or pd.isnull(value):
return None
if isinstance(value, float) and np.isinf(value):
return None
if value in TOTALS_MARKERS:
return TOTALS_VALUE
formatter = RAW_FIELD_FORMATTER.get(field.data_type, _identity)
return formatter(value,
date_as=date_as,
interval_key='iso')
def _format_number_field_value(value, **kwargs):
number = _format_decimal(value, **kwargs)
return wrap_styling(number, **kwargs)
@filter_kwargs
def _format_boolean_field(value):
return str(value).lower()
FIELD_DISPLAY_FORMATTER = {
DataType.date: _format_date_field,
DataType.number: _format_number_field_value,
DataType.boolean: _format_boolean_field,
DataType.text: return_none,
}
[docs]def display_value(value, field, date_as=date_as_string):
"""
Converts a metric value into the display value by applying formatting.
:param value:
The raw metric value.
:param field:
The slicer field that the value represents.
:param date_as:
A format function for datetimes.
:return:
A formatted string containing the display value for the metric.
"""
if pd.isnull(value):
return BLANK_VALUE
if isinstance(value, float) and np.isinf(value):
return INF_VALUE
if value in TOTALS_MARKERS:
return TOTALS_LABEL
format_kwargs = {key: getattr(field, key, None)
for key in ('prefix', 'suffix', 'thousands', 'precision', 'interval_key')}
format_kwargs = {key: value
for key, value in format_kwargs.items()
if value is not None}
formatter = FIELD_DISPLAY_FORMATTER.get(field.data_type, _identity)
return formatter(value,
date_as=date_as,
**format_kwargs)