8000 initial checkin for get background jobs by graysonarts · Pull Request #298 · tableau/server-client-python · GitHub
[go: up one dir, main page]

Skip to content

initial checkin for get background jobs #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions samples/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def main():
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
help='desired logging level (set to error by default)')

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

args = parser.parse_args()

Expand All @@ -41,7 +41,9 @@ def main():
endpoint = {
'workbook': server.workbooks,
'datasource': server.datasources,
'view': server.views
'view': server.views,
'job': server.jobs,
'project': server.projects,
}.get(args.resource_type)

for resource in TSC.Pager(endpoint.get):
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\
GroupItem, JobItem, PaginationItem, ProjectItem, ScheduleItem, \
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem, \
SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \
SubscriptionItem
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .exceptions import UnpopulatedPropertyError
from .group_item import GroupItem
from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval
from .job_item import JobItem
from .job_item import JobItem, BackgroundJobItem
from .pagination_item import PaginationItem
from .project_item import ProjectItem
from .schedule_item import ScheduleItem
Expand Down
85 changes: 85 additions & 0 deletions tableauserverclient/models/job_item.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import xml.etree.ElementTree as ET
from ..datetime_helpers import parse_datetime
from .target import Target


Expand Down Expand Up @@ -58,3 +59,87 @@ def _parse_element(cls, element, ns):
completed_at = element.get('completedAt', None)
finish_code = element.get('finishCode', -1)
return cls(id_, type_, created_at, started_at, completed_at, finish_code)


class BackgroundJobItem(object):
class Status:
Pending = "Pending"
InProgress = "InProgress"
Success = "Success"
Failed = "Failed"
Cancelled = "Cancelled"

def __init__(self, id_, created_at, priority, job_type, status, title=None, subtitle=None, started_at=None,
ended_at=None):
self._id = id_
self._type = job_type
self._status = status
self._created_at = created_at
self._started_at = started_at
self._ended_at = ended_at
self._priority = priority
self._title = title
self._subtitle = subtitle

@property
def id(self):
return self._id

@property
def name(self):
"""For API consistency - all other resource endpoints have a name attribute which is used to display what
they are. Alias title as name to allow consistent handling of resources in the list sample."""
return self._title

@property
def status(self):
return self._status

@property
def type(self):
return self._type

@property
def created_at(self):
return self._created_at

@property
def started_at(self):
return self._started_at

@property
def ended_at(self):
return self._ended_at

@property
def title(self):
return self._title

@property
def subtitle(self):
return self._subtitle

@property
def priority(self):
return self._priority

@classmethod
def from_response(cls, xml, ns):
parsed_response = ET.fromstring(xml)
all_tasks_xml = parsed_response.findall(
'.//t:backgroundJob', namespaces=ns)
return [cls._parse_element(x, ns) for x in all_tasks_xml]

@classmethod
def _parse_element(cls, element, ns):
id_ = element.get('id', None)
type_ = element.get('jobType', None)
status = element.get('status', None)
created_at = parse_datetime(element.get('createdAt', None))
started_at = parse_datetime(element.get('startedAt', None))
ended_at = parse_datetime(element.get('endedAt', None))
priority = element.get('priority', None)
title = element.get('title', None)
subtitle = element.get('subtitle', None)

return cls(id_, created_at, priority, type_, status, title, subtitle, started_at, ended_at)
2 changes: 1 addition & 1 deletion tableauserverclient/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .request_options import CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, RequestOptions
from .filter import Filter
from .sort import Sort
from .. import ConnectionItem, DatasourceItem, JobItem, \
from .. import ConnectionItem, DatasourceItem, JobItem, BackgroundJobItem, \
GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\
UserItem, ViewItem, WorkbookItem, TaskItem, SubscriptionItem
from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \
Expand Down
21 changes: 8 additions & 13 deletions tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def _make_common_headers(auth_token, content_type):

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


def api(version):
'''Annotate the minimum supported version for an endpoint.
"""Annotate the minimum supported version for an endpoint.

Checks the version on the server object and compares normalized versions.
It will raise an exception if the server version is > the version specified.
Expand All @@ -106,23 +106,18 @@ def api(version):
>>> @api(version="2.3")
>>> def get(self, req_options=None):
>>> ...
'''
"""
def _decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
server_version = Version(self.parent_srv.version or "0.0")
minimum_supported = Version(version)
if server_version < minimum_supported:
error = "This endpoint is not available in API version {}. Requires {}".format(
server_version, minimum_supported)
raise EndpointUnavailableError(error)
self.parent_srv.assert_at_least_version(version)
return func(self, *args, **kwargs)
return wrapper
return _decorator


def parameter_added_in(**params):
'''Annotate minimum versions for new parameters or request options on an endpoint.
"""Annotate minimum versions for new parameters or request options on an endpoint.

