8000 Jorwoods/type hint revisions by jorwoods · Pull Request #956 · tableau/server-client-python · GitHub
[go: up one dir, main page]

Skip to content

Jorwoods/type hint revisions #956

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 8 commits into from
Nov 11, 2021
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
3 changes: 2 additions & 1 deletion tableauserverclient/models/datasource_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
if TYPE_CHECKING:
from .permissions_item import PermissionsRule
from .connection_item import ConnectionItem
from .revision_item import RevisionItem
import datetime


Expand Down Expand Up @@ -168,7 +169,7 @@ def webpage_url(self) -> Optional[str]:
return self._webpage_url

@property
def revisions(self):
def revisions(self) -> List["RevisionItem"]:
if self._revisions is None:
error = "Datasource item must be populated with revisions first."
raise UnpopulatedPropertyError(error)
Expand Down
59 changes: 41 additions & 18 deletions tableauserverclient/models/revision_item.py
10000
Original file line number Diff line number Diff line change
@@ -1,46 +1,66 @@
import xml.etree.ElementTree as ET
from ..datetime_helpers import parse_datetime
from typing import List, Optional, TYPE_CHECKING, Type

if TYPE_CHECKING:
from datetime import datetime

class RevisionItem(object):
def __init__(self):
self._resource_id = None
self._resource_name = None
self._revision_number = None
self._current = None
self._deleted = None
self._created_at = None
self._resource_id: Optional[str] = None
self._resource_name: Optional[str] = None
self._revision_number: Optional[str] = None
self._current: Optional[bool] = None
self._deleted: Optional[bool] = None
self._created_at: Optional["datetime"] = None
self._user_id: Optional[str] = None
self._user_name: Optional[str] = None

@property
def resource_id(self):
def resource_id(self) -> Optional[str]:
return self._resource_id

@property
def resource_name(self):
def resource_name(self) -> Optional[str]:
return self._resource_name

@property
def revision_number(self):
def revision_number(self) -> Optional[str]:
return self._revision_number

@property
def current(self):
def current(self) -> Optional[bool]:
return self._current

@property
def deleted(self):
def deleted(self) -> Optional[bool]:
return self._deleted

@property
def created_at(self):
def created_at(self) -> Optional["datetime"]:
return self._created_at

@property
def user_id(self) -> Optional[str]:
return self._user_id

@property
def user_name(self) -> Optional[str]:
return self._user_name

def __repr__(self):
return (
"<RevisionItem# revisionNumber={_revision_number} "
"current={_current} deleted={_deleted}>".format(**self.__dict__)
"current={_current} deleted={_deleted} user={_user_id}>".format(**self.__dict__)
)

@classmethod
def from_response(cls, resp, ns, resource_item):
def from_response(
cls,
resp: bytes,
ns,
resource_item
) -> List["RevisionItem"]:
all_revision_items = list()
parsed_response = ET.fromstring(resp)
all_revision_xml = parsed_response.findall(".//t:revision", namespaces=ns)
Expand All @@ -49,14 +69,17 @@ def from_response(cls, resp, ns, resource_item):
revision_item._resource_id = resource_item.id
revision_item._resource_name = resource_item.name
revision_item._revision_number = revision_xml.get("revisionNumber", None)
revision_item._current = string_to_bool(revision_xml.get("current", ""))
revision_item._deleted = string_to_bool(revision_xml.get("deleted", ""))
revision_item._created_at = revision_xml.get("createdAt", None)
revision_item._current = string_to_bool(revision_xml.get("isCurrent", ""))
revision_item._deleted = string_to_bool(revision_xml.get("isDeleted", ""))
revision_item._created_at = parse_datetime(revision_xml.get("createdAt", None))
for user in revision_xml.findall('.//t:user', namespaces=ns):
revision_item._user_id = user.get("id", None)
revision_item._user_name = user.get("name", None)

all_revision_items.append(revision_item)
return all_revision_items


# Used to convert string represented boolean to a boolean type
def string_to_bool(s):
def string_to_bool(s: str) -> bool:
return s.lower() == "true"
7 changes: 4 additions & 3 deletions tableauserverclient/models/workbook_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
import copy
import uuid

from typing import Dict, List, Optional, Set, TYPE_CHECKING, Union
from typing import Dict, List, Optional, Set, TYPE_CHECKING

if TYPE_CHECKING:
from .connection_item import ConnectionItem
from .permissions_item import PermissionsRule
import datetime
from .revision_item import RevisionItem


