Skip to content

Commit 82076a5

Browse files
authored
Adding Mode dashboard usage extractor and generic loader (#225)
* Adding Mode dashboard usage extractor and generic loader * Update * Increment version
1 parent adf6d2c commit 82076a5

File tree

5 files changed

+151
-5
lines changed

5 files changed

+151
-5
lines changed

databuilder/extractor/dashboard/mode_analytics/mode_dashboard_extractor.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@
1313
from databuilder.transformer.template_variable_substitution_transformer import \
1414
TemplateVariableSubstitutionTransformer, TEMPLATE, FIELD_NAME as VAR_FIELD_NAME
1515

16-
# CONFIG KEYS
17-
ORGANIZATION = 'organization'
18-
MODE_ACCESS_TOKEN = 'mode_user_token'
19-
MODE_PASSWORD_TOKEN = 'mode_password_token'
2016

2117
LOGGER = logging.getLogger(__name__)
2218

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import logging
2+
3+
from pyhocon import ConfigTree # noqa: F401
4+
from typing import Any # noqa: F401
5+
6+
from databuilder.extractor.base_extractor import Extractor
7+
from databuilder.extractor.dashboard.mode_analytics.mode_dashboard_utils import ModeDashboardUtils
8+
from databuilder.rest_api.rest_api_query import RestApiQuery
9+
10+
11+
LOGGER = logging.getLogger(__name__)
12+
13+
14+
class ModeDashboardUsageExtractor(Extractor):
15+
"""
16+
A Extractor that extracts Mode dashboard's accumulated view count
17+
"""
18+
19+
def init(self, conf):
20+
# type: (ConfigTree) -> None
21+
22+
self._conf = conf
23+
24+
restapi_query = self._build_restapi_query()
25+
self._extractor = ModeDashboardUtils.create_mode_rest_api_extractor(restapi_query=restapi_query,
26+
conf=self._conf)
27+
28+
def extract(self):
29+
# type: () -> Any
30+
31+
return self._extractor.extract()
32+
33+
def get_scope(self):
34+
# type: () -> str
35+
36+
return 'extractor.mode_dashboard_usage'
37+
38+
def _build_restapi_query(self):
39+
"""
40+
Build REST API Query. To get Mode Dashboard usage, it needs to call two APIs (spaces API and reports
41+
API) joining together.
42+
:return: A RestApiQuery that provides Mode Dashboard metadata
43+
"""
44+
# type: () -> RestApiQuery
45+
46+
# https://mode.com/developer/api-reference/analytics/reports/#listReportsInSpace
47+
reports_url_template = 'https://app.mode.com/api/{organization}/spaces/{dashboard_group_id}/reports'
48+
49+
spaces_query = ModeDashboardUtils.get_spaces_query_api(conf=self._conf)
50+
params = ModeDashboardUtils.get_auth_params(conf=self._conf)
51+
52+
# Reports
53+
# JSONPATH expression. it goes into array which is located in _embedded.reports and then extracts token,
54+
# and view_count
55+
json_path = '_embedded.reports[*].[token,view_count]'
56+
field_names = ['dashboard_id', 'accumulated_view_count']
57+
reports_query = RestApiQuery(query_to_join=spaces_query, url=reports_url_template, params=params,
58+
json_path=json_path, field_names=field_names, skip_no_result=True)
59+
return reports_query
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import logging
2+
3+
from pyhocon import ConfigTree # noqa: F401
4+
from typing import Optional, Any # noqa: F401
5+
6+
from databuilder.loader.base_loader import Loader
7+
8+
LOGGER = logging.getLogger(__name__)
9+
10+
CALLBACK_FUNCTION = 'callback_function'
11+
12+
13+
def log_call_back(record):
14+
"""
15+
A Sample callback function. Implement any function follows this function's signature to fit your needs.
16+
:param record:
17+
:return:
18+
"""
19+
LOGGER.info('record: {}'.format(record))
20+
21+
22+
class GenericLoader(Loader):
23+
"""
24+
Loader class to call back a function provided by user
25+
"""
26+
27+
def init(self, conf):
28+
# type: (ConfigTree) -> None
29+
"""
30+
Initialize file handlers from conf
31+
:param conf:
32+
"""
33+
self.conf = conf
34+
self._callback_func = self.conf.get(CALLBACK_FUNCTION, log_call_back)
35+
36+
def load(self, record):
37+
# type: (Optional[Any]) -> None
38+
"""
39+
Write record to function
40+
:param record:
41+
:return:
42+
"""
43+
if not record:
44+
return
45+
46+
self._callback_func(record)
47+
48+
def close(self):
49+
# type: () -> None
50+
pass
51+
52+
def get_scope(self):
53+
# type: () -> str
54+
return "loader.generic"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from setuptools import setup, find_packages
33

44

5-
__version__ = '2.3.3'
5+
__version__ = '2.3.4'
66

77

88
requirements_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'requirements.txt')
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import unittest
2+
3+
from mock import MagicMock
4+
from pyhocon import ConfigFactory
5+
6+
from databuilder.loader.generic_loader import GenericLoader, CALLBACK_FUNCTION
7+
8+
9+
class TestGenericLoader(unittest.TestCase):
10+
11+
def test_loading(self):
12+
# type: () -> None
13+
14+
loader = GenericLoader()
15+
callback_func = MagicMock()
16+
loader.init(conf=ConfigFactory.from_dict({
17+
CALLBACK_FUNCTION: callback_func
18+
}))
19+
20+
loader.load({'foo': 'bar'})
21+
loader.close()
22+
23+
callback_func.assert_called_once()
24+
25+
def test_none_loading(self):
26+
# type: () -> None
27+
28+
loader = GenericLoader()
29+
callback_func = MagicMock()
30+
loader.init(conf=ConfigFactory.from_dict({
31+
CALLBACK_FUNCTION: callback_func
32+
}))
33+
34+
loader.load(None)
35+
loader.close()
36+
37+
callback_func.assert_not_called()

0 commit comments

Comments
 (0)