10000 Merge pull request #128 from tableau/development · delphinium/server-client-python@8e6c907 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8e6c907

Browse files
author
Russell Hay
authored
Merge pull request tableau#128 from tableau/development
Release 0.3
2 parents 05974c3 + aa56523 commit 8e6c907

33 files changed

+500
-87
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ python:
44
- "3.3"
55
- "3.4"
66
- "3.5"
7+
- "3.6"
78
- "pypy"
89
# command to install dependencies
910
install:
@@ -14,4 +15,4 @@ script:
1415
# Tests
1516
- python setup.py test
1617
# pep8 - disabled for now until we can scrub the files to make sure we pass before turning it on
17-
- pycodestyle .
18+
- pycodestyle tableauserverclient test

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
## 0.3 (11 January 2017)
2+
3+
* Return DateTime objects instead of strings (#102)
4+
* UserItem now is compatible with Pager (#107, #109)
5+
* Deprecated site in favor of site_id (#97)
6+
* Improved handling of large downloads (#105, #111)
7+
* Added support for oAuth when publishing (#117)
8+
* Added Testing against Py36 (#122, #123)
9+
* Added Version Checking to use highest supported REST api version (#100)
10+
* Added Infrastructure for throwing error if trying to do something that is not supported by REST api version (#124)
11+
* Various Code Cleanup
12+
* Added Documentation (#98)
13+
* Improved Test Infrastructure (#91)
14+
115
## 0.2 (02 November 2016)
216

317
* Added Initial Schedules Support (#48)

CONTRIBUTORS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The following people have contributed to this project to make it possible, and w
55
## Contributors
66

77
* [geordielad](https://github.com/geordielad)
8+
* [Hugo Stijns)(https://github.com/hugoboos)
89
* [kovner](https://github.com/kovner)
910

1011

@@ -14,3 +15,5 @@ The following people have contributed to this project to make it possible, and w
1415
* [lgraber](https://github.com/lgraber)
1516
* [t8y8](https://github.com/t8y8)
1617
* [RussTheAerialist](https://github.com/RussTheAerialist)
18+
* [Ben Lower](https://github.com/benlower)
19+
* [Jared Dominguez](https://github.com/jdomingu)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ This repository contains Python source code and sample files.
1111
For more information on installing and using TSC, see the documentation:
1212

1313
<https://tableau.github.io/server-client-python/docs/>
14+

samples/initialize_server.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
####
2+
# This script sets up a server. It uploads datasources and workbooks from the local filesystem.
3+
#
4+
# By default, all content is published to the Default project on the Default site.
5+
####
6+
7+
import tableauserverclient as TSC
8+
import argparse
9+
import getpass
10+
import logging
11+
import glob
12+
13+
14+
def main():
15+
parser = argparse.ArgumentParser(description='Initialize a server with content.')
16+
parser.add_argument('--server', '-s', required=True, help='server address')
17+
parser.add_argument('--datasources-folder', '-df', required=True, help='folder containing datasources')
18+
parser.add_argument('--workbooks-folder', '-wf', required=True, help='folder containing workbooks')
19+
parser.add_argument('--site', '-si', required=False, default='Default', help='site to use')
20+
parser.add_argument('--project', '-p', required=False, default='Default', help='project to use')
21+
parser.add_argument('--username', '-u', required=True, help='username to sign into server')
22+
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
23+
help='desired logging level (set to error by default)')
24+
args = parser.parse_args()
25+
26+
password = getpass.getpass("Password: ")
27+
28+
# Set logging level based on user input, or error by default
29+
logging_level = getattr(logging, args.logging_level.upper())
30+
logging.basicConfig(level=logging_level)
31+
32+
################################################################################
33+
# Step 1: Sign in to server.
34+
################################################################################
35+
tableau_auth = TSC.TableauAuth(args.username, password)
36+
server = TSC.Server(args.server)
37+
38+
with server.auth.sign_in(tableau_auth):
39+
40+
################################################################################
41+
# Step 2: Create the site we need only if it doesn't exist
42+
################################################################################
43+
print("Checking to see if we need to create the site...")
44+
45+
all_sites, _ = server.sites.get()
46+
existing_site = next((s for s in all_sites if s.name == args.site), None)
47+
48+
# Create the site if it doesn't exist
49+
if existing_site is None:
50+
print("Site not found: {0} Creating it...").format(args.site)
51+
new_site = TSC.SiteItem(name=args.site, content_url=args.site.replace(" ", ""),
52+
admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers)
53+
server.sites.create(new_site)
54+
else:
55+
print("Site {0} exists. Moving on...").format(args.site)
56+
57+
################################################################################
58+
# Step 3: Sign-in to our target site
59+
################################################################################
60+
print("Starting our content upload...")
61+
server_upload = TSC.Server(args.server)
62+
tableau_auth.site = args.site
63+
64+
with server_upload.auth.sign_in(tableau_auth):
65+
66+
################################################################################
67+
# Step 4: Create the project we need only if it doesn't exist
68+
################################################################################
69+
all_projects, _ = server_upload.projects.get()
70+
project = next((p for p in all_projects if p.name == args.project), None)
71+
72+
# Create our project if it doesn't exist
73+
if project is None:
74+
print("Project not found: {0} Creating it...").format(args.project)
75+
new_project = TSC.ProjectItem(name=args.project)
76+
project = server_upload.projects.create(new_project)
77+
78+
################################################################################
79+
# Step 5: Set up our content
80+
# Publish datasources to our site and project
81+
# Publish workbooks to our site and project
82+
################################################################################
83+
publish_datasources_to_site(server_upload, project, args.datasources_folder)
84+
publish_workbooks_to_site(server_upload, project, args.workbooks_folder)
85+
86+
87+
def publish_datasources_to_site(server_object, project, folder):
88+
path = folder + '/*.tds*'
89+
90+
for fname in glob.glob(path):
91+
new_ds = TSC.DatasourceItem(project.id)
92+
new_ds = server_object.datasources.publish(new_ds, fname, server_object.PublishMode.Overwrite)
93+
print("Datasource published. ID: {0}".format(new_ds.id))
94+
95+
96+
def publish_workbooks_to_site(server_object, project, folder):
97+
path = folder + '/*.twb*'
98+
99+
for fname in glob.glob(path):
100+
new_workbook = TSC.WorkbookItem(project.id)
101+
new_workbook.show_tabs = True
102+
new_workbook = server_object.workbooks.publish(new_workbook, fname, server_object.PublishMode.Overwrite)
103+
print("Workbook published. ID: {0}".format(new_workbook.id))
104+
105+
106+
if __name__ == "__main__":
107+
main()

samples/pagination_sample.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,6 @@ def main():
6666
# >>> request_options = TSC.RequestOptions(pagesize=1000)
6767
# >>> all_workbooks = list(TSC.Pager(server.workbooks, request_options))
6868

69+
6970
if __name__ == '__main__':
7071
main()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name='tableauserverclient',
8-
version='0.2',
8+
version='0.3',
99
author='Tableau',
1010
author_email='github@tableau.com',
1111
url='https://github.com/tableau/server-client-python',
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import datetime
2+
3+
4+
# This code below is from the python documentation for tzinfo: https://docs.python.org/2.3/lib/datetime-tzinfo.html
5+
ZERO = datetime.timedelta(0)
6+
HOUR = datetime.timedelta(hours=1)
7+
8+
# A UTC class.
9+
10+
11+
class UTC(datetime.tzinfo):
12+
"""UTC"""
13+
14+
def utcoffset(self, dt):
15+
return ZERO
16+
17+
def tzname(self, dt):
18+
return "UTC"
19+
20+
def dst(self, dt):
21+
return ZERO
22+
23+
24+
utc = UTC()
25+
26+
TABLEAU_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
27+
28+
29+
def parse_datetime(date):
30+
if date is None:
31+
return None
32+
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/connection_credentials.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ class ConnectionCredentials(object):
99
1010
"""
1111

12-
def __init__(self, name, password, embed=True):
12+
def __init__(self, name, password, embed=True, oauth=False):
1313
self.name = name
1414
self.password = password
1515
self.embed = embed
16+
self.oauth = oauth
1617

1718
@property
1819
def embed(self):
@@ -22,3 +23,12 @@ def embed(self):
2223
@property_is_boolean
2324
def embed(self, value):
2425
self._embed = value
26+
27+
@property
28+
def oauth(self):
29+
return self._oauth
30+
31+
@oauth.setter
32+
@property_is_boolean
33+
def oauth(self, value):
34+
self._oauth = value

tableauserverclient/models/datasource_item.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .property_decorators import property_not_nullable
44
from .tag_item import TagItem
55
from .. import NAMESPACE
6+
from ..datetime_helpers import parse_datetime
67

78

89
class DatasourceItem(object):
@@ -118,8 +119,8 @@ def _parse_element(datasource_xml):
118119
name = datasource_xml.get('name', None)
119120
datasource_type = datasource_xml.get('type', None)
120121
content_url = datasource_xml.get('contentUrl', None)
121-
created_at = datasource_xml.get('createdAt', None)
122-
updated_at = datasource_xml.get('updatedAt', None)
122+
created_at = parse_datetime(datasource_xml.get('createdAt', None))
123+
updated_at = parse_datetime(datasource_xml.get('updatedAt', None))
123124

124125
tags = None
125126
tags_elem = datasource_xml.find('.//t:tags', namespaces=NAMESPACE)

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: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval
55
from .property_decorators import property_is_enum, property_not_nullable, property_is_int
66
from .. import NAMESPACE
7+
from ..datetime_helpers import parse_datetime
78

89

910
class ScheduleItem(object):
@@ -208,12 +209,12 @@ def _parse_element(schedule_xml):
208209
id = schedule_xml.get('id', None)
209210
name = schedule_xml.get('name', None)
210211
state = schedule_xml.get('state', None)
211-
created_at = schedule_xml.get('createdAt', None)
212-
updated_at = schedule_xml.get('updatedAt', None)
212+
created_at = parse_datetime(schedule_xml.get('createdAt', None))
213+
updated_at = parse_datetime(schedule_xml.get('updatedAt', None))
213214
schedule_type = schedule_xml.get('type', None)
214215
frequency = schedule_xml.get('frequency', None)
215-
next_run_at = schedule_xml.get('nextRunAt', None)
216-
end_schedule_at = schedule_xml.get('endScheduleAt', None)
216+
next_run_at = parse_datetime(schedule_xml.get('nextRunAt', None))
217+
end_schedule_at = parse_datetime(schedule_xml.get('endScheduleAt', None))
217218
execution_order = schedule_xml.get('executionOrder', None)
218219

219220
priority = schedule_xml.get('priority', None)

tableauserverclient/models/tableau_auth.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ def site(self):
1818
warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.',
1919
DeprecationWarning)
2020
return self.site_id
21+
22+
@site.setter
23+
def site(self, value):
24+
import warnings
25+
warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.',
26+
DeprecationWarning)
27+
self.site_id = value

tableauserverclient/models/user_item.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .exceptions import UnpopulatedPropertyError
33
from .property_decorators import property_is_enum, property_not_empty, property_not_nullable
44
from .. import NAMESPACE
5+
from ..datetime_helpers import parse_datetime
56

67

78
class UserItem(object):
@@ -118,7 +119,7 @@ def _set_values(self, id, name, site_role, last_login,
118119

119120
@classmethod
120121
def from_response(cls, resp):
121-
all_user_items = set()
122+
all_user_items = []
122123
parsed_response = ET.fromstring(resp)
123124
all_user_xml = parsed_response.findall('.//t:user', namespaces=NAMESPACE)
124125
for user_xml in all_user_xml:
@@ -127,15 +128,15 @@ def from_response(cls, resp):
127128
user_item = cls(name, site_role)
128129
user_item._set_values(id, name, site_role, last_login, external_auth_user_id,
129130
fullname, email, auth_setting, domain_name)
130-
all_user_items.add(user_item)
131+
all_user_items.append(user_item)
131132
return all_user_items
132133

133134
@staticmethod
134135
def _parse_element(user_xml):
135136
id = user_xml.get('id', None)
136137
name = user_xml.get('name', None)
137138
site_role = user_xml.get('siteRole', None)
138-
last_login = user_xml.get('lastLogin', None)
139+
last_login = parse_datetime(user_xml.get('lastLogin', None))
139140
external_auth_user_id = user_xml.get('externalAuthUserId', None)
140141
fullname = user_xml.get('fullName', None)
141142
email = user_xml.get('email', None)

tableauserverclient/models/workbook_item.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .tag_item import TagItem
55
from .view_item import ViewItem
66
from .. import NAMESPACE
7+
from ..datetime_helpers import parse_datetime
78
import copy
89

910

@@ -163,8 +164,8 @@ def _parse_element(workbook_xml):
163164
id = workbook_xml.get('id', None)
164165
name = workbook_xml.get('name', None)
165166
content_url = workbook_xml.get('contentUrl', None)
166-
created_at = workbook_xml.get('createdAt', None)
167-
updated_at = workbook_xml.get('updatedAt', None)
167+
created_at = parse_datetime(workbook_xml.get('createdAt', None))
168+
updated_at = parse_datetime(workbook_xml.get('updatedAt', None))
168169

169170
size = workbook_xml.get('size', None)
170171
if size:

tableauserverclient/server/endpoint/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from .auth_endpoint import Auth
22
from .datasources_endpoint import Datasources
33
from .endpoint import Endpoint
4-
from .exceptions import ServerResponseError, MissingRequiredFieldError
4+
from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError
55
from .groups_endpoint import Groups
66
from .projects_endpoint import Projects
77
from .schedules_endpoint import Schedules

0 commit comments

Comments
 (0)
0