-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Mode dashboard owner * Not having dashboard_owner to create User node * Update
- Loading branch information
1 parent
b835b8b
commit 225c740
Showing
9 changed files
with
258 additions
and
7 deletions.
There are no files selected for viewing
76 changes: 76 additions & 0 deletions
76
databuilder/extractor/dashboard/mode_analytics/mode_dashboard_owner_extractor.py
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import logging | ||
|
||
from pyhocon import ConfigTree, ConfigFactory # noqa: F401 | ||
from typing import Any # noqa: F401 | ||
|
||
from databuilder.extractor.base_extractor import Extractor | ||
from databuilder.extractor.dashboard.mode_analytics.mode_dashboard_utils import ModeDashboardUtils | ||
from databuilder.extractor.restapi.rest_api_extractor import MODEL_CLASS | ||
from databuilder.rest_api.rest_api_failure_handlers import HttpFailureSkipOnStatus | ||
from databuilder.rest_api.rest_api_query import RestApiQuery | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class ModeDashboardOwnerExtractor(Extractor): | ||
""" | ||
An Extractor that extracts Dashboard owner. | ||
""" | ||
|
||
def init(self, conf): | ||
# type: (ConfigTree) -> None | ||
self._conf = conf | ||
|
||
restapi_query = self._build_restapi_query() | ||
self._extractor = ModeDashboardUtils.create_mode_rest_api_extractor( | ||
restapi_query=restapi_query, | ||
conf=self._conf.with_fallback( | ||
ConfigFactory.from_dict( | ||
{MODEL_CLASS: 'databuilder.models.dashboard.dashboard_owner.DashboardOwner', } | ||
) | ||
) | ||
) | ||
|
||
def extract(self): | ||
# type: () -> Any | ||
|
||
return self._extractor.extract() | ||
|
||
def get_scope(self): | ||
# type: () -> str | ||
return 'extractor.mode_dashboard_owner' | ||
|
||
def _build_restapi_query(self): | ||
""" | ||
Build REST API Query. To get Mode Dashboard owner, it needs to call three APIs (spaces API, reports | ||
API, and user API) joining together. | ||
:return: A RestApiQuery that provides Mode Dashboard owner | ||
""" | ||
# type: () -> RestApiQuery | ||
|
||
# https://mode.com/developer/api-reference/analytics/reports/#listReportsInSpace | ||
report_url_template = 'https://app.mode.com/api/{organization}/spaces/{dashboard_group_id}/reports' | ||
|
||
# https://mode.com/developer/api-reference/management/users/ | ||
creator_url_template = 'https://app.mode.com{creator_resource_path}' | ||
|
||
spaces_query = ModeDashboardUtils.get_spaces_query_api(conf=self._conf) | ||
params = ModeDashboardUtils.get_auth_params(conf=self._conf) | ||
|
||
# Reports | ||
json_path = '(_embedded.reports[*].token) | (_embedded.reports[*]._links.creator.href)' | ||
field_names = ['dashboard_id', 'creator_resource_path'] | ||
creator_resource_path_query = RestApiQuery(query_to_join=spaces_query, url=report_url_template, params=params, | ||
json_path=json_path, field_names=field_names, skip_no_result=True, | ||
json_path_contains_or=True) | ||
|
||
json_path = 'email' | ||
field_names = ['email'] | ||
failure_handler = HttpFailureSkipOnStatus(status_codes_to_skip={404}) | ||
owner_email_query = RestApiQuery(query_to_join=creator_resource_path_query, url=creator_url_template, | ||
params=params, | ||
json_path=json_path, field_names=field_names, skip_no_result=True, | ||
can_skip_failure=failure_handler.can_skip_failure) | ||
|
||
return owner_email_query |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import logging | ||
|
||
from typing import Optional, Dict, Any, Union, Iterator # noqa: F401 | ||
|
||
from databuilder.models.dashboard_metadata import DashboardMetadata | ||
from databuilder.models.neo4j_csv_serde import ( | ||
Neo4jCsvSerializable, RELATION_START_KEY, RELATION_END_KEY, RELATION_START_LABEL, | ||
RELATION_END_LABEL, RELATION_TYPE, RELATION_REVERSE_TYPE) | ||
from databuilder.models.owner_constants import OWNER_OF_OBJECT_RELATION_TYPE, OWNER_RELATION_TYPE | ||
from databuilder.models.user import User | ||
|
||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class DashboardOwner(Neo4jCsvSerializable): | ||
""" | ||
A model that encapsulate Dashboard's owner. | ||
Note that it does not create new user as it has insufficient information about user but it builds relation | ||
between User and Dashboard | ||
""" | ||
|
||
DASHBOARD_EXECUTION_RELATION_TYPE = 'LAST_EXECUTED' | ||
EXECUTION_DASHBOARD_RELATION_TYPE = 'LAST_EXECUTION_OF' | ||
|
||
def __init__(self, | ||
dashboard_group_id, # type: str | ||
dashboard_id, # type: str | ||
email, # type: str | ||
product='', # type: Optional[str] | ||
cluster='gold', # type: str | ||
**kwargs | ||
): | ||
self._dashboard_group_id = dashboard_group_id | ||
self._dashboard_id = dashboard_id | ||
self._email = email | ||
self._product = product | ||
self._cluster = cluster | ||
|
||
self._relation_iterator = self._create_relation_iterator() | ||
|
||
def create_next_node(self): | ||
# type: () -> Union[Dict[str, Any], None] | ||
return None | ||
|
||
def create_next_relation(self): | ||
# type: () -> Union[Dict[str, Any], None] | ||
try: | ||
return next(self._relation_iterator) | ||
except StopIteration: | ||
return None | ||
|
||
def _create_relation_iterator(self): | ||
# type: () -> Iterator[[Dict[str, Any]]] | ||
yield { | ||
RELATION_START_LABEL: DashboardMetadata.DASHBOARD_NODE_LABEL, | ||
RELATION_END_LABEL: User.USER_NODE_LABEL, | ||
RELATION_START_KEY: DashboardMetadata.DASHBOARD_KEY_FORMAT.format( | ||
product=self._product, | ||
cluster=self._cluster, | ||
dashboard_group=self._dashboard_group_id, | ||
dashboard_name=self._dashboard_id | ||
), | ||
RELATION_END_KEY: User.get_user_model_key(email=self._email), | ||
RELATION_TYPE: OWNER_RELATION_TYPE, | ||
RELATION_REVERSE_TYPE: OWNER_OF_OBJECT_RELATION_TYPE | ||
} | ||
|
||
def __repr__(self): | ||
return 'DashboardOwner({!r}, {!r}, {!r}, {!r}, {!r})'.format( | ||
self._dashboard_group_id, | ||
self._dashboard_id, | ||
self._email, | ||
self._product, | ||
self._cluster | ||
) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
|
||
OWNER_RELATION_TYPE = 'OWNER' | ||
OWNER_OF_OBJECT_RELATION_TYPE = 'OWNER_OF' |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import abc | ||
|
||
import six | ||
from requests.exceptions import HTTPError | ||
from typing import Iterable, Union, List, Dict, Any, Optional # noqa: F401 | ||
|
||
|
||
@six.add_metaclass(abc.ABCMeta) | ||
class BaseFailureHandler(object): | ||
|
||
@abc.abstractmethod | ||
def can_skip_failure(self, | ||
exception, # type: Exception | ||
): | ||
# type: (...) -> bool | ||
pass | ||
|
||
|
||
class HttpFailureSkipOnStatus(BaseFailureHandler): | ||
|
||
def __init__(self, | ||
status_codes_to_skip, # type: Iterable[int] | ||
): | ||
# type: (...) -> None | ||
self._status_codes_to_skip = {v for v in status_codes_to_skip} | ||
|
||
def can_skip_failure(self, | ||
exception, # type: Exception | ||
): | ||
# type: (...) -> bool | ||
|
||
if (isinstance(exception, HTTPError) or hasattr(exception, 'response')) \ | ||
and exception.response.status_code in self._status_codes_to_skip: | ||
return True | ||
|
||
return False |
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
Empty file.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import unittest | ||
|
||
from databuilder.models.dashboard.dashboard_owner import DashboardOwner | ||
from databuilder.models.neo4j_csv_serde import RELATION_START_KEY, RELATION_START_LABEL, RELATION_END_KEY, \ | ||
RELATION_END_LABEL, RELATION_TYPE, RELATION_REVERSE_TYPE | ||
|
||
|
||
class TestDashboardOwner(unittest.TestCase): | ||
|
||
def test_dashboard_owner_nodes(self): | ||
# type: () -> None | ||
dashboard_owner = DashboardOwner(email='[email protected]', cluster='cluster_id', product='product_id', | ||
dashboard_id='dashboard_id', dashboard_group_id='dashboard_group_id') | ||
|
||
actual = dashboard_owner.create_next_node() | ||
self.assertIsNone(actual) | ||
|
||
def test_dashboard_owner_relations(self): | ||
# type: () -> None | ||
dashboard_owner = DashboardOwner(email='[email protected]', cluster='cluster_id', product='product_id', | ||
dashboard_id='dashboard_id', dashboard_group_id='dashboard_group_id') | ||
|
||
actual = dashboard_owner.create_next_relation() | ||
expected = {RELATION_END_KEY: '[email protected]', RELATION_START_LABEL: 'Dashboard', RELATION_END_LABEL: 'User', | ||
RELATION_START_KEY: 'product_id_dashboard://cluster_id.dashboard_group_id/dashboard_id', | ||
RELATION_TYPE: 'OWNER', | ||
RELATION_REVERSE_TYPE: 'OWNER_OF'} | ||
self.assertDictEqual(actual, expected) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import unittest | ||
|
||
from databuilder.rest_api.rest_api_failure_handlers import HttpFailureSkipOnStatus | ||
from mock import MagicMock | ||
|
||
|
||
class TestHttpFailureSkipOnStatus(unittest.TestCase): | ||
|
||
def testSkip(self): | ||
# typ: (...) -> None | ||
|
||
failure_handler = HttpFailureSkipOnStatus([404, 400]) | ||
|
||
exception = MagicMock() | ||
exception.response.status_code = 404 | ||
self.assertTrue(failure_handler.can_skip_failure(exception=exception)) | ||
|
||
exception.response.status_code = 400 | ||
self.assertTrue(failure_handler.can_skip_failure(exception=exception)) | ||
|
||
exception.response.status_code = 500 | ||
self.assertFalse(failure_handler.can_skip_failure(exception=exception)) |