Skip to content

Commit

Permalink
Initial implementation to address #102 and provide datetime objects
Browse files Browse the repository at this point in the history
  • Loading branch information
Russell Hay committed Nov 16, 2016
1 parent 39f4b22 commit c86992b
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 9 deletions.
37 changes: 37 additions & 0 deletions tableauserverclient/datetime_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import datetime

try:
from pytz import utc
except ImportError:
# If pytz is not installed, let's polyfill a UTC timezone so it all just works
# This code below is from the python documentation for tzinfo: https://docs.python.org/2.3/lib/datetime-tzinfo.html
ZERO = datetime.timedelta(0)
HOUR = datetime.timedelta(hours=1)


# A UTC class.

class UTC(datetime.tzinfo):
"""UTC"""

def utcoffset(self, dt):
return ZERO

def tzname(self, dt):
return "UTC"

def dst(self, dt):
return ZERO


utc = UTC()

TABLEAU_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"


def parse_datetime(date):
return datetime.datetime.strptime(date, TABLEAU_DATE_FORMAT).replace(tzinfo=utc)


def format_datetime(date):
return date.astimezone(tz=utc).strftime(TABLEAU_DATE_FORMAT)
7 changes: 6 additions & 1 deletion tableauserverclient/models/datasource_item.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_not_nullable
from .property_decorators import property_not_nullable, property_is_datetime
from .tag_item import TagItem
from .. import NAMESPACE

Expand Down Expand Up @@ -34,6 +34,11 @@ def content_url(self):
def created_at(self):
return self._created_at

@created_at.setter
@property_is_datetime
def created_at(self, value):
self._created_at = value

@property
def id(self):
return self._id
Expand Down
29 changes: 29 additions & 0 deletions tableauserverclient/models/property_decorators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import datetime
import re
from functools import wraps
from ..datetime_helpers import parse_datetime
try:
basestring
except NameError:
# In case we are in python 3 the string check is different
basestring = str


def property_is_enum(enum_type):
Expand Down Expand Up @@ -99,3 +106,25 @@ def validate_regex_decorator(self, value):
return func(self, value)
return validate_regex_decorator
return wrapper


def property_is_datetime(func):
""" Takes the following datetime format and turns it into a datetime object:
2016-08-18T18:25:36Z
Because we return everything with Z as the timezone, we assume everything is in UTC and create
a timezone aware datetime.
"""

@wraps(func)
def wrapper(self, value):
if isinstance(value, datetime.datetime):
return func(self, value)
if not isinstance(value, basestring):
raise ValueError("Cannot convert {} into a datetime, cannot update {}".format(value.__class__.__name__,
func.__name__))

dt = parse_datetime(value)
return func(self, dt)
return wrapper
12 changes: 11 additions & 1 deletion tableauserverclient/models/schedule_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime

from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval
from .property_decorators import property_is_enum, property_not_nullable, property_is_int
from .property_decorators import property_is_enum, property_not_nullable, property_is_int, property_is_datetime
from .. import NAMESPACE


Expand Down Expand Up @@ -36,6 +36,11 @@ def __init__(self, name, priority, schedule_type, execution_order, interval_item
def created_at(self):
return self._created_at

@created_at.setter
@property_is_datetime
def created_at(self, value):
self._created_at = value

@property
def end_schedule_at(self):
return self._end_schedule_at
Expand Down Expand Up @@ -98,6 +103,11 @@ def state(self, value):
def updated_at(self):
return self._updated_at

@updated_at.setter
@property_is_datetime
def updated_at(self, value):
self._updated_at = value

def _parse_common_tags(self, schedule_xml):
if not isinstance(schedule_xml, ET.Element):
schedule_xml = ET.fromstring(schedule_xml).find('.//t:schedule', namespaces=NAMESPACE)
Expand Down
12 changes: 11 additions & 1 deletion tableauserverclient/models/workbook_item.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_not_nullable, property_is_boolean
from .property_decorators import property_not_nullable, property_is_boolean, property_is_datetime
from .tag_item import TagItem
from .view_item import ViewItem
from .. import NAMESPACE
Expand Down Expand Up @@ -40,6 +40,11 @@ def content_url(self):
def created_at(self):
return self._created_at

@created_at.setter
@property_is_datetime
def created_at(self, value):
self._created_at = value

@property
def id(self):
return self._id
Expand Down Expand Up @@ -81,6 +86,11 @@ def size(self):
def updated_at(self):
return self._updated_at

@updated_at.setter
@property_is_datetime
def updated_at(self, value):
self._updated_at = value

@property
def views(self):
if self._views is None:
Expand Down
1 change: 1 addition & 0 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ..datetime_helpers import format_datetime
import xml.etree.ElementTree as ET

from requests.packages.urllib3.fields import RequestField
Expand Down
32 changes: 32 additions & 0 deletions test/test_datasource_model.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import unittest
import tableauserverclient as TSC

Expand All @@ -8,3 +9,34 @@ def test_invalid_project_id(self):
datasource = TSC.DatasourceItem("10")
with self.assertRaises(ValueError):
datasource.project_id = None

def test_datetime_conversion(self):
datasource = TSC.DatasourceItem("10")
datasource.created_at = "2016-08-18T19:25:36Z"
actual = datasource.created_at
self.assertIsInstance(actual, datetime.datetime)
self.assertEquals(actual.year, 2016)
self.assertEquals(actual.month, 8)
self.assertEquals(actual.day, 18)
self.assertEquals(actual.hour, 19)
self.assertEquals(actual.minute, 25)
self.assertEquals(actual.second, 36)

def test_datetime_conversion_allows_datetime_passthrough(self):
datasource = TSC.DatasourceItem("10")
now = datetime.datetime.utcnow()
datasource.created_at = now
self.assertEquals(datasource.created_at, now)

def test_datetime_conversion_is_timezone_aware(self):
datasource = TSC.DatasourceItem("10")
datasource.created_at = "2016-08-18T19:25:36Z"
actual = datasource.created_at
self.assertEquals(actual.utcoffset().seconds, 0)

def test_datetime_conversion_rejects_things_that_cannot_be_converted(self):
datasource = TSC.DatasourceItem("10")
with self.assertRaises(ValueError):
datasource.created_at = object()
with self.assertRaises(ValueError):
datasource.created_at = "This is so not a datetime"
12 changes: 6 additions & 6 deletions test/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ def test_make_get_request(self):
auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM',
content_type='text/xml')

self.assertEquals(resp.request.query, 'pagenumber=13&pagesize=13')
self.assertEquals(resp.request.headers['x-tableau-auth'], 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM')
self.assertEquals(resp.request.headers['content-type'], 'text/xml')
self.assertEqual(resp.request.query, 'pagenumber=13&pagesize=13')
self.assertEqual(resp.request.headers['x-tableau-auth'], 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM')
self.assertEqual(resp.request.headers['content-type'], 'text/xml')

def test_make_post_request(self):
with requests_mock.mock() as m:
Expand All @@ -42,6 +42,6 @@ def test_make_post_request(self):
request_object=None,
auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM',
content_type='multipart/mixed')
self.assertEquals(resp.request.headers['x-tableau-auth'], 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM')
self.assertEquals(resp.request.headers['content-type'], 'multipart/mixed')
self.assertEquals(resp.request.body, b'1337')
self.assertEqual(resp.request.headers['x-tableau-auth'], 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM')
self.assertEqual(resp.request.headers['content-type'], 'multipart/mixed')
self.assertEqual(resp.request.body, b'1337')

0 comments on commit c86992b

Please sign in to comment.