diff --git a/.gitignore b/.gitignore index 5f5db36d7..36c353401 100644 --- a/.gitignore +++ b/.gitignore @@ -92,7 +92,8 @@ ENV/ # Rope project settings .ropeproject - +# VSCode project settings +.vscode/ # macOS.gitignore from https://github.com/github/gitignore *.DS_Store diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index bb938c8fa..b438d8a2e 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -1,5 +1,5 @@ from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE -from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\ +from .models import ConnectionCredentials, ConnectionItem, DataAlertItem, DatasourceItem,\ GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem,\ SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError,\ HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem,\ diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index c86057a3d..dff12a29d 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -2,6 +2,7 @@ from .connection_item import ConnectionItem from .column_item import ColumnItem from .data_acceleration_report_item import DataAccelerationReportItem +from .data_alert_item import DataAlertItem from .datasource_item import DatasourceItem from .database_item import DatabaseItem from .exceptions import UnpopulatedPropertyError diff --git a/tableauserverclient/models/data_alert_item.py b/tableauserverclient/models/data_alert_item.py new file mode 100644 index 000000000..559050b4b --- /dev/null +++ b/tableauserverclient/models/data_alert_item.py @@ -0,0 +1,197 @@ +import xml.etree.ElementTree as ET + +from .property_decorators import property_not_empty, property_is_enum, property_is_boolean +from .user_item import UserItem +from .view_item import ViewItem + + +class DataAlertItem(object): + class Frequency: + Once = 'Once' + Frequently = 'Frequently' + Hourly = 'Hourly' + Daily = 'Daily' + Weekly = 'Weekly' + + def __init__(self): + self._id = None + self._subject = None + self._creatorId = None + self._createdAt = None + self._updatedAt = None + self._frequency = None + self._public = None + self._owner_id = None + self._owner_name = None + self._view_id = None + self._view_name = None + self._workbook_id = None + self._workbook_name = None + self._project_id = None + self._project_name = None + self._recipients = None + + def __repr__(self): + return "".format(**self.__dict__) + + @property + def id(self): + return self._id + + @property + def subject(self): + return self._subject + + @subject.setter + @property_not_empty + def subject(self, value): + self._subject = value + + @property + def frequency(self): + return self._frequency + + @frequency.setter + @property_is_enum(Frequency) + def frequency(self, value): + self._frequency = value + + @property + def public(self): + return self._public + + @public.setter + @property_is_boolean + def public(self, value): + self._public = value + + @property + def creatorId(self): + return self._creatorId + + @property + def recipients(self): + return self._recipients or list() + + @property + def createdAt(self): + return self._createdAt + + @property + def updatedAt(self): + return self._updatedAt + + @property + def owner_id(self): + return self._owner_id + + @property + def owner_name(self): + return self._owner_name + + @property + def view_id(self): + return self._view_id + + @property + def view_name(self): + return self._view_name + + @property + def workbook_id(self): + return self._workbook_id + + @property + def workbook_name(self): + return self._workbook_name + + @property + def project_id(self): + return self._project_id + + @property + def project_name(self): + return self._project_name + + def _set_values(self, id, subject, creatorId, createdAt, updatedAt, + frequency, public, recipients, owner_id, owner_name, + view_id, view_name, workbook_id, workbook_name, project_id, + project_name): + if id is not None: + self._id = id + if subject: + self._subject = subject + if creatorId: + self._creatorId = creatorId + if createdAt: + self._createdAt = createdAt + if updatedAt: + self._updatedAt = updatedAt + if frequency: + self._frequency = frequency + if public: + self._public = public + if owner_id: + self._owner_id = owner_id + if owner_name: + self._owner_name = owner_name + if view_id: + self._view_id = view_id + if view_name: + self._view_name = view_name + if workbook_id: + self._workbook_id = workbook_id + if workbook_name: + self._workbook_name = workbook_name + if project_id: + self._project_id = project_id + if project_name: + self._project_name = project_name + if recipients: + self._recipients = recipients + + @classmethod + def from_response(cls, resp, ns): + all_alert_items = list() + parsed_response = ET.fromstring(resp) + all_alert_xml = parsed_response.findall('.//t:dataAlert', namespaces=ns) + + for alert_xml in all_alert_xml: + kwargs = cls._parse_element(alert_xml, ns) + alert_item = cls() + alert_item._set_values(**kwargs) + all_alert_items.append(alert_item) + + return all_alert_items + + @staticmethod + def _parse_element(alert_xml, ns): + kwargs = dict() + kwargs['id'] = alert_xml.get('id', None) + kwargs['subject'] = alert_xml.get('subject', None) + kwargs['creatorId'] = alert_xml.get('creatorId', None) + kwargs['createdAt'] = alert_xml.get('createdAt', None) + kwargs['updatedAt'] = alert_xml.get('updatedAt', None) + kwargs['frequency'] = alert_xml.get('frequency', None) + kwargs['public'] = alert_xml.get('public', None) + + owner = alert_xml.findall('.//t:owner', namespaces=ns)[0] + kwargs['owner_id'] = owner.get('id', None) + kwargs['owner_name'] = owner.get('name', None) + + view_response = alert_xml.findall('.//t:view', namespaces=ns)[0] + kwargs['view_id'] = view_response.get('id', None) + kwargs['view_name'] = view_response.get('name', None) + + workbook_response = view_response.findall('.//t:workbook', namespaces=ns)[0] + kwargs['workbook_id'] = workbook_response.get('id', None) + kwargs['workbook_name'] = workbook_response.get('name', None) + project_response = view_response.findall('.//t:project', namespaces=ns)[0] + kwargs['project_id'] = project_response.get('id', None) + kwargs['project_name'] = project_response.get('name', None) + + recipients = alert_xml.findall('.//t:recipient', namespaces=ns) + kwargs['recipients'] = [recipient.get('id', None) for recipient in recipients] + + return kwargs diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index aff549559..afebafabe 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -2,11 +2,11 @@ from .request_options import CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, RequestOptions from .filter import Filter from .sort import Sort -from .. import ConnectionItem, DatasourceItem, DatabaseItem, JobItem, BackgroundJobItem, \ +from .. import ConnectionItem, DataAlertItem, DatasourceItem, DatabaseItem, JobItem, BackgroundJobItem, \ GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\ UserItem, ViewItem, WorkbookItem, TableItem, TaskItem, SubscriptionItem, \ PermissionsRule, Permission, ColumnItem, FlowItem, WebhookItem -from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \ +from .endpoint import Auth, DataAlerts, Datasources, Endpoint, Groups, Projects, Schedules, \ Sites, Tables, Users, Views, Workbooks, Subscriptions, ServerResponseError, \ MissingRequiredFieldError, Flows, Favorites from .server import Server diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index 1341ecd3f..5d55509cf 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -1,5 +1,6 @@ from .auth_endpoint import Auth from .data_acceleration_report_endpoint import DataAccelerationReport +from .data_alert_endpoint import DataAlerts from .datasources_endpoint import Datasources from .databases_endpoint import Databases from .endpoint import Endpoint diff --git a/tableauserverclient/server/endpoint/data_alert_endpoint.py b/tableauserverclient/server/endpoint/data_alert_endpoint.py new file mode 100644 index 000000000..b28ec14c4 --- /dev/null +++ b/tableauserverclient/server/endpoint/data_alert_endpoint.py @@ -0,0 +1,94 @@ +from .endpoint import api, Endpoint +from .exceptions import MissingRequiredFieldError +from .permissions_endpoint import _PermissionsEndpoint +from .default_permissions_endpoint import _DefaultPermissionsEndpoint + +from .. import RequestFactory, DataAlertItem, PaginationItem, UserItem + +import logging + +logger = logging.getLogger('tableau.endpoint.dataAlerts') + + +class DataAlerts(Endpoint): + def __init__(self, parent_srv): + super(DataAlerts, self).__init__(parent_srv) + + @property + def baseurl(self): + return "{0}/sites/{1}/dataAlerts".format(self.parent_srv.baseurl, self.parent_srv.site_id) + + @api(version="3.2") + def get(self, req_options=None): + logger.info('Querying all dataAlerts on site') + url = self.baseurl + server_response = self.get_request(url, req_options) + pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) + all_dataAlert_items = DataAlertItem.from_response(server_response.content, self.parent_srv.namespace) + return all_dataAlert_items, pagination_item + + # Get 1 dataAlert + @api(version="3.2") + def get_by_id(self, dataAlert_id): + if not dataAlert_id: + error = "dataAlert ID undefined." + raise ValueError(error) + logger.info('Querying single dataAlert (ID: {0})'.format(dataAlert_id)) + url = "{0}/{1}".format(self.baseurl, dataAlert_id) + server_response = self.get_request(url) + return DataAlertItem.from_response(server_response.content, self.parent_srv.namespace)[0] + + @api(version="3.2") + def delete(self, dataAlert): + dataAlert_id = getattr(dataAlert, 'id', dataAlert) + if not dataAlert_id: + error = "Dataalert ID undefined." + raise ValueError(error) + # DELETE /api/api-version/sites/site-id/dataAlerts/data-alert-id/users/user-id + url = "{0}/{1}".format(self.baseurl, dataAlert_id) + self.delete_request(url) + logger.info('Deleted single dataAlert (ID: {0})'.format(dataAlert_id)) + + @api(version="3.2") + def delete_user_from_alert(self, dataAlert, user): + dataAlert_id = getattr(dataAlert, 'id', dataAlert) + user_id = getattr(user, 'id', user) + if not dataAlert_id: + error = "Dataalert ID undefined." + raise ValueError(error) + if not user_id: + error = "User ID undefined." + raise ValueError(error) + # DELETE /api/api-version/sites/site-id/dataAlerts/data-alert-id/users/user-id + url = "{0}/{1}/users/{2}".format(self.baseurl, dataAlert_id, user_id) + self.delete_request(url) + logger.info('Deleted User (ID {0}) from dataAlert (ID: {1})'.format(user_id, dataAlert_id)) + + @api(version="3.2") + def add_user_to_alert(self, dataAlert_item, user): + if not dataAlert_item.id: + error = "Dataalert item missing ID." + raise MissingRequiredFieldError(error) + user_id = getattr(user, 'id', user) + if not user_id: + error = "User ID undefined." + raise ValueError(error) + url = "{0}/{1}/users".format(self.baseurl, dataAlert_item.id) + update_req = RequestFactory.DataAlert.add_user_to_alert(dataAlert_item, user_id) + server_response = self.post_request(url, update_req) + logger.info('Added user (ID {0}) to dataAlert item (ID: {1})'.format(user_id, dataAlert_item.id)) + user = UserItem.from_response(server_response.content, self.parent_srv.namespace)[0] + return user + + @api(version="3.2") + def update(self, dataAlert_item): + if not dataAlert_item.id: + error = "Dataalert item missing ID." + raise MissingRequiredFieldError(error) + + url = "{0}/{1}".format(self.baseurl, dataAlert_item.id) + update_req = RequestFactory.DataAlert.update_req(dataAlert_item) + server_response = self.put_request(url, update_req) + logger.info('Updated dataAlert item (ID: {0})'.format(dataAlert_item.id)) + updated_dataAlert = DataAlertItem.from_response(server_response.content, self.parent_srv.namespace)[0] + return updated_dataAlert diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index c1a54760a..a1b0f1c26 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -86,6 +86,27 @@ def update_req(self, column_item): return ET.tostring(xml_request) +class DataAlertRequest(object): + def add_user_to_alert(self, alert_item, user_id): + xml_request = ET.Element('tsRequest') + user_element = ET.SubElement(xml_request, 'user') + user_element.attrib['id'] = user_id + + return ET.tostring(xml_request) + + def update_req(self, alert_item): + xml_request = ET.Element('tsRequest') + dataAlert_element = ET.SubElement(xml_request, 'dataAlert') + dataAlert_element.attrib['subject'] = alert_item.subject + dataAlert_element.attrib['frequency'] = alert_item.frequency.lower() + dataAlert_element.attrib['public'] = alert_item.public + + owner = ET.SubElement(dataAlert_element, 'owner') + owner.attrib['id'] = alert_item.owner_id + + return ET.tostring(xml_request) + + class DatabaseRequest(object): def update_req(self, database_item): xml_request = ET.Element('tsRequest') @@ -637,6 +658,7 @@ class RequestFactory(object): Auth = AuthRequest() Connection = Connection() Column = ColumnRequest() + DataAlert = DataAlertRequest() Datasource = DatasourceRequest() Database = DatabaseRequest() Empty = EmptyRequest() diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index c36ee0f4b..6aff0c126 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -4,7 +4,7 @@ from ..namespace import Namespace from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \ Schedules, ServerInfo, Tasks, Subscriptions, Jobs, Metadata,\ - Databases, Tables, Flows, Webhooks, DataAccelerationReport, Favorites + Databases, Tables, Flows, Webhooks, DataAccelerationReport, Favorites, DataAlerts from .endpoint.exceptions import EndpointUnavailableError, ServerInfoEndpointNotFoundError import requests @@ -58,6 +58,7 @@ def __init__(self, server_address, use_server_version=False): self.tables = Tables(self) self.webhooks = Webhooks(self) self.data_acceleration_report = DataAccelerationReport(self) + self.data_alerts = DataAlerts(self) self._namespace = Namespace() if use_server_version: diff --git a/test/assets/data_alerts_add_user.xml b/test/assets/data_alerts_add_user.xml new file mode 100644 index 000000000..2a367a7f1 --- /dev/null +++ b/test/assets/data_alerts_add_user.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/test/assets/data_alerts_get.xml b/test/assets/data_alerts_get.xml new file mode 100644 index 000000000..78a55d4ca --- /dev/null +++ b/test/assets/data_alerts_get.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/assets/data_alerts_get_by_id.xml b/test/assets/data_alerts_get_by_id.xml new file mode 100644 index 000000000..1a7456545 --- /dev/null +++ b/test/assets/data_alerts_get_by_id.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/assets/data_alerts_update.xml b/test/assets/data_alerts_update.xml new file mode 100644 index 000000000..78a55d4ca --- /dev/null +++ b/test/assets/data_alerts_update.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test_dataalert.py b/test/test_dataalert.py new file mode 100644 index 000000000..7822d3000 --- /dev/null +++ b/test/test_dataalert.py @@ -0,0 +1,115 @@ +import unittest +import os +import requests_mock +import xml.etree.ElementTree as ET +import tableauserverclient as TSC +from tableauserverclient.datetime_helpers import format_datetime +from tableauserverclient.server.endpoint.exceptions import InternalServerError +from tableauserverclient.server.request_factory import RequestFactory +from ._utils import read_xml_asset, read_xml_assets, asset + +GET_XML = 'data_alerts_get.xml' +GET_BY_ID_XML = 'data_alerts_get_by_id.xml' +ADD_USER_TO_ALERT = 'data_alerts_add_user.xml' +UPDATE_XML = 'data_alerts_update.xml' + + +class DataAlertTests(unittest.TestCase): + def setUp(self): + self.server = TSC.Server('http://test') + + # Fake signin + self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' + self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' + self.server.version = "3.2" + + self.baseurl = self.server.data_alerts.baseurl + + def test_get(self): + response_xml = read_xml_asset(GET_XML) + with requests_mock.mock() as m: + m.get(self.baseurl, text=response_xml) + all_alerts, pagination_item = self.server.data_alerts.get() + + self.assertEqual(1, pagination_item.total_available) + self.assertEqual('5ea59b45-e497-5673-8809-bfe213236f75', all_alerts[0].id) + self.assertEqual('Data Alert test', all_alerts[0].subject) + self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', all_alerts[0].creatorId) + self.assertEqual('2020-08-10T23:17:06Z', all_alerts[0].createdAt) + self.assertEqual('2020-08-10T23:17:06Z', all_alerts[0].updatedAt) + self.assertEqual('Daily', all_alerts[0].frequency) + self.assertEqual('true', all_alerts[0].public) + self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', all_alerts[0].owner_id) + self.assertEqual('Bob', all_alerts[0].owner_name) + self.assertEqual('d79634e1-6063-4ec9-95ff-50acbf609ff5', all_alerts[0].view_id) + self.assertEqual('ENDANGERED SAFARI', all_alerts[0].view_name) + self.assertEqual('6d13b0ca-043d-4d42-8c9d-3f3313ea3a00', all_alerts[0].workbook_id) + self.assertEqual('Safari stats', all_alerts[0].workbook_name) + self.assertEqual('5241e88d-d384-4fd7-9c2f-648b5247efc5', all_alerts[0].project_id) + self.assertEqual('Default', all_alerts[0].project_name) + + def test_get_by_id(self): + response_xml = read_xml_asset(GET_BY_ID_XML) + with requests_mock.mock() as m: + m.get(self.baseurl + '/5ea59b45-e497-5673-8809-bfe213236f75', text=response_xml) + alert = self.server.data_alerts.get_by_id('5ea59b45-e497-5673-8809-bfe213236f75') + + self.assertTrue(isinstance(alert.recipients, list)) + self.assertEqual(len(alert.recipients), 1) + self.assertEqual(alert.recipients[0], 'dd2239f6-ddf1-4107-981a-4cf94e415794') + + def test_update(self): + response_xml = read_xml_asset(UPDATE_XML) + with requests_mock.mock() as m: + m.put(self.baseurl + '/5ea59b45-e497-5673-8809-bfe213236f75', text=response_xml) + single_alert = TSC.DataAlertItem() + single_alert._id = '5ea59b45-e497-5673-8809-bfe213236f75' + single_alert._subject = 'Data Alert test' + single_alert._frequency = 'Daily' + single_alert._public = "true" + single_alert._owner_id = "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" + single_alert = self.server.data_alerts.update(single_alert) + + self.assertEqual('5ea59b45-e497-5673-8809-bfe213236f75', single_alert.id) + self.assertEqual('Data Alert test', single_alert.subject) + self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', single_alert.creatorId) + self.assertEqual('2020-08-10T23:17:06Z', single_alert.createdAt) + self.assertEqual('2020-08-10T23:17:06Z', single_alert.updatedAt) + self.assertEqual('Daily', single_alert.frequency) + self.assertEqual('true', single_alert.public) + self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', single_alert.owner_id) + self.assertEqual('Bob', single_alert.owner_name) + self.assertEqual('d79634e1-6063-4ec9-95ff-50acbf609ff5', single_alert.view_id) + self.assertEqual('ENDANGERED SAFARI', single_alert.view_name) + self.assertEqual('6d13b0ca-043d-4d42-8c9d-3f3313ea3a00', single_alert.workbook_id) + self.assertEqual('Safari stats', single_alert.workbook_name) + self.assertEqual('5241e88d-d384-4fd7-9c2f-648b5247efc5', single_alert.project_id) + self.assertEqual('Default', single_alert.project_name) + + def test_add_user_to_alert(self): + response_xml = read_xml_asset(ADD_USER_TO_ALERT) + single_alert = TSC.DataAlertItem() + single_alert._id = '0448d2ed-590d-4fa0-b272-a2a8a24555b5' + in_user = TSC.UserItem('Bob', TSC.UserItem.Roles.Explorer) + in_user._id = '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7' + + with requests_mock.mock() as m: + m.post(self.baseurl + '/0448d2ed-590d-4fa0-b272-a2a8a24555b5/users', text=response_xml) + + out_user = self.server.data_alerts.add_user_to_alert(single_alert, in_user) + + self.assertEqual(out_user.id, in_user.id) + self.assertEqual(out_user.name, in_user.name) + self.assertEqual(out_user.site_role, in_user.site_role) + + def test_delete(self): + with requests_mock.mock() as m: + m.delete(self.baseurl + '/0448d2ed-590d-4fa0-b272-a2a8a24555b5', status_code=204) + self.server.data_alerts.delete('0448d2ed-590d-4fa0-b272-a2a8a24555b5') + + def test_delete_user_from_alert(self): + alert_id = '5ea59b45-e497-5673-8809-bfe213236f75' + user_id = '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7' + with requests_mock.mock() as m: + m.delete(self.baseurl + '/{0}/users/{1}'.format(alert_id, user_id), status_code=204) + self.server.data_alerts.delete_user_from_alert(alert_id, user_id)