8000 Jorwoods/type hint revisions (#956) · tableau/server-client-python@9752d85 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9752d85

Browse files
jorwoodsjacalata
authored andcommitted
Jorwoods/type hint revisions (#956)
Add type hints and tests for workbook and data source revisions
1 parent 240ce4e commit 9752d85

9 files changed

+220
-39
lines changed

tableauserverclient/models/datasource_item.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
if TYPE_CHECKING:
1515
from .permissions_item import PermissionsRule
1616
from .connection_item import ConnectionItem
17+
from .revision_item import RevisionItem
1718
import datetime
1819

1920

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

170171
@property
171-
def revisions(self):
172+
def revisions(self) -> List["RevisionItem"]:
172173
if self._revisions is None:
173174
error = "Datasource item must be populated with revisions first."
174175
raise UnpopulatedPropertyError(error)
Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,66 @@
11
import xml.etree.ElementTree as ET
2+
from ..datetime_helpers import parse_datetime
3+
from typing import List, Optional, TYPE_CHECKING, Type
4+
5+
if TYPE_CHECKING:
6+
from datetime import datetime
27

38
class RevisionItem(object):
49
def __init__(self):
5-
self._resource_id = None
6-
self._resource_name = None
7-
self._revision_number = None
8-
self._current = None
9-
self._deleted = None
10-
self._created_at = None
10+
self._resource_id: Optional[str] = None
11+
self._resource_name: Optional[str] = None
12+
self._revision_number: Optional[str] = None
13+
self._current: Optional[bool] = None
14+
self._deleted: Optional[bool] = None
15+
self._created_at: Optional["datetime"] = None
16+
self._user_id: Optional[str] = None
17+
self._user_name: Optional[str] = None
1118

1219
@property
13-
def resource_id(self):
20+
def resource_id(self) -> Optional[str]:
1421
return self._resource_id
1522

1623
@property
17-
def resource_name(self):
24+
def resource_name(self) -> Optional[str]:
1825
return self._resource_name
1926

2027
@property
21-
def revision_number(self):
28+
def revision_number(self) -> Optional[str]:
2229
return self._revision_number
2330

2431
@property
25-
def current(self):
32+
def current(self) -> Optional[bool]:
2633
return self._current
2734

2835
@property
29-
def deleted(self):
36+
def deleted(self) -> Optional[bool]:
3037
return self._deleted
3138

3239
@property
33-
def created_at(self):
40+
def created_at(self) -> Optional["datetime"]:
3441
return self._created_at
3542

43+
@property
44+
def user_id(self) -> Optional[str]:
45+
return self._user_id
46+
47+
@property
48+
def user_name(self) -> Optional[str]:
49+
return self._user_name
50+
3651
def __repr__(self):
3752
return (
3853
"<RevisionItem# revisionNumber={_revision_number} "
39-
"current={_current} deleted={_deleted}>".format(**self.__dict__)
54+
"current={_current} deleted={_deleted} user={_user_id}>".format(**self.__dict__)
4055
)
4156

4257
@classmethod
43-
def from_response(cls, resp, ns, resource_item):
58+
def from_response(
59+
cls,
60+
resp: bytes,
61+
ns,
62+
resource_item
63+
) -> List["RevisionItem"]:
4464
all_revision_items = list()
4565
parsed_response = ET.fromstring(resp)
4666
all_revision_xml = parsed_response.findall(".//t:revision", namespaces=ns)
@@ -49,14 +69,17 @@ def from_response(cls, resp, ns, resource_item):
4969
revision_item._resource_id = resource_item.id
5070
revision_item._resource_name = resource_item.name
5171
revision_item._revision_number = revision_xml.get("revisionNumber", None)
52-
revision_item._current = string_to_bool(revision_xml.get("current", ""))
53-
revision_item._deleted = string_to_bool(revision_xml.get("deleted", ""))
54-
revision_item._created_at = revision_xml.get("createdAt", None)
72+
revision_item._current = string_to_bool(revision_xml.get("isCurrent", ""))
73+
revision_item._deleted = string_to_bool(revision_xml.get("isDeleted", ""))
74+
revision_item._created_at = parse_datetime(revision_xml.get("createdAt", None))
75+
for user in revision_xml.findall('.//t:user', namespaces=ns):
76+
revision_item._user_id = user.get("id", None)
77+
revision_item._user_name = user.get("name", None)
5578

5679
all_revision_items.append(revision_item)
5780
return all_revision_items
5881

5982

6083
# Used to convert string represented boolean to a boolean type
61-
def string_to_bool(s):
84+
def string_to_bool(s: str) -> bool:
6285
return s.lower() == "true"

tableauserverclient/models/workbook_item.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212
import copy
1313
import uuid
1414

15-
from typing import Dict, List, Optional, Set, TYPE_CHECKING, Union
15+
from typing import Dict, List, Optional, Set, TYPE_CHECKING
1616

1717
if TYPE_CHECKING:
1818
from .connection_item import ConnectionItem
1919
from .permissions_item import PermissionsRule
2020
import datetime
21+
from .revision_item import RevisionItem
2122

2223

2324
class WorkbookItem(object):
@@ -40,7 +41,7 @@ def __init__(self, project_id: str, name: str = None, show_tabs: bool = False) -
4041
self.owner_id: Optional[str] = None
4142
self.project_id = project_id
4243
self.show_tabs = show_tabs
43-
self.hidden_views = None
44+
self.hidden_views: Optional[List[str]] = None
4445
self.tags: Set[str] = set()
4546
self.data_acceleration_config = {
4647
"acceleration_enabled": None,
@@ -157,7 +158,7 @@ def data_acceleration_config(self, value):
157158
self._data_acceleration_config = value
158159

159160
@property
160-
def revisions(self):
161+
def revisions(self) -> List["RevisionItem"]:
161162
if self._revisions is None:
162163
error = "Workbook item must be populated with revisions first."
163164
raise UnpopulatedPropertyError(error)

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
get_file_object_size,
1313
)
1414
from ...models.job_item import JobItem
15-
from ...models import ConnectionCredentials
15+
from ...models import ConnectionCredentials, RevisionItem
1616

1717
import io
1818
import os
@@ -393,7 +393,8 @@ def delete_dqw(self, item):
393393
self._data_quality_warnings.clear(item)
394394

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

409-
def _get_datasource_revisions(self, datasource_item, req_options=None):
410+
def _get_datasource_revisions(
411+
self,
412+
datasource_item: DatasourceItem,
413+
req_options: Optional["RequestOptions"] = None
414+
) -> List[RevisionItem]:
410415
url = "{0}/{1}/revisions".format(self.baseurl, datasource_item.id)
411416
server_response = self.get_request(url, req_options)
412417
revisions = RevisionItem.from_response(
@@ -415,14 +420,15 @@ def _get_datasource_revisions(self, datasource_item, req_options=None):
415420
return revisions
416421

417422
# Download 1 datasource revision by revision number
423+
@api(version="2.3")
418424
def download_revision(
419425
self,
420-
datasource_id,
421-
revision_number,
422-
filepath=None,
423-
include_extract=True,
424-
no_extract=None,
425-
):
426+
datasource_id: str,
427+
revision_number: str,
428+
filepath: Optional[PathOrFile] = None,
429+
include_extract: bool = True,
430+
no_extract: Optional[bool] = None,
431+
) -> str:
426432
if not datasource_id:
427433
error = "Datasource ID undefined."
428434
raise ValueError(error)
@@ -459,3 +465,13 @@ def download_revision(
459465
)
460466
)
461467
return os.path.abspath(download_path)
468+
469+
@api(version="2.3")
470+
def delete_revision(self, datasource_id: str, revision_number: str) -> None:
471+
if datasource_id is None or revision_number is None:
472+
raise ValueError
473+
url = "/".join([self.baseurl, datasource_id, "revisions", revision_number])
474+
475+
self.delete_request(url)
476+
logger.info("Deleted single datasource revsision (ID: {0}) (Revision: {1})".format(datasource_id, revision_number))
477+

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .resource_tagger import _ResourceTagger
55
from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem
66
from ...models.job_item import JobItem
7+
from ...models.revision_item import RevisionItem
78
from ...filesys_helpers import (
89
to_filename,
910
make_download_path,
@@ -429,7 +430,8 @@ def publish(
429430
return new_workbook
430431

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

445-
def _get_workbook_revisions(self, workbook_item, req_options=None):
447+
def _get_workbook_revisions(
448+
self,
449+
workbook_item: WorkbookItem,
450+
req_options: Optional["RequestOptions"]=None
451+
) -> List[RevisionItem]:
446452
url = "{0}/{1}/revisions".format(self.baseurl, workbook_item.id)
447453
server_response = self.get_request(url, req_options)
448454
revisions = RevisionItem.from_response(
@@ -451,14 +457,15 @@ def _get_workbook_revisions(self, workbook_item, req_options=None):
451457
return revisions
452458

453459
# Download 1 workbook revision by revision number
460+
@api(version="2.3")
454461
def download_revision(
455462
self,
456-
workbook_id,
457-
revision_number,
458-
filepath=None,
459-
include_extract=True,
460-
no_extract=None,
461-
):
463+
workbook_id: str,
464+
revision_number: str,
465+
filepath: Optional[PathOrFile] = None,
466+
include_extract: bool = True,
467+
no_extract: Optional[bool] = None,
468+
) -> str:
462469
if not workbook_id:
463470
error = "Workbook ID undefined."
464471
raise ValueError(error)
@@ -495,3 +502,12 @@ def download_revision(
495502
)
496503
)
497504
return os.path.abspath(download_path)
505+
506+
@api(version="2.3")
507+
def delete_revision(self, workbook_id: str, revision_number: str) -> None:
508+
if workbook_id is None or revision_number is None:
509+
raise ValueError
510+
url = "/".join([self.baseurl, workbook_id, "revisions", revision_number])
511+
512+
self.delete_request(url)
513+
logger.info("Deleted single workbook revsision (ID: {0}) (Revision: {1})".format(workbook_id, revision_number))

test/assets/datasource_revision.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<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">
3+
<pagination pageNumber="1" pageSize="100" totalAvailable="3" />
4+
<revisions>
5+
<revision createdAt="2016-07-26T20:34:56Z" revisionNumber="1" isDeleted="false">
6+
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="Cassie"/>
7+
</revision>
8+
<revision createdAt="2016-07-27T20:34:56Z" revisionNumber="2" isDeleted="false">
9+
</revision>
10+
<revision createdAt="2016-07-28T20:34:56Z" revisionNumber="3" isCurrent="true">
11+
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="Cassie"/>
12+
</revision>
13+
</revisions>
14+
</tsResponse>

test/assets/workbook_revision.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<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">
3+
<pagination pageNumber="1" pageSize="100" totalAvailable="3" />
4+
<revisions>
5+
<revision createdAt="2016-07-26T20:34:56Z" revisionNumber="1" isDeleted="false">
6+
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="Cassie"/>
7+
</revision>
8+
<revision createdAt="2016-07-27T20:34:56Z" revisionNumber="2" isDeleted="false">
9+
</revision>
10+
<revision createdAt="2016-07-28T20:34:56Z" revisionNumber="3" isCurrent="true">
11+
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="Cassie"/>
12+
</revision>
13+
</revisions>
14+
</tsResponse>

test/test_datasource.py

Lines changed: 46 additions & 0 deletions
10000
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import requests_mock
66
import xml.etree.ElementTree as ET
77
from zipfile import ZipFile
8+
import tempfile
89

910
import tableauserverclient as TSC
1011
from tableauserverclient.datetime_helpers import format_datetime
@@ -22,6 +23,7 @@
2223
PUBLISH_XML = 'datasource_publish.xml'
2324
PUBLISH_XML_ASYNC = 'datasource_publish_async.xml'
2425
REFRESH_XML = 'datasource_refresh.xml'
26+
REVISION_XML = 'datasource_revision.xml'
2527
UPDATE_XML = 'datasource_update.xml'
2628
UPDATE_HYPER_DATA_XML = 'datasource_data_update.xml'
2729
UPDATE_CONNECTION_XML = 'datasource_connection_update.xml'
@@ -600,3 +602,47 @@ def test_create_extracts_encrypted(self) -> None:
600602
m.post(self.baseurl + '/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract',
601603
status_code=200, text=response_xml)
602604
self.server.datasources.create_extract('3cc6cd06-89ce-4fdc-b935-5294135d6d42', True)
605+
606+
def test_revisions(self) -> None:
607+
datasource = TSC.DatasourceItem('project', 'test')
608+
datasource._id = '06b944d2-959d-4604-9305-12323c95e70e'
609+
610+
response_xml = read_xml_asset(REVISION_XML)
611+
with requests_mock.mock() as m:
612+
m.get("{0}/{1}/revisions".format(self.baseurl, datasource.id), text=response_xml)
613+
self.server.datasources.populate_revisions(datasource)
614+
revisions = datasource.revisions
615+
616+
self.assertEqual(len(revisions), 3)
617+
self.assertEqual("2016-07-26T20:34:56Z", format_datetime(revisions[0].created_at))
618+
self.assertEqual("2016-07-27T20:34:56Z", format_datetime(revisions[1].created_at))
619+
self.assertEqual("2016-07-28T20:34:56Z", format_datetime(revisions[2].created_at))
620+
621+
self.assertEqual(False, revisions[0].deleted)
622+
self.assertEqual(False, revisions[0].current)
623+
self.assertEqual(False, revisions[1].deleted)
624+
self.assertEqual(False, revisions[1].current)
625+
self.assertEqual(False, revisions[2].deleted)
626+
self.assertEqual(True, revisions[2].current)
627+
628+
self.assertEqual("Cassie", revisions[0].user_name)
629+
self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", revisions[0].user_id)
630+
self.assertIsNone(revisions[1].user_name)
631+
self.assertIsNone(revisions[1].user_id)
632+
self.assertEqual("Cassie", revisions[2].user_name)
633+
self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", revisions[2].user_id)
634+
635+
def test_delete_revision(self) -> None:
636+
datasource = TSC.DatasourceItem('project', 'test')
637+
datasource._id = '06b944d2-959d-4604-9305-12323c95e70e'
638+
639+
with requests_mock.mock() as m:
640+
m.delete("{0}/{1}/revisions/3".format(self.baseurl, datasource.id))
641+
self.server.datasources.delete_revision(datasource.id, "3")
642+
643+
def test_download_revision(self) -> None:
644+
with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td:
645+
m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/revisions/3/content',
646+
headers={'Content-Disposition': 'name="tableau_datasource"; filename="Sample datasource.tds"'})
647+
file_path = self.server.datasources.download_revision('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', "3", td)
648+
self.assertTrue(os.path.exists(file_path))

0 commit comments

Comments
 (0)
0