class WorkbookItem(object):
Expand All @@ -40,7 +41,7 @@ def __init__(self, project_id: str, name: str = None, show_tabs: bool = False) -
self.owner_id: Optional[str] = None
self.project_id = project_id
self.show_tabs = show_tabs
self.hidden_views = None
self.hidden_views: Optional[List[str]] = None
self.tags: Set[str] = set()
self.data_acceleration_config = {
"acceleration_enabled": None,
Expand Down Expand Up @@ -157,7 +158,7 @@ def data_acceleration_config(self, value):
self._data_acceleration_config = value

@property
def revisions(self):
def revisions(self) -> List["RevisionItem"]:
if self._revisions is None:
error = "Workbook item must be populated with revisions first."
raise UnpopulatedPropertyError(error)
Expand Down
34 changes: 25 additions & 9 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
get_file_object_size,
)
from ...models.job_item import JobItem
from ...models import ConnectionCredentials
from ...models import ConnectionCredentials, RevisionItem

import io
import os
Expand Down Expand Up @@ -393,7 +393,8 @@ def delete_dqw(self, item):
self._data_quality_warnings.clear(item)

# Populate datasource item's revisions
def populate_revisions(self, datasource_item):
@api(version="2.3")
def populate_revisions(self, datasource_item: DatasourceItem) -> None:
if not datasource_item.id:
error = "Datasource item missing ID. Datasource must be retrieved from server first."
raise MissingRequiredFieldError(error)
Expand All @@ -406,7 +407,11 @@ def revisions_fetcher():
"Populated revisions for datasource (ID: {0})".format(datasource_item.id)
)

def _get_datasource_revisions(self, datasource_item, req_options=None):
def _get_datasource_revisions(
self,
datasource_item: DatasourceItem,
req_options: Optional["RequestOptions"] = None
) -> List[RevisionItem]:
url = "{0}/{1}/revisions".format(self.baseurl, datasource_item.id)
server_response = self.get_request(url, req_options)
revisions = RevisionItem.from_response(
Expand All @@ -415,14 +420,15 @@ def _get_datasource_revisions(self, datasource_item, req_options=None):
return revisions

# Download 1 datasource revision by revision number
@api(version="2.3")
def download_revision(
self,
datasource_id,
revision_number,
filepath=None,
include_extract=True,
no_extract=None,
):
datasource_id: str,
revision_number: str,
filepath: Optional[PathOrFile] = None,
include_extract: bool = True,
no_extract: Optional[bool] = None,
) -> str:
if not datasource_id:
error = "Datasource ID undefined."
raise ValueError(error)
Expand Down Expand Up @@ -459,3 +465,13 @@ def download_revision(
)
)
return os.path.abspath(download_path)

@api(version="2.3")
def delete_revision(self, datasource_id: str, revision_number: str) -> None:
if datasource_id is None or revision_number is None:
raise ValueError
url = "/".join([self.baseurl, datasource_id, "revisions", revision_number])

self.delete_request(url)
logger.info("Deleted single datasource revsision (ID: {0}) (Revision: {1})".format(datasource_id, revision_number))

32 changes: 24 additions & 8 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .resource_tagger import _ResourceTagger
from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem
from ...models.job_item import JobItem
from ...models.revision_item import RevisionItem
from ...filesys_helpers import (
to_filename,
make_download_path,
Expand Down Expand Up @@ -429,7 +430,8 @@ def publish(
return new_workbook

F438 # Populate workbook item's revisions
def populate_revisions(self, workbook_item):
@api(version="2.3")
def populate_revisions(self, workbook_item: WorkbookItem) -> None:
if not workbook_item.id:
error = "Workbook item missing ID. Workbook must be retrieved from server first."
raise MissingRequiredFieldError(error)
Expand All @@ -442,7 +444,11 @@ def revisions_fetcher():
"Populated revisions for workbook (ID: {0})".format(workbook_item.id)
)

def _get_workbook_revisions(self, workbook_item, req_options=None):
def _get_workbook_revisions(
self,
workbook_item: WorkbookItem,
req_options: Optional["RequestOptions"]=None
) -> List[RevisionItem]:
url = "{0}/{1}/revisions".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url, req_options)
revisions = RevisionItem.from_response(
Expand All @@ -451,14 +457,15 @@ def _get_workbook_revisions(self, workbook_item, req_options=None):
return revisions

# Download 1 workbook revision by revision number
@api(version="2.3")
def download_revision(
self,
workbook_id,
revision_number,
filepath=None,
include_extract=True,
no_extract=None,
):
workbook_id: str,
revision_number: str,
filepath: Optional[PathOrFile] = None,
include_extract: bool = True,
no_extract: Optional[bool] = None,
) -> str:
if not workbook_id:
error = "Workbook ID undefined."
raise ValueError(error)
Expand Down Expand Up @@ -495,3 +502,12 @@ def download_revision(
)
)
return os.path.abspath(download_path)

