8000 Release with security and functionality updates (#1021) · tableau/server-client-python@1e55df6 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1e55df6

Browse files
committed
Release with security and functionality updates (#1021)
Switched to using defused_xml for xml attack protection added linting and type hints improve experience with self-signed certificates/invalid ssl updated samples new item types: metrics, revisions for datasources and workbooks features: support adding flows to schedules, exporting workbooks to powerpoint fixes: delete extracts
1 parent 4266120 commit 1e55df6

17 files changed

+105
-242
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Use the Tableau Server Client (TSC) library to increase your productivity as you
88
* Create users and groups.
99
* Query projects, sites, and more.
1010

11-
This repository contains Python source code for the library and sample files showing how to use it. As of May 2022, Python versions 3.7 and up are supported.
11+
This repository contains Python source code for the library and sample files showing how to use it. Python versions 3.6 and up are supported.
1212

1313
To see sample code that works directly with the REST API (in Java, Python, or Postman), visit the [REST API Samples](https://github.com/tableau/rest-api-samples) repo.
1414

samples/export.py

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This script demonstrates how to export a view using the Tableau
33
# Server Client.
44
#
5-
# To run the script, you must have installed Python 3.7 or later.
5+
# To run the script, you must have installed Python 3.6 or later.
66
####
77

88
import argparse
@@ -40,13 +40,10 @@ def main():
4040
group.add_argument(
4141
"--csv", dest="type", action="store_const", const=("populate_csv", "CSVRequestOptions", "csv", "csv")
4242
)
43-
# other options shown in explore_workbooks: workbook.download, workbook.preview_image
44-
45-
parser.add_argument("--workbook", action="store_true")
4643

4744
parser.add_argument("--file", "-f", help="filename to store the exported data")
4845
parser.add_argument("--filter", "-vf", metavar="COLUMN:VALUE", help="View filter to apply to the view")
49-
parser.add_argument("resource_id", help="LUID for the view or workbook")
46+
parser.add_argument("resource_id", help="LUID for the view")
5047

5148
args = parser.parse_args()
5249

@@ -55,46 +52,34 @@ def main():
5552
logging.basicConfig(level=logging_level)
5653

5754
tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
58-
server = TSC.Server(args.server, use_server_version=True, http_options={"verify": False})
55+
server = TSC.Server(args.server, use_server_version=True)
5956
with server.auth.sign_in(tableau_auth):
60-
print("Connected")
61-
if args.workbook:
62-
item = server.workbooks.get_by_id(args.resource_id)
63-
else:
64-
item = server.views.get_by_id(args.resource_id)
65-
66-
if not item:
67-
print("No item found for id {}".format(args.resource_id))
68-
exit(1)
57+
views = filter(lambda x: x.id == args.resource_id or x.name == args.resource_id, TSC.Pager(server.views.get))
58+
view = list(views).pop() # in python 3 filter() returns a filter object
6959

70-
print("Item found: {}".format(item.name))
7160
# We have a number of different types and functions for each different export type.
7261
# We encode that information above in the const=(...) parameter to the add_argument function to make
7362
# the code automatically adapt for the type of export the user is doing.
7463
# We unroll that information into methods we can call, or objects we can create by using getattr()
7564
(populate_func_name, option_factory_name, member_name, extension) = args.type
7665
populate = getattr(server.views, populate_func_name)
77-
if args.workbook:
78-
populate = getattr(server.workbooks, populate_func_name)
79-
8066
option_factory = getattr(TSC, option_factory_name)
8167

8268
if args.filter:
8369
options = option_factory().vf(*args.filter.split(":"))
8470
else:
8571
options = None
86-
8772
if args.file:
8873
filename = args.file
8974
else:
9075
filename = "out.{}".format(extension)
9176

92-
populate(item, options)
77+
populate(view, options)
9378
with open(filename, "wb") as f:
9479
if member_name == "csv":
95-
f.writelines(getattr(item, member_name))
80+
f.writelines(getattr(view, member_name))
9681
else:
97-
f.write(getattr(item, member_name))
82+
f.write(getattr(view, member_name))
9883
print("saved to " + filename)
9984

10085

setup.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# This makes work easier for offline installs or low bandwidth machines
1616
needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv)
1717
pytest_runner = ['pytest-runner'] if needs_pytest else []
18-
test_requirements = ['black', 'mock', 'pytest', 'requests-mock>=1.0,<2.0', 'mypy>=0.920']
18+
test_requirements = ['black', 'mock', 'pytest', 'requests-mock>=1.0,<2.0', 'mypy==0.910']
1919

2020
setup(
2121
name='tableauserverclient',
@@ -25,10 +25,7 @@
2525
author_email='github@tableau.com',
2626
url='https://github.com/tableau/server-client-python',
2727
package_data={'tableauserverclient':['py.typed']},
28-
packages=['tableauserverclient',
29-
'tableauserverclient.helpers',
30-
'tableauserverclient.models',
31-
'tableauserverclient.server',
28+
packages=['tableauserverclient', 'tableauserverclient.models', 'tableauserverclient.server',
3229
'tableauserverclient.server.endpoint'],
3330
license='MIT',
3431
description='A Python module for working with the Tableau Server REST API.',
@@ -40,7 +37,6 @@
4037
'defusedxml>=0.7.1',
4138
'requests>=2.11,<3.0',
4239
],
43-
python_requires='>3.7.0',
4440
tests_require=test_requirements,
4541
extras_require={
4642
'test': test_requirements

tableauserverclient/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
NotSignedInError,
5353
Pager,
5454
)
55-
from .helpers import *
5655

5756
__version__ = get_versions()["version"]
5857
__VERSION__ = __version__

tableauserverclient/models/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@
2222
from .metric_item import MetricItem
2323
from .pagination_item import PaginationItem
2424
from .permissions_item import PermissionsRule, Permission
25+
from .personal_access_token_auth import PersonalAccessTokenAuth
26+
from .personal_access_token_auth import PersonalAccessTokenAuth
2527
from .project_item import ProjectItem
2628
from .revision_item import RevisionItem
2729
from .schedule_item import ScheduleItem
2830
from .server_info_item import ServerInfoItem
2931
from .site_item import SiteItem
3032
from .subscription_item import SubscriptionItem
3133
from .table_item import TableItem
32-
from .tableau_auth import Credentials, TableauAuth, PersonalAccessTokenAuth
34+
from .tableau_auth import TableauAuth
3335
from .target import Target
3436
from .task_item import TaskItem
3537
from .user_item import UserItem

tableauserverclient/models/job_item.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ def __init__(
3131
finish_code: int = 0,
3232
notes: Optional[List[str]] = None,
3333
mode: Optional[str] = None,
34-
workbook_id: Optional[str] = None,
35-
datasource_id: Optional[str] = None,
3634
flow_run: Optional[FlowRunItem] = None,
3735
):
3836
self._id = id_
@@ -44,8 +42,6 @@ def __init__(
4442
self._finish_code = finish_code
4543
self._notes: List[str] = notes or []
4644
self._mode = mode
47-
self._workbook_id = workbook_id
48-
self._datasource_id = datasource_id
4945
self._flow_run = flow_run
5046

5147
@property
@@ -89,22 +85,6 @@ def mode(self, value: str) -> None:
8985
# check for valid data here
9086
self._mode = value
9187

92-
@property
93-
def workbook_id(self) -> Optional[str]:
94-
return self._workbook_id
95-
96-
@workbook_id.setter
97-
def workbook_id(self, value: Optional[str]) -> None:
98-
self._workbook_id = value
99-
100-
@property
101-
def datasource_id(self) -> Optional[str]:
102-
return self._datasource_id
103-
104-
@datasource_id.setter
105-
def datasource_id(self, value: Optional[str]) -> None:
106-
self._datasource_id = value
107-
10888
@property
10989
def flow_run(self):
11090
return self._flow_run
@@ -139,10 +119,6 @@ def _parse_element(cls, element, ns):
139119
finish_code = int(element.get("finishCode", -1))
140120
notes = [note.text for note in element.findall(".//t:notes", namespaces=ns)] or None
141121
mode = element.get("mode", None)
142-
workbook = element.find(".//t:workbook[@id]", namespaces=ns)
143-
workbook_id = workbook.get("id") if workbook is not None else None
144-
datasource = element.find(".//t:datasource[@id]", namespaces=ns)
145-
datasource_id = datasource.get("id") if datasource is not None else None
146122
flow_run = None
147123
for flow_job in element.findall(".//t:runFlowJobType", namespaces=ns):
148124
flow_run = FlowRunItem()
@@ -160,8 +136,6 @@ def _parse_element(cls, element, ns):
160136
finish_code,
161137
notes,
162138
mode,
163-
workbook_id,
164-
datasource_id,
165139
flow_run,
166140
)
167141

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,41 @@
1-
class Credentials:
2-
def __init__(self, site_id=None, user_id_to_impersonate=None):
3-
self.site_id = site_id or ""
4-
self.user_id_to_impersonate = user_id_to_impersonate or None
5-
6-
@property
7-
def credentials(self):
8-
credentials = "Credentials can be username/password, Personal Access Token, or JWT"
9-
+"This method returns values to set as an attribute on the credentials element of the request"
10-
11-
def __repr__(self):
12-
display = "All Credentials types must have a debug display that does not print secrets"
13-
14-
15-
def deprecate_site_attribute():
16-
import warnings
17-
18-
warnings.warn(
19-
"TableauAuth(..., site=...) is deprecated, " "please use TableauAuth(..., site_id=...) instead.",
20-
DeprecationWarning,
21-
)
22-
23-
24-
# The traditional auth type: username/password
25-
class TableauAuth(Credentials):
1+
class TableauAuth(object):
262
def __init__(self, username, password, site=None, site_id=None, user_id_to_impersonate=None):
273
if site is not None:
28-
deprecate_site_attribute()
4+
import warnings
5+
6+
warnings.warn(
7+
"TableauAuth(..., site=...) is deprecated, " "please use TableauAuth(..., site_id=...) instead.",
8+
DeprecationWarning,
9+
)
2910
site_id = site
30-
super().__init__(site_id, user_id_to_impersonate)
3111
if password is None:
3212
raise TabError("Must provide a password when using traditional authentication")
13+
14+
self.user_id_to_impersonate = user_id_to_impersonate
3315
self.password = password
16+
self.site_id = site_id if site_id is not None else ""
3417
self.username = username
3518

36-
@property
37-
def credentials(self):
38-
return {"name": self.username, "password": self.password}
39-
40-
def __repr__(self):
41-
return "<Credentials username={} password={}>".format(self.username, "<redacted>")
42-
4319
@property
4420
def site(self):
45-
deprecate_site_attribute()
21+
import warnings
22+
23+
warnings.warn(
24+
"TableauAuth.site is deprecated, use TableauAuth.site_id instead.",
25+
DeprecationWarning,
26+
)
4627
return self.site_id
4728

4829
@site.setter
4930
def site(self, value):
50-
deprecate_site_attribute()
51-
self.site_id = value
31+
import warnings
5232

53-
54-
class PersonalAccessTokenAuth(Credentials):
55-
def __init__(self, token_name, personal_access_token, site_id=None):
56-
super().__init__(site_id=site_id)
57-
self.token_name = token_name
58-
self.personal_access_token = personal_access_token
33+
warnings.warn(
34+
"TableauAuth.site is deprecated, use TableauAuth.site_id instead.",
35+
DeprecationWarning,
36+
)
37+
self.site_id = value
5938

6039
@property
6140
def credentials(self):
62-
return {
63-
"personalAccessTokenName": self.token_name,
64-
"personalAccessTokenSecret": self.personal_access_token,
65-
}
66-
67-
def __repr__(self):
68-
return "<PersonalAccessToken name={} token={} site={}>".format(
69-
self.token_name, self.personal_access_token[:2] + "...", self.site_id
70-
)
41+
return {"name": self.username, "password": self.password}

tableauserverclient/models/view_item.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import copy
2-
from typing import Callable, Generator, Iterator, List, Optional, Set, TYPE_CHECKING
2+
from typing import Callable, Iterable, List, Optional, Set, TYPE_CHECKING
33

44
from defusedxml.ElementTree import fromstring
55

@@ -24,8 +24,8 @@ def __init__(self) -> None:
2424
self._preview_image: Optional[Callable[[], bytes]] = None
2525
self._project_id: Optional[str] = None
2626
self._pdf: Optional[Callable[[], bytes]] = None
27-
self._csv: Optional[Callable[[], Iterator[bytes]]] = None
28-
self._excel: Optional[Callable[[], Iterator[bytes]]] = None
27+
self._csv: Optional[Callable[[], Iterable[bytes]]] = None
28+
self._excel: Optional[Callable[[], Iterable[bytes]]] = None
2929
self._total_views: Optional[int] = None
3030
self._sheet_type: Optional[str] = None
3131
self._updated_at: Optional["datetime"] = None
@@ -94,14 +94,14 @@ def pdf(self) -> bytes:
9494
return self._pdf()
9595

9696
@property
97-
def csv(self) -> Iterator[bytes]:
97+
def csv(self) -> Iterable[bytes]:
9898
if self._csv is None:
9999
error = "View item must be populated with its csv first."
100100
raise UnpopulatedPropertyError(error)
101101
return self._csv()
102102

103103
@property
104-
def excel(self) -> Iterator[bytes]:
104+
def excel(self) -> Iterable[bytes]:
105105
if self._excel is None:
106106
error = "View item must be populated with its excel first."
107107
raise UnpopulatedPropertyError(error)

tableauserverclient/models/workbook_item.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def project_id(self) -> Optional[str]:
124124
return self._project_id
125125

126126
@project_id.setter
127+
@property_not_nullable
127128
def project_id(self, value: str):
128129
self._project_id = value
129130

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ def update_hyper_data(
339339
*,
340340
request_id: str,
341341
actions: Sequence[Mapping],
342-
payload: Optional[FilePath] = None,
342+
payload: Optional[FilePath] = None
343343
) -> JobItem:
344344
if isinstance(datasource_or_connection_item, DatasourceItem):
345345
datasource_id = datasource_or_connection_item.id

0 commit comments

Comments
 (0)
0