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
Adding Schedules Support Round 2 #48
Changes from 15 commits
03e8ce3
312a1c7
b32a632
0adb03f
66e1793
0342bed
bb9ff8f
34457b3
f4490fb
79271a1
b710d9d
46cc1d0
fd17154
371b6f9
28ebb9e
c330687
d7add3a
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,78 @@ | ||
#### | ||
# This script demonstrates how to create schedules using the Tableau | ||
# Server Client. | ||
# | ||
# To run the script, you must have installed Python 2.7.9 or later. | ||
#### | ||
|
||
|
||
import argparse | ||
import getpass | ||
import logging | ||
|
||
from datetime import time | ||
|
||
import tableauserverclient as TSC | ||
|
||
|
||
def main(): | ||
|
||
parser = argparse.ArgumentParser(description='Creates sample schedules for each type of frequency.') | ||
parser.add_argument('--server', '-s', required=True, help='server address') | ||
parser.add_argument('--username', '-u', required=True, help='username to sign into server') | ||
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error', | ||
help='desired logging level (set to error by default)') | ||
args = parser.parse_args() | ||
|
||
password = getpass.getpass("Password: ") | ||
|
||
# Set logging level based on user input, or error by default | ||
logging_level = getattr(logging, args.logging_level.upper()) | ||
logging.basicConfig(level=logging_level) | ||
|
||
tableau_auth = TSC.TableauAuth(args.username, password) | ||
server = TSC.Server(args.server) | ||
with server.auth.sign_in(tableau_auth): | ||
# Hourly Schedule | ||
# This schedule will run every 2 hours between 2:30AM and 11:00PM | ||
hourly_interval = TSC.HourlyInterval(start_time=time(2, 30), | ||
end_time=time(23, 0), | ||
interval_occurrence=TSC.IntervalItem.Occurrence.Hours, | ||
interval_value=2) | ||
|
||
hourly_schedule = TSC.ScheduleItem("Hourly-Schedule", 50, TSC.ScheduleItem.Type.Extract, | ||
TSC.ScheduleItem.ExecutionOrder.Parallel, hourly_interval) | ||
hourly_schedule = server.schedules.create(hourly_schedule) | ||
print("Hourly schedule created (ID: {}).".format(hourly_schedule.id)) | ||
|
||
# Daily Schedule | ||
# This schedule will run every day at 5AM | ||
daily_interval = TSC.DailyInterval(start_time=time(5)) | ||
daily_schedule = TSC.ScheduleItem("Daily-Schedule", 60, TSC.ScheduleItem.Type.Subscription, | ||
TSC.ScheduleItem.ExecutionOrder.Serial, daily_interval) | ||
daily_schedule = server.schedules.create(daily_schedule) | ||
print("Daily schedule created (ID: {}).".format(daily_schedule.id)) | ||
|
||
# Weekly Schedule | ||
# This schedule will wun every Monday, Wednesday, and Friday at 7:15PM | ||
weekly_interval = TSC.WeeklyInterval(time(19, 15), | ||
TSC.IntervalItem.Day.Monday, | ||
TSC.IntervalItem.Day.Wednesday, | ||
TSC.IntervalItem.Day.Friday) | ||
weekly_schedule = TSC.ScheduleItem("Weekly-Schedule", 70, TSC.ScheduleItem.Type.Extract, | ||
TSC.ScheduleItem.ExecutionOrder.Serial, weekly_interval) | ||
weekly_schedule = server.schedules.create(weekly_schedule) | ||
print("Weekly schedule created (ID: {}).".format(weekly_schedule.id)) | ||
|
||
# Monthly Schedule | ||
# This schedule will run on the 15th of every month at 11:30PM | ||
monthly_interval = TSC.MonthlyInterval(start_time=time(23, 30), | ||
interval_value=15) | ||
monthly_schedule = TSC.ScheduleItem("Monthly-Schedule", 80, TSC.ScheduleItem.Type.Subscription, | ||
TSC.ScheduleItem.ExecutionOrder.Parallel, monthly_interval) | ||
monthly_schedule = server.schedules.create(monthly_schedule) | ||
print("Monthly schedule created (ID: {}).".format(monthly_schedule.id)) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
from .property_decorators import property_is_valid_time, property_not_nullable | ||
|
||
|
||
class IntervalItem(object): | ||
class Frequency: | ||
Hourly = "Hourly" | ||
Daily = "Daily" | ||
Weekly = "Weekly" | ||
Monthly = "Monthly" | ||
|
||
class Occurrence: | ||
Minutes = "minutes" | ||
Hours = "hours" | ||
WeekDay = "weekDay" | ||
MonthDay = "monthDay" | ||
|
||
class Day: | ||
Sunday = "Sunday" | ||
Monday = "Monday" | ||
Tuesday = "Tuesday" | ||
Wednesday = "Wednesday" | ||
Thursday = "Thursday" | ||
Friday = "Friday" | ||
Saturday = "Saturday" | ||
LastDay = "LastDay" | ||
|
||
|
||
class HourlyInterval(object): | ||
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 properties should have an decorators if they cannot be none / empty |
||
def __init__(self, start_time, end_time, interval_value): | ||
|
||
self.start_time = start_time | ||
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. For other code, it seems we start by setting the internal variable to None before calling the setter. Any reason for using that pattern versus not? 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. This feels cleaner to me because it hide the I then don't need to have a bunch of these in
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. Russell? |
||
self.end_time = end_time | ||
self._frequency = IntervalItem.Frequency.Hourly | ||
self.interval = interval_value | ||
|
||
@property | ||
def start_time(self): | ||
return self._start_time | ||
|
||
@start_time.setter | ||
@property_is_valid_time | ||
@property_not_nullable | ||
def start_time(self, value): | ||
self._start_time = value | ||
|
||
@property | ||
def end_time(self): | ||
return self._end_time | ||
|
||
@end_time.setter | ||
@property_is_valid_time | ||
@property_not_nullable | ||
def end_time(self, value): | ||
self._end_time = value | ||
|
||
@property | ||
def interval(self): | ||
return self._interval | ||
|
||
@interval.setter | ||
def interval(self, interval): | ||
VALID_INTERVALS = {.25, .5, 1, 2, 4, 6, 8, 12} | ||
|
||
if float(interval) not in VALID_INTERVALS: | ||
error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS)) | ||
raise ValueError(error) | ||
|
||
# We use fractional hours for the two minute-based intervals. | ||
# Need to convert to minutes from hours here | ||
if interval in {.25, .5}: | ||
interval_value = int(interval * 60) | ||
interval_occurrence = IntervalItem.Occurrence.Minutes | ||
else: | ||
interval_value = interval | ||
interval_occurrence = IntervalItem.Occurrence.Hours | ||
|
||
self._interval = [(interval_occurrence, str(interval_value))] | ||
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. How do I read the interval? I expect to be able to read the value I set or one that was returned from a "get" call to the server. 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. Intervals are tuples of (Occurence, Value), so in this case it would return
It's consistent across the different interval values -- which is how the serializer wants it:
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. I want to return what we set. We have made the "Occurence" disappear. Why are we putting it back? if I write: x should equal .25 We can make the serialization work without exposing this to users. 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. I think that's going to be a fairly large change. We'd need a serializer for each IntervalType maybe? Or... I'd have to think more about it, when it's not so late. 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. We could do it in the serializer or you can have an internal function on each of them which returns this pair. We can call internal functions from our serializer ... this is already a pattern we use. But the value I set for interval should be the value I get back for it (and I should be able to directly set it, ie not just in the constructor). 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. I like the latter
One-line change in the serializer, and then the internal function is easy to write as well. |
||
|
||
|
||
class DailyInterval(object): | ||
def __init__(self, start_time): | ||
self.start_time = start_time | ||
self._frequency = IntervalItem.Frequency.Daily | ||
self.end_time = 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. why do you have this (besides the issue with serialization)? I would rather have the serializer clean it than have properties which are meaningless but visible. 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. Are you talking about the Yeah, that was so I didn't have to re-write the serializer, which I consider to be the riskiest part. We'd have to add a bunch of |
||
self.interval = 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 for this |
||
|
||
@property | ||
def start_time(self): | ||
return self._start_time | ||
|
||
@start_time.setter | ||
@property_is_valid_time | ||
@property_not_nullable | ||
def start_time(self, value): | ||
self._start_time = value | ||
|
||
|
||
class WeeklyInterval(object): | ||
def __init__(self, start_time, *interval_values): | ||
self.start_time = start_time | ||
self._frequency = IntervalItem.Frequency.Weekly | ||
self.interval = interval_values | ||
self.end_time = 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 |
||
|
||
@property | ||
def start_time(self): | ||
return self._start_time | ||
|
||
@start_time.setter | ||
@property_is_valid_time | ||
@property_not_nullable | ||
def start_time(self, value): | ||
self._start_time = value | ||
|
||
@property | ||
def interval(self): | ||
return self._interval | ||
|
||
@interval.setter | ||
def interval(self, interval_values): | ||
if not all(hasattr(IntervalItem.Day, day) for day in interval_values): | ||
raise ValueError("Invalid week day defined " + str(interval_values)) | ||
|
||
self._interval = [(IntervalItem.Occurrence.WeekDay, day) for day in interval_values] | ||
|
||
|
||
class MonthlyInterval(object): | ||
def __init__(self, start_time, interval_value): | ||
self.start_time = start_time | ||
self._frequency = IntervalItem.Frequency.Monthly | ||
self.interval = str(interval_value) | ||
self.end_time = 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. And here. No? 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. Done, you're right, it doesn't look that bad |
||
|
||
@property | ||
def start_time(self): | ||
return self._start_time | ||
|
||
@start_time.setter | ||
@property_is_valid_time | ||
@property_not_nullable | ||
def start_time(self, value): | ||
self._start_time = value | ||
|
||
@property | ||
def interval(self): | ||
return self._interval | ||
|
||
@interval.setter | ||
def interval(self, interval_value): | ||
error = "Invalid interval value for a monthly frequency: {}.".format(interval_value) | ||
|
||
# This is weird because the value could be a str or an int | ||
# The only valid str is 'LastDay' so we check that first. If that's not it | ||
# try to convert it to an int, if that fails because it's an incorrect string | ||
# like 'badstring' we catch and re-raise. Otherwise we convert to int and check | ||
# that it's in range 1-31 | ||
|
||
if interval_value != "LastDay": | ||
try: | ||
if not (1 <= int(interval_value) <= 31): | ||
raise ValueError(error) | ||
except ValueError as e: | ||
if interval_value != "LastDay": | ||
raise ValueError(error) | ||
|
||
self._interval = [(IntervalItem.Occurrence.MonthDay, str(interval_value))] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,3 +46,34 @@ def wrapper(self, value): | |
return func(self, value) | ||
|
||
return wrapper | ||
|
||
|
||
def property_is_valid_time(func): | ||
@wraps(func) | ||
def wrapper(self, value): | ||
units_of_time = {"hour", "minute", "second"} | ||
|
||
if not any(hasattr(value, unit) for unit in units_of_time): | ||
error = "Invalid time object defined." | ||
raise ValueError(error) | ||
return func(self, value) | ||
|
||
return wrapper | ||
|
||
|
||
def property_is_int(min, max=2**31): | ||
def property_type_decorator(func): | ||
@wraps(func) | ||
def wrapper(self, value): | ||
if min is None: | ||
return func(self, value) | ||
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. I don't get this? Why can't I set the min to None and the max to some number (like 100). Not saying you are doing this right now but this code would break. Just do the if "min is not None and value < min" and similarly "if max is not None and value > max". No? 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. I could enable that, but then how would we say "this should be an int but not checked within a range"
I could:
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. I am fine with accepting a pair using range=(None,100). It is more readable. I am good with that. 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. Done.
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. Okay. I am fine with this. If it is ever actually a need, we can debate a case with only a max or only a min but we have no case so I am not going to debate just for the sake of debating. I like the syntax. Thanks! |
||
|
||
if value < min or value > max: | ||
error = "Invalid priority defined: {}.".format(value) | ||
raise ValueError(error) | ||
|
||
return func(self, value) | ||
|
||
return wrapper | ||
|
||
return property_type_decorator |
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.
Minutes should come before hours.