@api(version="2.3")
def delete_revision(self, workbook_id: str, revision_number: str) -> None:
if workbook_id is None or revision_number is None:
raise ValueError
url = "/".join([self.baseurl, workbook_id, "revisions", revision_number])

self.delete_request(url)
logger.info("Deleted single workbook revsision (ID: {0}) (Revision: {1})".format(workbook_id, revision_number))
14 changes: 14 additions & 0 deletions test/assets/datasource_revision.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?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-2.3.xsd">
<pagination pageNumber="1" pageSize="100" totalAvailable="3" />
<revisions>
<revision createdAt="2016-07-26T20:34:56Z" revisionNumber="1" isDeleted="false">
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="Cassie"/>
</revision>
<revision createdAt="2016-07-27T20:34:56Z" revisionNumber="2" isDeleted="false">
</revision>
<revision createdAt="2016-07-28T20:34:56Z" revisionNumber="3" isCurrent="true">
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="Cassie"/>
</revision>
</revisions>
</tsResponse>
14 changes: 14 additions & 0 deletions test/assets/workbook_revision.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?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-2.3.xsd">
<pagination pageNumber="1" pageSize="100" totalAvailable="3" />
<revisions>
<revision createdAt="2016-07-26T20:34:56Z" revisionNumber="1" isDeleted="false">
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="Cassie"/>
</revision>
<revision createdAt="2016-07-27T20:34:56Z" revisionNumber="2" isDeleted="false">
</revision>
<revision createdAt="2016-07-28T20:34:56Z" revisionNumber="3" isCurrent="true">
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="Cassie"/>
</revision>
</revisions>
</tsResponse>
46 changes: 46 additions & 0 deletions test/test_datasource.py
622E
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import requests_mock
import xml.etree.ElementTree as ET
from zipfile import ZipFile
import tempfile

import tableauserverclient as TSC
from tableauserverclient.datetime_helpers import format_datetime
Expand All @@ -22,6 +23,7 @@
PUBLISH_XML = 'datasource_publish.xml'
PUBLISH_XML_ASYNC = 'datasource_publish_async.xml'
REFRESH_XML = 'datasource_refresh.xml'
REVISION_XML = 'datasource_revision.xml'
UPDATE_XML = 'datasource_update.xml'
UPDATE_HYPER_DATA_XML = 'datasource_data_update.xml'
UPDATE_CONNECTION_XML = 'datasource_connection_update.xml'
Expand Down Expand Up @@ -600,3 +602,47 @@ def test_create_extracts_encrypted(self) -> None:
m.post(self.baseurl + '/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract',
status_code=200, text=response_xml)
self.server.datasources.create_extract('3cc6cd06-89ce-4fdc-b935-5294135d6d42', True)

def test_revisions(self) -> None:
datasource = TSC.DatasourceItem('project', 'test')
datasource._id = '06b944d2-959d-4604-9305-12323c95e70e'

response_xml = read_xml_asset(REVISION_XML)
with requests_mock.mock() as m:
m.get("{0}/{1}/revisions".format(self.baseurl, datasource.id), text=response_xml)
self.server.datasources.populate_revisions(datasource)
revisions = datasource.revisions

self.assertEqual(len(revisions), 3)
self.assertEqual("2016-07-26T20:34:56Z", format_datetime(revisions[0].created_at))
self.assertEqual("2016-07-27T20:34:56Z", format_datetime(revisions[1].created_at))
self.assertEqual("2016-07-28T20:34:56Z", format_datetime(revisions[2].created_at))

self.assertEqual(False, revisions[0].deleted)
self.assertEqual(False, revisions[0].current)
self.assertEqual(False, revisions[1].deleted)
self.assertEqual(False, revisions[1].current)
self.assertEqual(False, revisions[2].deleted)
self.assertEqual(True, revisions[2].current)

self.assertEqual("Cassie", revisions[0].user_name)
self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", revisions[0].user_id)
self.assertIsNone(revisions[1].user_name)
self.assertIsNone(revisions[1].user_id)
self.assertEqual("Cassie", revisions[2].user_name)
self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", revisions[2].user_id)

def test_delete_revision(self) -> None:
datasource = TSC.DatasourceItem('project', 'test')
datasource._id = '06b944d2-959d-4604-9305-12323c95e70e'

with requests_mock.mock() as m:
m.delete("{0}/{1}/revisions/3".format(self.baseurl, datasource.id))
self.server.datasources.delete_revision(datasource.id, "3")

def test_download_revision(self) -> None:
with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td:
m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/revisions/3/content',
headers={'Content-Disposition': 'name="tableau_datasource"; filename="Sample datasource.tds"'})
file_path = self.server.datasources.download_revision('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', "3", td)
self.assertTrue(os.path.exists(file_path))
Loading
0