The api decorator documents when an endpoint was added, this decorator annotates
keyword arguments on endpoints that may control functionality added after an endpoint was introduced.
Expand All @@ -142,7 +137,7 @@ def parameter_added_in(**params):
>>> @parameter_added_in(no_extract='2.5')
>>> def download(self, workbook_id, filepath=None, extract_only=False):
>>> ...
'''
"""
def _decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
Expand Down
17 changes: 15 additions & 2 deletions tableauserverclient/server/endpoint/jobs_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .endpoint import Endpoint, api
from .. import JobItem
from .. import JobItem, BackgroundJobItem, PaginationItem
import logging

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

@api(version='2.6')
def get(self, job_id):
def get(self, job_id=None, req_options=None):
# Backwards Compatibility fix until we rev the major version
if job_id is not None and isinstance(job_id, basestring):
import warnings
warnings.warn("Jobs.get(job_id) is deprecated, update code to use Jobs.get_by_id(job_id)")
return self.get_by_id(job_id)
self.parent_srv.assert_at_least_version('3.1')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now, you need to call this assert to check that it's > 3.1 and also keep the > 2.6 for old back compat. I need to think about this, maybe it's better to have another decorator behavior_changed_in... or something like that.

But this seems fine for now

server_response = self.get_request(self.baseurl, req_options)
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
jobs = BackgroundJobItem.from_response(server_response.content, self.parent_srv.namespace)
return jobs, pagination_item

@api(version='2.6')
def get_by_id(self, job_id):
logger.info('Query for information about job ' + job_id)
url = "{0}/{1}".format(self.baseurl, job_id)
server_response = self.get_request(url)
Expand Down
14 changes: 14 additions & 0 deletions tableauserverclient/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@
from ..namespace import Namespace
from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \
Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError, Subscriptions, Jobs
from .endpoint.exceptions import EndpointUnavailableError

import requests

try:
from distutils2.version import NormalizedVersion as Version
except ImportError:
from distutils.version import LooseVersion as Version

_PRODUCT_TO_REST_VERSION = {
'10.0': '2.3',
'9.3': '2.2',
Expand Down Expand Up @@ -94,6 +100,14 @@ def use_highest_version(self):
import warnings
warnings.warn("use use_server_version instead", DeprecationWarning)

def assert_at_least_version(self, version):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why move this to the server object itself?

server_version = Version(self.version or "0.0")
minimum_supported = Version(version)
if server_version < minimum_supported:
error = "This endpoint is not available in API version {}. Requires {}".format(
server_version, minimum_supported)
raise EndpointUnavailableError(error)

@property
def baseurl(self):
return "{0}/api/{1}".format(self._server_address, str(self.version))
Expand Down
10 changes: 10 additions & 0 deletions test/assets/job_get.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-3.1.xsd">
<pagination pageNumber="1" pageSize="100" totalAvailable="1"/>
<backgroundJobs>
<backgroundJob id="2eef4225-aa0c-41c4-8662-a76d89ed7336" status="Success"
createdAt="2018-05-22T13:00:29Z" startedAt="2018-05-22T13:00:37Z" endedAt="2018-05-22T13:00:45Z"
priority="50" jobType="single_subscription_notify"/>
</backgroundJobs>
</tsResponse>
45 changes: 45 additions & 0 deletions test/test_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import unittest
import os
from datetime import datetime
import requests_mock
import tableauserverclient as TSC
from tableauserverclient.datetime_helpers import utc

TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')

GET_XML = os.path.join(TEST_ASSET_DIR, 'job_get.xml')


class JobTests(unittest.TestCase):
def setUp(self):
self.server = TSC.Server('http://test')
self.server.version = '3.1'

# Fake signin
self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67'
self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM'

self.baseurl = self.server.jobs.baseurl

def test_get(self):
with open(GET_XML, 'rb') as f:
response_xml = f.read().decode('utf-8')
with requests_mock.mock() as m:
m.get(self.baseurl, text=response_xml)
all_jobs, pagination_item = self.server.jobs.get()
job = all_jobs[0]
created_at = datetime(2018, 5, 22, 13, 0, 29, tzinfo=utc)
started_at = datetime(2018, 5, 22, 13, 0, 37, tzinfo=utc)
ended_at = datetime(2018, 5, 22, 13, 0, 45, tzinfo=utc)
self.assertEquals(1, pagination_item.total_available)
self.assertEquals('2eef4225-aa0c-41c4-8662-a76d89ed7336', job.id)
self.assertEquals('Success', job.status)
self.assertEquals('50', job.priority)
self.assertEquals('single_subscription_notify', job.type)
self.assertEquals(created_at, job.created_at)
self.assertEquals(started_at, job.started_at)
self.assertEquals(ended_at, job.ended_at)

def test_get_before_signin(self):
self.server._auth_token = None
self.assertRaises(TSC.NotSignedInError, self.server.jobs.get)
0