10000 Initial implementation to address #102 and provide datetime objects · rickyren/server-client-python@2c8d1a5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2c8d1a5

Browse files
author
Russell Hay
committed
Initial implementation to address tableau#102 and provide datetime objects
1 parent 8b5c7b1 commit 2c8d1a5

File tree

8 files changed

+133
-9
lines changed

8 files changed

+133
-9
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import datetime
2+
3+
try:
4+
from pytz import utc
5+
except ImportError:
6+
# If pytz is not installed, let's polyfill a UTC timezone so it all just works
7+
# This code below is from the python documentation for tzinfo: https://docs.python.org/2.3/lib/datetime-tzinfo.html
8+
ZERO = datetime.timedelta(0)
9+
HOUR = datetime.timedelta(hours=1)
10+
11+
12+
# A UTC class.
13+
14+
class UTC(datetime.tzinfo):
15+
"""UTC"""
16+
17+
def utcoffset(self, dt):
18+
return ZERO
19+
20+
def tzname(self, dt):
21+
return "UTC"
22+
23+
def dst(self, dt):
24+
return ZERO
25+
26+
27+
utc = UTC()
28+
29+
TABLEAU_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
30+
31+
32+
def parse_datetime(date):
33+
return datetime.datetime.strptime(date, TABLEAU_DATE_FORMAT).replace(tzinfo=utc)
34+
35+
36+
def format_datetime(date):
37+
return date.astimezone(tz=utc).strftime(TABLEAU_DATE_FORMAT)

tableauserverclient/models/datasource_item.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import xml.etree.ElementTree as ET
22
from .exceptions import UnpopulatedPropertyError
3-
from .property_decorators import property_not_nullable
3+
from .property_decorators import property_not_nullable, property_is_datetime
44
from .tag_item import TagItem
55
from .. import NAMESPACE
66

@@ -34,6 +34,11 @@ def content_url(self):
3434
def created_at(self):
3535
return self._created_at
3636

37+
@created_at.setter
38+
@property_is_datetime
39+
def created_at(self, value):
40+
self._created_at = value
41+
3742
@property
3843
def id(self):
3944
return self._id

tableauserverclient/models/property_decorators.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import datetime
12
import re
23
from functools import wraps
4+
from ..datetime_helpers import parse_datetime
5+
try:
6+
basestring
7+
except NameError:
8+
# In case we are in python 3 the string check is different
9+
basestring = str
310

411

512
def property_is_enum(enum_type):
@@ -99,3 +106,25 @@ def validate_regex_decorator(self, value):
99106
return func(self, value)
100107
return validate_regex_decorator
101108
return wrapper
109+
110+
111+
def property_is_datetime(func):
112+
""" Takes the following datetime format and turns it into a datetime object:
113+
114+
2016-08-18T18:25:36Z
115+
116+
Because we return everything with Z as the timezone, we assume everything is in UTC and create
117+
a timezone aware datetime.
118+
"""
119+
120+
@wraps(func)
121+
def wrapper(self, value):
122+
if isinstance(value, datetime.datetime):
123+
return func(self, value)
124+
if not isinstance(value, basestring):
125+
raise ValueError("Cannot convert {} into a datetime, cannot update {}".format(value.__class__.__name__,
126+
func.__name__))
127+
128+
dt = parse_datetime(value)
129+
return func(self, dt)
130+
return wrapper

tableauserverclient/models/schedule_item.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from datetime import datetime
33

44
from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval
5-
from .property_decorators import property_is_enum, property_not_nullable, property_is_int
5+
from .property_decorators import property_is_enum, property_not_nullable, property_is_int, property_is_datetime
66
from .. import NAMESPACE
77

88

@@ -36,6 +36,11 @@ def __init__(self, name, priority, schedule_type, execution_order, interval_item
3636
def created_at(self):
3737
return self._created_at
3838

39+
@created_at.setter
40+
@property_is_datetime
41+
def created_at(self, value):
42+
self._created_at = value
43+
3944
@property
4045
def end_schedule_at(self):
4146
return self._end_schedule_at
@@ -98,6 +103,11 @@ def state(self, value):
98103
def updated_at(self):
99104
return self._updated_at
100105

106+
@updated_at.setter
107+
@property_is_datetime
108+
def updated_at(self, value):
109+
self._updated_at = value
110+
101111
def _parse_common_tags(self, schedule_xml):
102112
if not isinstance(schedule_xml, ET.Element):
103113
schedule_xml = ET.fromstring(schedule_xml).find('.//t:schedule', namespaces=NAMESPACE)

tableauserverclient/models/workbook_item.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import xml.etree.ElementTree as ET
22
from .exceptions import UnpopulatedPropertyError
3-
from .property_decorators import property_not_nullable, property_is_boolean
3+
from .property_decorators import property_not_nullable, property_is_boolean, property_is_datetime
44
from .tag_item import TagItem
55
from .view_item import ViewItem
66
from .. import NAMESPACE
@@ -40,6 +40,11 @@ def content_url(self):
4040
def created_at(self):
4141
return self._created_at
4242

43+
@created_at.setter
44+
@property_is_datetime
45+
def created_at(self, value):
46+
self._created_at = value
47+
4348
@property
4449
def id(self):
4550
return self._id
@@ -81,6 +86,11 @@ def size(self):
8186
def updated_at(self):
8287
return self._updated_at
8388

89+
@updated_at.setter
90+
@property_is_datetime
91+
def updated_at(self, value):
92+
self._updated_at = value
93+
8494
@property
8595
def views(self):
8696
if self._views is None:

tableauserverclient/server/request_factory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ..datetime_helpers import format_datetime
12
import xml.etree.ElementTree as ET
23

34
from requests.packages.urllib3.fields import RequestField

test/test_datasource_model.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import unittest
23
import tableauserverclient as TSC
34

@@ -8,3 +9,34 @@ def test_invalid_project_id(self):
89
datasource = TSC.DatasourceItem("10")
910
with self.assertRaises(ValueError):
1011
datasource.project_id = None
12+
13+
def test_datetime_conversion(self):
14+
datasource = TSC.DatasourceItem("10")
15+
datasource.created_at = "2016-08-18T19:25:36Z"
16+
actual = datasource.created_at
17+
self.assertIsInstance(actual, datetime.datetime)
18+
self.assertEquals(actual.year, 2016)
19+
self.assertEquals(actual.month, 8)
20+
self.assertEquals(actual.day, 18)
21+
self.assertEquals(actual.hour, 19)
22+
self.assertEquals(actual.minute, 25)
23+
self.assertEquals(actual.second, 36)
24+
25+
def test_datetime_conversion_allows_datetime_passthrough(self):
26+
datasource = TSC.DatasourceItem("10")
27+
now = datetime.datetime.utcnow()
28+
datasource.created_at = now
29+
self.assertEquals(datasource.created_at, now)
30+
31+
def test_datetime_conversion_is_timezone_aware(self):
32+
datasource = TSC.DatasourceItem("10")
33+
datasource.created_at = "2016-08-18T19:25:36Z"
34+
actual = datasource.created_at
35+
self.assertEquals(actual.utcoffset().seconds, 0)
36+
37+
def test_datetime_conversion_rejects_things_that_cannot_be_converted(self):
38+
datasource = TSC.DatasourceItem("10")
39+
with self.assertRaises(ValueError):
40+
datasource.created_at = object()
41+
with self.assertRaises(ValueError):
42+
datasource.created_at = "This is so not a datetime"

test/test_requests.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ def test_make_get_request(self):
2828
auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM',
2929
content_type='text/xml')
3030

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

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

0 commit comments

Comments
 (0)
0