Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
114922b
Test request construction (#91)
t8y8 Nov 2, 2016
818645b
new sample & new feature instructions
benlower Nov 2, 2016
78b9684
Initial documentation for TSC (#98)
jdomingu Nov 3, 2016
39f4b22
Merge branch 'master' into development
Nov 3, 2016
c86992b
Initial implementation to address #102 and provide datetime objects
Nov 16, 2016
b2e9e08
Fix pep8 failures
Nov 16, 2016
68fa442
Remove setters and move to doing the conversion during parsing
Nov 17, 2016
ff23f08
removing attempt to import pytz
Nov 17, 2016
db0dac1
fixing yet another pep8 failure
Nov 17, 2016
e853d7c
Merge pull request #103 from tableau/feature-102-datetimes
Nov 18, 2016
c0c31cc
Fix Pager by making UserItem return a list like the other models (#109)
t8y8 Dec 2, 2016
c2230ae
Add deprecation warning to site setter too (#97)
t8y8 Dec 9, 2016
a7ca709
Fix large downloads (#105) (#111)
t8y8 Dec 14, 2016
f551c0b
Enhancement #117: Add support for the oAuth flag
hugoboos Dec 23, 2016
49922e1
New sample: Migrate with Datasources
benlower Dec 28, 2016
9a3c8c6
Revert "New sample: Migrate with Datasources"
benlower Dec 28, 2016
fc2b9bf
Python 3.6 Released in Dec (#123)
t8y8 Jan 4, 2017
bfff665
Implement call to move to highest supported REST API version (#100)
t8y8 Jan 5, 2017
ee7b70b
Add annotation for endpoints to indicate minimum supported API versio…
t8y8 Jan 6, 2017
55bf420
Test request construction (#91)
t8y8 Nov 2, 2016
f8b8857
new sample & new feature instructions
benlower Nov 2, 2016
8b5c7b1
Initial documentation for TSC (#98)
jdomingu Nov 3, 2016
2c8d1a5
Initial implementation to address #102 and provide datetime objects
Nov 16, 2016
5dfc155
Fix pep8 failures
Nov 16, 2016
898526d
Remove setters and move to doing the conversion during parsing
Nov 17, 2016
a15a8f5
removing attempt to import pytz
Nov 17, 2016
ccfd0c2
fixing yet another pep8 failure
Nov 17, 2016
5bf5d1f
Fix Pager by making UserItem return a list like the other models (#109)
t8y8 Dec 2, 2016
65ce464
Add deprecation warning to site setter too (#97)
t8y8 Dec 9, 2016
9d0c8ca
Fix large downloads (#105) (#111)
t8y8 Dec 14, 2016
01235ea
Enhancement #117: Add support for the oAuth flag
hugoboos Dec 23, 2016
f310f3d
New sample: Migrate with Datasources
benlower Dec 28, 2016
2e31644
Revert "New sample: Migrate with Datasources"
benlower Dec 28, 2016
f172490
Python 3.6 Released in Dec (#123)
t8y8 Jan 4, 2017
8a0112e
Implement call to move to highest supported REST API version (#100)
t8y8 Jan 5, 2017
f7da0db
Add annotation for endpoints to indicate minimum supported API versio…
t8y8 Jan 6, 2017
8dfeabf
Prepping for release 0.3
Jan 11, 2017
9ab86de
site_id is prefered, not site.
Jan 11, 2017
fec8755
Adding missing contributors
Jan 11, 2017
aa56523
Merge pull request #127 from RussTheAerialist/development
Jan 11, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ python:
- "3.3"
- "3.4"
- "3.5"
- "3.6"
- "pypy"
# command to install dependencies
install:
Expand All @@ -14,4 +15,4 @@ script:
# Tests
- python setup.py test
# pep8 - disabled for now until we can scrub the files to make sure we pass before turning it on
- pycodestyle .
- pycodestyle tableauserverclient test
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## 0.3 (11 January 2017)

* Return DateTime objects instead of strings (#102)
* UserItem now is compatible with Pager (#107, #109)
* Deprecated site in favor of site_id (#97)
* Improved handling of large downloads (#105, #111)
* Added support for oAuth when publishing (#117)
* Added Testing against Py36 (#122, #123)
* Added Version Checking to use highest supported REST api version (#100)
* Added Infrastructure for throwing error if trying to do something that is not supported by REST api version (#124)
* Various Code Cleanup
* Added Documentation (#98)
* Improved Test Infrastructure (#91)

## 0.2 (02 November 2016)

* Added Initial Schedules Support (#48)
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The following people have contributed to this project to make it possible, and w
## Contributors

* [geordielad](https://github.com/geordielad)
* [Hugo Stijns)(https://github.com/hugoboos)
* [kovner](https://github.com/kovner)


Expand All @@ -14,3 +15,5 @@ The following people have contributed to this project to make it possible, and w
* [lgraber](https://github.com/lgraber)
* [t8y8](https://github.com/t8y8)
* [RussTheAerialist](https://github.com/RussTheAerialist)
* [Ben Lower](https://github.com/benlower)
* [Jared Dominguez](https://github.com/jdomingu)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ This repository contains Python source code and sample files.
For more information on installing and using TSC, see the documentation:

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

107 changes: 107 additions & 0 deletions samples/initialize_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
####
# This script sets up a server. It uploads datasources and workbooks from the local filesystem.
#
# By default, all content is published to the Default project on the Default site.
####

import tableauserverclient as TSC
import argparse
import getpass
import logging
import glob


def main():
parser = argparse.ArgumentParser(description='Initialize a server with content.')
parser.add_argument('--server', '-s', required=True, help='server address')
parser.add_argument('--datasources-folder', '-df', required=True, help='folder containing datasources')
parser.add_argument('--workbooks-folder', '-wf', required=True, help='folder containing workbooks')
parser.add_argument('--site', '-si', required=False, default='Default', help='site to use')
parser.add_argument('--project', '-p', required=False, default='Default', help='project to use')
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)

################################################################################
# Step 1: Sign in to server.
################################################################################
tableau_auth = TSC.TableauAuth(args.username, password)
server = TSC.Server(args.server)

with server.auth.sign_in(tableau_auth):

################################################################################
# Step 2: Create the site we need only if it doesn't exist
################################################################################
print("Checking to see if we need to create the site...")

all_sites, _ = server.sites.get()
existing_site = next((s for s in all_sites if s.name == args.site), None)

# Create the site if it doesn't exist
if existing_site is None:
print("Site not found: {0} Creating it...").format(args.site)
new_site = TSC.SiteItem(name=args.site, content_url=args.site.replace(" ", ""),
admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers)
server.sites.create(new_site)
else:
print("Site {0} exists. Moving on...").format(args.site)

################################################################################
# Step 3: Sign-in to our target site
################################################################################
print("Starting our content upload...")
server_upload = TSC.Server(args.server)
tableau_auth.site = args.site

with server_upload.auth.sign_in(tableau_auth):

################################################################################
# Step 4: Create the project we need only if it doesn't exist
################################################################################
all_projects, _ = server_upload.projects.get()
project = next((p for p in all_projects if p.name == args.project), None)

# Create our project if it doesn't exist
if project is None:
print("Project not found: {0} Creating it...").format(args.project)
new_project = TSC.ProjectItem(name=args.project)
project = server_upload.projects.create(new_project)

################################################################################
# Step 5: Set up our content
# Publish datasources to our site and project
# Publish workbooks to our site and project
################################################################################
publish_datasources_to_site(server_upload, project, args.datasources_folder)
publish_workbooks_to_site(server_upload, project, args.workbooks_folder)


def publish_datasources_to_site(server_object, project, folder):
path = folder + '/*.tds*'

for fname in glob.glob(path):
new_ds = TSC.DatasourceItem(project.id)
new_ds = server_object.datasources.publish(new_ds, fname, server_object.PublishMode.Overwrite)
print("Datasource published. ID: {0}".format(new_ds.id))


def publish_workbooks_to_site(server_object, project, folder):
path = folder + '/*.twb*'

for fname in glob.glob(path):
new_workbook = TSC.WorkbookItem(project.id)
new_workbook.show_tabs = True
new_workbook = server_object.workbooks.publish(new_workbook, fname, server_object.PublishMode.Overwrite)
print("Workbook published. ID: {0}".format(new_workbook.id))


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions samples/pagination_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ def main():
# >>> request_options = TSC.RequestOptions(pagesize=1000)
# >>> all_workbooks = list(TSC.Pager(server.workbooks, request_options))


if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name='tableauserverclient',
version='0.2',
version='0.3',
author='Tableau',
author_email='[email protected]',
url='https://github.com/tableau/server-client-python',
Expand Down
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


# 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):
if date is None:
return None

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


def format_datetime(date):
return date.astimezone(tz=utc).strftime(TABLEAU_DATE_FORMAT)
12 changes: 11 additions & 1 deletion tableauserverclient/models/connection_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ class ConnectionCredentials(object):

"""

def __init__(self, name, password, embed=True):
def __init__(self, name, password, embed=True, oauth=False):
self.name = name
self.password = password
self.embed = embed
self.oauth = oauth

@property
def embed(self):
Expand All @@ -22,3 +23,12 @@ def embed(self):
@property_is_boolean
def embed(self, value):
self._embed = value

@property
def oauth(self):
return self._oauth

@oauth.setter
@property_is_boolean
def oauth(self, value):
self._oauth = value
5 changes: 3 additions & 2 deletions tableauserverclient/models/datasource_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .property_decorators import property_not_nullable
from .tag_item import TagItem
from .. import NAMESPACE
from ..datetime_helpers import parse_datetime


class DatasourceItem(object):
Expand Down Expand Up @@ -118,8 +119,8 @@ def _parse_element(datasource_xml):
name = datasource_xml.get('name', None)
datasource_type = datasource_xml.get('type', None)
content_url = datasource_xml.get('contentUrl', None)
created_at = datasource_xml.get('createdAt', None)
updated_at = datasource_xml.get('updatedAt', None)
created_at = parse_datetime(datasource_xml.get('createdAt', None))
updated_at = parse_datetime(datasource_xml.get('updatedAt', None))

tags = None
tags_elem = datasource_xml.find('.//t:tags', namespaces=NAMESPACE)
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
9 changes: 5 additions & 4 deletions tableauserverclient/models/schedule_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval
from .property_decorators import property_is_enum, property_not_nullable, property_is_int
from .. import NAMESPACE
from ..datetime_helpers import parse_datetime


class ScheduleItem(object):
Expand Down Expand Up @@ -208,12 +209,12 @@ def _parse_element(schedule_xml):
id = schedule_xml.get('id', None)
name = schedule_xml.get('name', None)
state = schedule_xml.get('state', None)
created_at = schedule_xml.get('createdAt', None)
updated_at = schedule_xml.get('updatedAt', None)
created_at = parse_datetime(schedule_xml.get('createdAt', None))
updated_at = parse_datetime(schedule_xml.get('updatedAt', None))
schedule_type = schedule_xml.get('type', None)
frequency = schedule_xml.get('frequency', None)
next_run_at = schedule_xml.get('nextRunAt', None)
end_schedule_at = schedule_xml.get('endScheduleAt', None)
next_run_at = parse_datetime(schedule_xml.get('nextRunAt', None))
end_schedule_at = parse_datetime(schedule_xml.get('endScheduleAt', None))
execution_order = schedule_xml.get('executionOrder', None)

priority = schedule_xml.get('priority', None)
Expand Down
7 changes: 7 additions & 0 deletions tableauserverclient/models/tableau_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ def site(self):
warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.',
DeprecationWarning)
return self.site_id

@site.setter
def site(self, value):
import warnings
warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.',
DeprecationWarning)
self.site_id = value
7 changes: 4 additions & 3 deletions tableauserverclient/models/user_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_is_enum, property_not_empty, property_not_nullable
from .. import NAMESPACE
from ..datetime_helpers import parse_datetime


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

@classmethod
def from_response(cls, resp):
all_user_items = set()
all_user_items = []
parsed_response = ET.fromstring(resp)
all_user_xml = parsed_response.findall('.//t:user', namespaces=NAMESPACE)
for user_xml in all_user_xml:
Expand All @@ -127,15 +128,15 @@ def from_response(cls, resp):
user_item = cls(name, site_role)
user_item._set_values(id, name, site_role, last_login, external_auth_user_id,
fullname, email, auth_setting, domain_name)
all_user_items.add(user_item)
all_user_items.append(user_item)
return all_user_items

@staticmethod
def _parse_element(user_xml):
id = user_xml.get('id', None)
name = user_xml.get('name', None)
site_role = user_xml.get('siteRole', None)
last_login = user_xml.get('lastLogin', None)
last_login = parse_datetime(user_xml.get('lastLogin', None))
external_auth_user_id = user_xml.get('externalAuthUserId', None)
fullname = user_xml.get('fullName', None)
email = user_xml.get('email', None)
Expand Down
5 changes: 3 additions & 2 deletions tableauserverclient/models/workbook_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .tag_item import TagItem
from .view_item import ViewItem
from .. import NAMESPACE
from ..datetime_helpers import parse_datetime
import copy


Expand Down Expand Up @@ -163,8 +164,8 @@ def _parse_element(workbook_xml):
id = workbook_xml.get('id', None)
name = workbook_xml.get('name', None)
content_url = workbook_xml.get('contentUrl', None)
created_at = workbook_xml.get('createdAt', None)
updated_at = workbook_xml.get('updatedAt', None)
created_at = parse_datetime(workbook_xml.get('createdAt', None))
updated_at = parse_datetime(workbook_xml.get('updatedAt', None))

size = workbook_xml.get('size', None)
if size:
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/server/endpoint/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .auth_endpoint import Auth
from .datasources_endpoint import Datasources
from .endpoint import Endpoint
from .exceptions import ServerResponseError, MissingRequiredFieldError
from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError
from .groups_endpoint import Groups
from .projects_endpoint import Projects
from .schedules_endpoint import Schedules
Expand Down
Loading