10000 initial checkin for get background jobs (#298) · jmt1992/server-client-python@6ddbb80 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6ddbb80

Browse files
author
Russell Hay
authored
initial checkin for get background jobs (tableau#298)
* initial checkin for get background jobs * pep8 fixes * addressed code review feedback
1 parent 29a1c60 commit 6ddbb80

File tree

10 files changed

+184
-20
lines changed

10 files changed

+184
-20
lines changed

samples/list.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def main():
2121
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
2222
help='desired logging level (set to error by default)')
2323

24-
parser.add_argument('resource_type', choices=['workbook', 'datasource', 'view'])
24+
parser.add_argument('resource_type', choices=['workbook', 'datasource', 'project', 'view', 'job'])
2525

2626
args = parser.parse_args()
2727

@@ -41,7 +41,9 @@ def main():
4141
endpoint = {
4242
'workbook': server.workbooks,
4343
'datasource': server.datasources,
44-
'view': server.views
44+
'view': server.views,
45+
'job': server.jobs,
46+
'project': server.projects,
4547
}.get(args.resource_type)
4648

4749
for resource in TSC.Pager(endpoint.get):

tableauserverclient/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
22
from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\
3-
GroupItem, JobItem, PaginationItem, ProjectItem, ScheduleItem, \
3+
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem, \
44
SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
55
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \
66
SubscriptionItem

tableauserverclient/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .exceptions import UnpopulatedPropertyError
55
from .group_item import GroupItem
66
from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval
7-
from .job_item import JobItem
7+
from .job_item import JobItem, BackgroundJobItem
88
from .pagination_item import PaginationItem
99
from .project_item import ProjectItem
1010
from .schedule_item import ScheduleItem

tableauserverclient/models/job_item.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import xml.etree.ElementTree as ET
2+
from ..datetime_helpers import parse_datetime
23
from .target import Target
34

45

@@ -58,3 +59,87 @@ def _parse_element(cls, element, ns):
5859
completed_at = element.get('completedAt', None)
5960
finish_code = element.get('finishCode', -1)
6061
return cls(id_, type_, created_at, started_at, completed_at, finish_code)
62+
63+
64+
class BackgroundJobItem(object):
65+
class Status:
66+
Pending = "Pending"
67+
InProgress = "InProgress"
68+
Success = "Success"
69+
Failed = "Failed"
70+
Cancelled = "Cancelled"
71+
72+
def __init__(self, id_, created_at, priority, job_type, status, title=None, subtitle=None, started_at=None,
73+
ended_at=None):
74+
self._id = id_
75+
self._type = job_type
76+
self._status = status
77+
self._created_at = created_at
78+
self._started_at = started_at
79+
self._ended_at = ended_at
80+
self._priority = priority
81+
self._title = title
82+
self._subtitle = subtitle
83+
84+
@property
85+
def id(self):
86+
return self._id
87+
88+
@property
89+
def name(self):
90+
"""For API consistency - all other resource endpoints have a name attribute which is used to display what
91+
they are. Alias title as name to allow consistent handling of resources in the list sample."""
92+
return self._title
93+
94+
@property
95+
def status(self):
96+
return self._status
97+
98+
@property
99+
def type(self):
100+
return self._type
101+
102+
@property
103+
def created_at(self):
104+
return self._created_at
105+
106+
@property
107+
def started_at(self):
108+
return self._started_at
109+
110+
@property
111+
def ended_at(self):
112+
return self._ended_at
113+
114+
@property
115+
def title(self):
116+
return self._title
117+
118+
@property
119+
def subtitle(self):
120+
return self._subtitle
121+
122+
@property
123+
def priority(self):
124+
return self._priority
125+
126+
@classmethod
127+
def from_response(cls, xml, ns):
128+
parsed_response = ET.fromstring(xml)
129+
all_tasks_xml = parsed_response.findall(
130+
'.//t:backgroundJob', namespaces=ns)
131+
return [cls._parse_element(x, ns) for x in all_tasks_xml]
132+
133+
@classmethod
134+
def _parse_element(cls, element, ns):
135+
id_ = element.get('id', None)
136+
type_ = element.get('jobType', None)
137+
status = element.get('status', None)
138+
created_at = parse_datetime(element.get('createdAt', None))
139+
started_at = parse_datetime(element.get('startedAt', None))
140+
ended_at = parse_datetime(element.get('endedAt', None))
141+
priority = element.get('priority', None)
142+
title = element.get('title', None)
143+
subtitle = element.get('subtitle', None)
144+
145+
return cls(id_, created_at, priority, type_, status, title, subtitle, started_at, ended_at)

tableauserverclient/server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from .request_options import CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, RequestOptions
33
from .filter import Filter
44
from .sort import Sort
5-
from .. import ConnectionItem, DatasourceItem, JobItem, \
5+
from .. import ConnectionItem, DatasourceItem, JobItem, BackgroundJobItem, \
66
GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\
77
UserItem, ViewItem, WorkbookItem, TaskItem, SubscriptionItem
88
from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \

tableauserverclient/server/endpoint/endpoint.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ def _make_common_headers(auth_token, content_type):
2929

3030
@staticmethod
3131
def _safe_to_log(server_response):
32-
'''Checks if the server_response content is not xml (eg binary image or zip)
33-
and and replaces it with a constant
34-
'''
32+
"""Checks if the server_response content is not xml (eg binary image or zip)
33+
and replaces it with a constant
34+
"""
3535
ALLOWED_CONTENT_TYPES = ('application/xml', 'application/xml;charset=utf-8')
3636
if server_response.headers.get('Content-Type', None) not in ALLOWED_CONTENT_TYPES:
3737
return '[Truncated File Contents]'
@@ -90,7 +90,7 @@ def post_request(self, url, xml_request, content_type='text/xml'):
9090

9191

9292
def api(version):
93-
'''Annotate the minimum supported version for an endpoint.
93+
"""Annotate the minimum supported version for an endpoint.
9494
9595
Checks the version on the server object and compares normalized versions.
9696
It will raise an exception if the server version is > the version specified.
@@ -106,23 +106,18 @@ def api(version):
106106
>>> @api(version="2.3")
107107
>>> def get(self, req_options=None):
108108
>>> ...
109-
'''
109+
"""
110110
def _decorator(func):
111111
@wraps(func)
112112
def wrapper(self, *args, **kwargs):
113-
server_version = Version(self.parent_srv.version or "0.0")
114-
minimum_supported = Version(version)
115-
if server_version < minimum_supported:
116-
error = "This endpoint is not available in API version {}. Requires {}".format(
117-
server_version, minimum_supported)
118-
raise EndpointUnavailableError(error)
113+
self.parent_srv.assert_at_least_version(version)
119114
return func(self, *args, **kwargs)
120115
return wrapper
121116
return _decorator
122117

123118

124119
def parameter_added_in(**params):
125-
'''Annotate minimum versions for new parameters or request options on an endpoint.
120+
"""Annotate minimum versions for new parameters or request options on an endpoint.
126121
127122
The api decorator documents when an endpoint was added, this decorator annotates
128123
keyword arguments on endpoints that may control functionality added after an endpoint was introduced.
@@ -142,7 +137,7 @@ def parameter_added_in(**params):
142137
>>> @parameter_added_in(no_extract='2.5')
143138
>>> def download(self, workbook_id, filepath=None, extract_only=False):
144139
>>> ...
145-
'''
140+
"""
146141
def _decorator(func):
147142
@wraps(func)
148143
def wrapper(self, *args, **kwargs):

tableauserverclient/server/endpoint/jobs_endpoint.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .endpoint import Endpoint, api
2-
from .. import JobItem
2+
from .. import JobItem, BackgroundJobItem, PaginationItem
33
import logging
44

55
logger = logging.getLogger('tableau.endpoint.jobs')
@@ -11,7 +11,20 @@ def baseurl(self):
1111
return "{0}/sites/{1}/jobs".format(self.parent_srv.baseurl, self.parent_srv.site_id)
1212

1313
@api(version='2.6')
14-
def get(self, job_id):
14+
def get(self, job_id=None, req_options=None):
15+
# Backwards Compatibility fix until we rev the major version
16+
if job_id is not None and isinstance(job_id, basestring):
17+
import warnings
18+
warnings.warn("Jobs.get(job_id) is deprecated, update code to use Jobs.get_by_id(job_id)")
19+
return self.get_by_id(job_id)
20+
self.parent_srv.assert_at_least_version('3.1')
21+
server_response = self.get_request(self.baseurl, req_options)
22+
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
23+
jobs = BackgroundJobItem.from_response(server_response.content, self.parent_srv.namespace)
24+
return jobs, pagination_item
25+
26+
@api(version='2.6')
27+
def get_by_id(self, job_id):
1528
logger.info('Query for information about job ' + job_id)
1629
url = "{0}/{1}".format(self.baseurl, job_id)
1730
server_response = self.get_request(url)

tableauserverclient/server/server.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
from ..namespace import Namespace
55
from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \
66
Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError, Subscriptions, Jobs
7+
from .endpoint.exceptions import EndpointUnavailableError
78

89
import requests
910

11+
try:
12+
from distutils2.version import NormalizedVersion as Version
13+
except ImportError:
14+
from distutils.version import LooseVersion as Version
15+
1016
_PRODUCT_TO_REST_VERSION = {
1117
'10.0': '2.3',
1218
'9.3': '2.2',
@@ -94,6 +100,14 @@ def use_highest_version(self):
94100
import warnings
95101
warnings.warn("use use_server_version instead", DeprecationWarning)
96102

103+
def assert_at_least_version(self, version):
104+
server_version = Version(self.version or "0.0")
105+
minimum_supported = Version(version)
106+
if server_version < minimum_supported:
107+
error = "This endpoint is not available in API version {}. Requires {}".format(
108+
server_version, minimum_supported)
109+
raise EndpointUnavailableError(error)
110+
97111
@property
98112
def baseurl(self):
99113
return "{0}/api/{1}".format(self._server_address, str(self.version))

test/assets/job_get.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-3.1.xsd">
4+
<pagination pageNumber="1" pageSize="100" totalAvailable="1"/>
5+
<backgroundJobs>
6+
<backgroundJob id="2eef4225-aa0c-41c4-8662-a76d89ed7336" status="Success"
7+
createdAt="2018-05-22T13:00:29Z" startedAt="2018-05-22T13:00:37Z" endedAt="2018-05-22T13:00:45Z"
8+
priority="50" jobType="single_subscription_notify"/>
9+
</backgroundJobs>
10+
</tsResponse>

test/test_job.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import unittest
2+
import os
3+
from datetime import datetime
4+
import requests_mock
5+
import tableauserverclient as TSC
6+
from tableauserverclient.datetime_helpers import utc
7+
8+
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
9+
10+
GET_XML = os.path.join(TEST_ASSET_DIR, 'job_get.xml')
11+
12+
13+
class JobTests(unittest.TestCase):
14+
def setUp(self):
15+
self.server = TSC.Server('http://test')
16+
self.server.version = '3.1'
17+
18+
# Fake signin
19+
self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67'
20+
self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM'
21+
22+
self.baseurl = self.server.jobs.baseurl
23+
24+
def test_get(self):
25+
with open(GET_XML, 'rb') as f:
26+
response_xml = f.read().decode('utf-8')
27+
with requests_mock.mock() as m:
28+
m.get(self.baseurl, text=response_xml)
29+
all_jobs, pagination_item = self.server.jobs.get()
30+
job = all_jobs[0]
31+
< 8A05 span class=pl-s1>created_at = datetime(2018, 5, 22, 13, 0, 29, tzinfo=utc)
32+
started_at = datetime(2018, 5, 22, 13, 0, 37, tzinfo=utc)
33+
ended_at = datetime(2018, 5, 22, 13, 0, 45, tzinfo=utc)
34+
self.assertEquals(1, pagination_item.total_available)
35+
self.assertEquals('2eef4225-aa0c-41c4-8662-a76d89ed7336', job.id)
36+
self.assertEquals('Success', job.status)
37+
self.assertEquals('50', job.priority)
38+
self.assertEquals('single_subscription_notify', job.type)
39+
self.assertEquals(created_at, job.created_at)
40+
self.assertEquals(started_at, job.started_at)
41+
self.assertEquals(ended_at, job.ended_at)
42+
43+
def test_get_before_signin(self):
44+
self.server._auth_token = None
45+
self.assertRaises(TSC.NotSignedInError, self.server.jobs.get)

0 commit comments

Comments
 (0)
0