Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@tamaramalysh5991
Created June 22, 2020 17:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tamaramalysh5991/8d27330d8fc361e8d89392dcd2814548 to your computer and use it in GitHub Desktop.
Save tamaramalysh5991/8d27330d8fc361e8d89392dcd2814548 to your computer and use it in GitHub Desktop.
from abc import ABC
from collections import OrderedDict
from typing import Iterable
from django.db.models import QuerySet, Q
from stripe_analytics.constants import DATE_COLUMN_FORMAT, PeriodType
from stripe_analytics.period import Period
from zerver.models import Realm
class AbstractDashboardMetric(ABC):
"""Abstract Metrics Class
Need to build analytics data like stripe dashboard
Metric should calculate data and return it in dict
For chart data, need to return x and y axis (labels and data)
Attributes:
* serializer: need to serialize data for API
* aggregation: func to calculate metric like sum or count
* date_column_name: define date field like `created`
* value_column_name: define the value field like `amount`
* is_contain_chart: define that metric contain the chart data.
* period_split: default metric period unit (day, month..)
Examples:
>>> gross = GrossVolumeMetric(account=realm, period=period)
>>> gross.aggregated_data
{'volume': 149.0}
>>>gross.as_dict()
{'volume': '149.00'}
>>>gross.chart_data
OrderedDict([(datetime.date(2020, 2, 14), 0.0),
(datetime.date(2020, 2, 21), 48.0),
(datetime.date(2020, 3, 24), 24.0),
(datetime.date(2020, 3, 25), 0.0),
(datetime.date(2020, 4, 8), 0.0),
(datetime.date(2020, 4, 9), 0.0),
(datetime.date(2020, 4, 10), 0.0),
(datetime.date(2020, 4, 11), 0.0),
(datetime.date(2020, 4, 21), 0.0),
(datetime.date(2020, 4, 22), 0.0),
(datetime.date(2020, 4, 23), 0.0),
(datetime.date(2020, 4, 24), 53.0),
(datetime.date(2020, 4, 25), 0.0),
(datetime.date(2020, 4, 26), 0.0),
(datetime.date(2020, 4, 27), 0.0),
(datetime.date(2020, 4, 28), 24.0),
...
>>>gross.get_chart_columns()
{
'data': [0, 0, 0, 0, 0, 0, 0],
'labels': [
'May 04',
'May 05',
'May 06',
'May 07',
'May 08',
'May 09',
'May 10'
]
}
"""
DEFAULT_METRIC_EMPTY_VALUE = 0
serializer = None
period_split = PeriodType.DAY
# attributes for build chart data
date_column_name = ''
value_column_name = ''
is_contain_chart = True
def __init__(
self,
account: Realm,
period: Period,
):
"""Constructor of metric
Args:
account: group which metrics needed
period: Period instance for which we should calculate the metrics
data: raw data to build metric
chart_data: chart data to build chart
aggregated_data: processed data with result
"""
self.account = account
self.period = period
self.data = None
self.aggregated_data = None
self.chart_data = None
self.get_raw_data()
# process aggregated data
self.build_aggregated_data()
# process chart data
if self.is_contain_chart:
self.build_chart_data()
def get_query_by_dates(self, period: Period = None) -> dict:
"""Get params to query by period
Args:
period: Period instance
Returns:
dict with start and end periods
"""
period = period or self.period
return dict(
start=period.start.timestamp,
end=period.end.timestamp
)
def get_query_params(self, period: Period = None) -> dict:
"""Define proper params for API query
in a concrete metrics class
"""
raise NotImplementedError()
def get_raw_data(self):
"""Get data to build metric"""
raise NotImplementedError()
def build_aggregated_data(self):
"""Build aggregation data"""
raise NotImplementedError()
def get_raw_chart_data(self):
"""Get data for charts"""
return self.data
def as_dict(self) -> dict:
"""Return serialized aggregated data in dict"""
many = isinstance(self.aggregated_data, list)
return self.serializer(self.aggregated_data, many=many).data
def calculate(self, data: Iterable, period: Period = None):
"""Calculate metric in certain period or for all the time
Args:
data: metric data
period: certain period, like one day
Returns:
calculated data
"""
raise NotImplementedError()
def build_chart_data(self, raw_chart_data=None) -> None:
"""Build chart data of metric
Need to get the metric value for the concete period
For charts, x axis is time, y axis is metric value in x time
Args:
raw_chart_data: data to build metric in certain period
Returns:
data to build chart
"""
chart_data = OrderedDict()
chart_raw_data = raw_chart_data or self.get_raw_chart_data()
periods = self.period.split(by=self.period_split)
for period in periods:
chart_data[period.end.date()] = self.calculate(
period=period, data=chart_raw_data
)
self.chart_data = chart_data
def get_chart_columns(self) -> dict:
"""Get formatted chart data
Returns:
labels and data to build chart
"""
if not self.chart_data:
return {}
return dict(
labels=list(map(
lambda x: x.strftime(DATE_COLUMN_FORMAT),
list(self.chart_data.keys()))
),
data=list(self.chart_data.values())
)
class StripeAPIAbstractMetric(AbstractDashboardMetric):
"""Metric where source of data is a stripe API
Attributes:
* prefetch_data_func: func to get data from stripe
"""
prefetch_data_func = None
def get_query_params(self, period: Period = None) -> dict:
"""Build query for retrieve metric data"""
query_params = self.get_query_by_dates(period=period)
query_params.update(
stripe_account=self.account.stripe_account_id
)
return query_params
def get_raw_data(self):
"""Get data from stripe API"""
query_params = self.get_query_params()
self.data = self.prefetch_data_func(**query_params)
def aggregation(self, data: Iterable, *args, **kwargs):
"""Aggregate the stripe data if need
Args:
data: stripe data like subscriptions
Returns:
aggregated data
"""
def calculate(self, data: Iterable, period: Period = None):
"""Default calculation of stripe metric
In most cases need to calculate sum of count of stripe objects
like subscriptions or payments
Args:
data: stripe data, like subscriptions, payouts and etc
period: certain period. For example, need calculate count of
of new subscriptions in one day
Returns:
calculated metric in one period
"""
if not data:
return self.DEFAULT_METRIC_EMPTY_VALUE
dates = self.get_query_by_dates(period=period)
if not dates:
filtered_data = [item[self.value_column_name] for item in data]
else:
filtered_data = [
item[self.value_column_name] for item in data
if dates['start'] <= item[
self.date_column_name
] <= dates['end']
]
return (
self.aggregation(filtered_data)
if self.aggregation else filtered_data
)
class DjangoAbstractMetric(AbstractDashboardMetric):
"""Metric where source of data is database
Attributes:
* queryset - source of data
* queryset_filter: default filter for queryset
* queryset_aggregation
"""
queryset_filter = None
queryset = None
def get_query_by_dates(self, period: Period = None) -> Q:
"""Build queryset query by period
Args:
period: Period instance
Returns:
query by period dates
"""
period = period or self.period
query = {
'%s__range' % self.date_column_name: [
period.start.datetime, period.end.datetime
]
}
return Q(**query)
def get_query_params(self, period: Period = None) -> Q:
"""Return default query"""
query = self.get_query_by_dates(period=period)
return Q(query)
def get_raw_data(self):
"""Retrieve data from database"""
self.data = self.queryset.filter(self.get_query_params())
def queryset_aggregation(self, queryset: QuerySet):
"""Aggregate queryset (count and etc)
Args:
queryset: queryset to aggregate
Returns:
aggregation result
"""
raise NotImplementedError()
def calculate(self, data: QuerySet = None, period: Period = None):
"""Calculate metric in period if need
In this case data is Django queryset
Args:
data: metric data
period: certain period
Returns:
calculated data
"""
data = data or self.data
if period:
data = data.filter(self.get_query_by_dates(period=period))
return self.queryset_aggregation(data)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment