-
-
Save tamaramalysh5991/8d27330d8fc361e8d89392dcd2814548 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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