-
Notifications
You must be signed in to change notification settings - Fork 421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
added support for schedules #30
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import xml.etree.ElementTree as ET | ||
from .. import NAMESPACE | ||
|
||
|
||
class ScheduleItem(object): | ||
def __init__(self): | ||
self._created_at = None | ||
self._end_schedule_at = None | ||
self._frequency = None | ||
self._id = None | ||
self._name = None | ||
self._next_run_at = None | ||
self._priority = None | ||
self._state = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here (what is state) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the schedule is 'on' or not. "Active" or "Suspended" |
||
self._type = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is type. This doesn't feel like it should be just an arbitrary string There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Extract" or "Subscription" -- maybe more in the future |
||
self._updated_at = None | ||
|
||
@property | ||
def created_at(self): | ||
return self._created_at | ||
|
||
@property | ||
def end_schedule_at(self): | ||
return self._end_schedule_at | ||
|
||
@property | ||
def frequency(self): | ||
return self._frequency | ||
|
||
@property | ||
def id(self): | ||
return self._id | ||
|
||
@property | ||
def name(self): | ||
return self._name | ||
|
||
@property | ||
def next_run_at(self): | ||
return self._next_run_at | ||
|
||
@property | ||
def priority(self): | ||
return self._priority | ||
|
||
@property | ||
def state(self): | ||
return self._state | ||
|
||
@property | ||
def type(self): | ||
return self._type | ||
|
||
@property | ||
def updated_at(self): | ||
return self._updated_at | ||
|
||
@classmethod | ||
def from_response(cls, resp): | ||
all_schedule_items = list() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: We seem to use the literal in most places There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
parsed_response = ET.fromstring(resp) | ||
all_schedule_xml = parsed_response.findall('.//t:schedule', namespaces=NAMESPACE) | ||
for schedule_xml in all_schedule_xml: | ||
schedule_item = cls() | ||
schedule_item._id = schedule_xml.get('id', None) | ||
schedule_item._name = schedule_xml.get('name', None) | ||
schedule_item._state = schedule_xml.get('state', None) | ||
schedule_item._created_at = schedule_xml.get('createdAt', None) | ||
schedule_item._updated_at = schedule_xml.get('updatedAt', None) | ||
schedule_item._type = schedule_xml.get('type', None) | ||
schedule_item._frequency = schedule_xml.get('frequency', None) | ||
schedule_item._next_run_at = schedule_xml.get('nextRunAt', None) | ||
schedule_item._end_schedule_at = schedule_xml.get('endScheduleAt', None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this supposed to be a date? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a date in the format "YYYY-MM-DDTHH:MM:SSZ" Looks like the python strptime string would be We could make these native datetime objects, that seems reasonable |
||
|
||
priority = schedule_xml.get('priority', None) | ||
if priority: | ||
schedule_item._priority = int(priority) | ||
all_schedule_items.append(schedule_item) | ||
return all_schedule_items |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from .endpoint import Endpoint | ||
from .. import PaginationItem, ScheduleItem | ||
import logging | ||
|
||
logger = logging.getLogger('tableau.endpoint.schedules') | ||
|
||
|
||
class Schedules(Endpoint): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can't update a schedule right now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No :'( |
||
def __init__(self, parent_srv): | ||
super(Endpoint, self).__init__() | ||
self.baseurl = "{0}/schedules" | ||
self.parent_srv = parent_srv | ||
|
||
def _construct_url(self): | ||
return self.baseurl.format(self.parent_srv.baseurl) | ||
|
||
def get(self, req_options=None): | ||
logger.info("Querying all schedules") | ||
url = self._construct_url() | ||
server_response = self.get_request(url, req_options) | ||
pagination_item = PaginationItem.from_response(server_response.content) | ||
all_schedule_items = ScheduleItem.from_response(server_response.content) | ||
return all_schedule_items, pagination_item | ||
|
||
def delete(self, schedule_id): | ||
if not schedule_id: | ||
error = "Schedule ID undefined" | ||
raise ValueError(error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The exception should get logged as an error -- or do we not do that on other endpoints? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed the XSD schema only allows nonNegInts |
||
url = "{0}/{1}".format(self._construct_url(), schedule_id) | ||
self.delete_request(url) | ||
logger.info("Deleted single schedule (ID: {0})".format(schedule_id)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?xml version='1.0' encoding='UTF-8'?> | ||
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd"> | ||
<pagination pageNumber="1" pageSize="100" totalAvailable="2" /> | ||
<schedules> | ||
<schedule id="c9cff7f9-309c-4361-99ff-d4ba8c9f5467" name="Weekday early mornings" state="Active" priority="50" createdAt="2016-07-06T20:19:00Z" updatedAt="2016-09-13T11:00:32Z" type="Extract" frequency="Weekly" nextRunAt="2016-09-14T11:00:00Z" /> | ||
<schedule id="bcb79d07-6e47-472f-8a65-d7f51f40c36c" name="Saturday night" state="Active" priority="80" createdAt="2016-07-07T20:19:00Z" updatedAt="2016-09-12T16:39:38Z" type="Subscription" frequency="Weekly" nextRunAt="2016-09-18T06:00:00Z" /> | ||
</schedules> | ||
</tsResponse> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<?xml version='1.0' encoding='UTF-8'?> | ||
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd"> | ||
<pagination pageNumber="1" pageSize="100" totalAvailable="0" /> | ||
<schedules /> | ||
</tsResponse> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import unittest | ||
import os | ||
import requests_mock | ||
import tableauserverclient as TSC | ||
|
||
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") | ||
|
||
GET_XML = os.path.join(TEST_ASSET_DIR, "schedule_get.xml") | ||
GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_empty.xml") | ||
|
||
|
||
class ScheduleTests(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.baseurl = self.server.schedules._construct_url() | ||
|
||
def test_get(self): | ||
with open(GET_XML, "rb") as f: | ||
response_xml = f.read().decode("utf-8") | ||
with requests_mock.mock() as m: | ||
m.get(self.baseurl, text=response_xml) | ||
all_schedules, pagination_item = self.server.schedules.get() | ||
|
||
self.assertEqual(2, pagination_item.total_available) | ||
self.assertEqual("c9cff7f9-309c-4361-99ff-d4ba8c9f5467", all_schedules[0].id) | ||
self.assertEqual("Weekday early mornings", all_schedules[0].name) | ||
self.assertEqual("Active", all_schedules[0].state) | ||
self.assertEqual(50, all_schedules[0].priority) | ||
self.assertEqual("2016-07-06T20:19:00Z", all_schedules[0].created_at) | ||
self.assertEqual("2016-09-13T11:00:32Z", all_schedules[0].updated_at) | ||
self.assertEqual("Extract", all_schedules[0].type) | ||
self.assertEqual("Weekly", all_schedules[0].frequency) | ||
self.assertEqual("2016-09-14T11:00:00Z", all_schedules[0].next_run_at) | ||
|
||
self.assertEqual("bcb79d07-6e47-472f-8a65-d7f51f40c36c", all_schedules[1].id) | ||
self.assertEqual("Saturday night", all_schedules[1].name) | ||
self.assertEqual("Active", all_schedules[1].state) | ||
self.assertEqual(80, all_schedules[1].priority) | ||
self.assertEqual("2016-07-07T20:19:00Z", all_schedules[1].created_at) | ||
self.assertEqual("2016-09-12T16:39:38Z", all_schedules[1].updated_at) | ||
self.assertEqual("Subscription", all_schedules[1].type) | ||
self.assertEqual("Weekly", all_schedules[1].frequency) | ||
self.assertEqual("2016-09-18T06:00:00Z", all_schedules[1].next_run_at) | ||
|
||
def test_get_empty(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we test this for every content type? I'm not sure it's a super valuable test but it's harmless. Is it just testing that the Schedule.from_response doesn't choke on an empty input? If that's the case the mocked request might be unnecessary. This is minor though, I bet this follows the other test's and that's cool. |
||
with open(GET_EMPTY_XML, "rb") as f: | ||
response_xml = f.read().decode("utf-8") | ||
with requests_mock.mock() as m: | ||
m.get(self.baseurl, text=response_xml) | ||
all_schedules, pagination_item = self.server.schedules.get() | ||
|
||
self.assertEqual(0, pagination_item.total_available) | ||
self.assertEqual([], all_schedules) | ||
|
||
def test_delete(self): | ||
with requests_mock.mock() as m: | ||
m.delete(self.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", status_code=204) | ||
self.server.schedules.delete("c9cff7f9-309c-4361-99ff-d4ba8c9f5467") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here (what is frequency)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Frequency is a string (or, I guess, an enum of a few potential values)
"Weekly", "Daily", "Hourly" there might be another one