8000 Push code for 0.25 with custom views (#1206) · tableau/server-client-python@e4fbe41 · GitHub
[go: up one dir, main page]

Skip to content

Commit e4fbe41

Browse files
jacalataMrwanBaghdadjorwoodsbcantoniTrimPeachu
authored
Push code for 0.25 with custom views (#1206)
* Implement custom view objects (#1195) * Fix bug in update-datasources before 3.15 (#1203) (fixes #1072) * catch exceptions from ServerInfo (#1204) * add query-tagging attribute to connection (#1202) (add explanation for why it doesn't work on hyper) --------- Co-authored-by: Marwan Baghdad <mrwanbaghdad76@gmail.com> Co-authored-by: jorwoods <jorwoods@users.noreply.github.com> Co-authored-by: Brian Cantoni <bcantoni@salesforce.com> Co-authored-by: TrimPeachu <77048868+TrimPeachu@users.noreply.github.com> Co-authored-by: Stu Tomlinson <stu@nosnilmot.com> Co-authored-by: Jeremy Harris <jercharris89@gmail.com>
1 parent ccdd790 commit e4fbe41

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+963
-426
lines changed

.github/workflows/code-coverage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
strategy:
1111
fail-fast: false
1212
matrix:
13-
os: [ubuntu-latest, macos-latest, windows-latest]
13+
os: [ubuntu-latest]
1414
python-version: ['3.10']
1515

1616
runs-on: ${{ matrix.os }}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ classifiers = [
3030
repository = "https://github.com/tableau/server-client-python"
3131

3232
[project.optional-dependencies]
33-
test = ["argparse", "black", "mock", "mypy", "pytest>=7.0", "requests-mock>=1.0,<2.0"]
33+
test = ["argparse", "black", "mock", "mypy", "pytest>=7.0", "pytest-subtests", "requests-mock>=1.0,<2.0"]
3434

3535
[tool.black]
3636
line-length = 120

samples/explore_workbook.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def main():
7272
if all_workbooks:
7373
# Pick one workbook from the list
7474
sample_workbook = all_workbooks[0]
75+
sample_workbook.name = "Name me something cooler"
76+
sample_workbook.description = "That doesn't work"
77+
updated: TSC.WorkbookItem = server.workbooks.update(sample_workbook)
78+
print(updated.name, updated.description)
7579

7680
# Populate views
7781
server.workbooks.populate_views(sample_workbook)
@@ -125,6 +129,31 @@ def main():
125129
f.write(sample_workbook.preview_image)
126130
print("\nDownloaded preview image of workbook to {}".format(os.path.abspath(args.preview_image)))
127131

132+
# get custom views
133+
cvs, _ = server.custom_views.get()
134+
for c in cvs:
135+
print(c)
136+
137+
# for the last custom view in the list
138+
139+
# update the name
140+
# note that this will fail if the name is already changed to this value
141+
changed: TSC.CustomViewItem(id=c.id, name="I was updated by tsc")
142+
verified_change = server.custom_views.update(changed)
143+
print(verified_change)
144+
145+
# export as image. Filters etc could be added here as usual
146+
server.custom_views.populate_image(c)
147+
filename = c.id + "-image-export.png"
148+
with open(filename, "wb") as f:
149+
f.write(c.image)
150+
print("saved to " + filename)
151+
152+
if args.delete:
153+
print("deleting {}".format(c.id))
154+
unlucky = TSC.CustomViewItem(c.id)
155+
server.custom_views.delete(unlucky.id)
156+
128157

129158
if __name__ == "__main__":
130159
main()

tableauserverclient/__init__.py

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,6 @@
11
from ._version import get_versions
22
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
3-
from .models import (
4-
BackgroundJobItem,
5-
ColumnItem,
6-
ConnectionCredentials,
7-
ConnectionItem,
8-
DQWItem,
9-
DailyInterval,
10-
DataAlertItem,
11-
DatabaseItem,
12-
DatasourceItem,
13-
FlowItem,
14-
FlowRunItem,
15-
GroupItem,
16-
HourlyInterval,
17-
IntervalItem,
18-
JobItem,
19-
MetricItem,
20-
MonthlyInterval,
21-
PaginationItem,
22-
Permission,
23-
PermissionsRule,
24-
PersonalAccessTokenAuth,
25-
ProjectItem,
26-
RevisionItem,
27-
ScheduleItem,
28-
SiteItem,
29-
SubscriptionItem,
30-
TableItem,
31-
TableauAuth,
32-
Target,
33-
TaskItem,
34-
UnpopulatedPropertyError,
35-
UserItem,
36-
ViewItem,
37-
WebhookItem,
38-
WeeklyInterval,
39-
WorkbookItem,
40-
)
3+
from .models import *
414
from .server import (
425
CSVRequestOptions,
436
ExcelRequestOptions,

tableauserverclient/models/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from .column_item import ColumnItem
22
from .connection_credentials import ConnectionCredentials
33
from .connection_item import ConnectionItem
4+
from .custom_view_item import CustomViewItem
45
from .data_acceleration_report_item import DataAccelerationReportItem
56
from .data_alert_item import DataAlertItem
67
from .database_item import DatabaseItem
78
from .datasource_item import DatasourceItem
89
from .dqw_item import DQWItem
910
from .exceptions import UnpopulatedPropertyError
1011
from .favorites_item import FavoriteItem
12+
from .fileupload_item import FileuploadItem
1113
from .flow_item import FlowItem
1214
from .flow_run_item import FlowRunItem
1315
from .group_item import GroupItem
@@ -31,6 +33,7 @@
3133
from .table_item import TableItem
3234
from .tableau_auth import Credentials, TableauAuth, PersonalAccessTokenAuth
3335
from .tableau_types import Resource, TableauItem, plural_type
36+
from .tag_item import TagItem
3437
from .target import Target
3538
from .task_item import TaskItem
3639
from .user_item import UserItem

tableauserverclient/models/connection_item.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from typing import TYPE_CHECKING, List, Optional
1+
import logging
2+
from typing import List, Optional
3+
24
from defusedxml.ElementTree import fromstring
35

46
from .connection_credentials import ConnectionCredentials
5-
6-
if TYPE_CHECKING:
7-
from tableauserverclient.models.connection_credentials import ConnectionCredentials
7+
from .property_decorators import property_is_boolean
88

99

1010
class ConnectionItem(object):
@@ -18,7 +18,8 @@ def __init__(self):
1818
self.server_address: Optional[str] = None
1919
self.server_port: Optional[str] = None
2020
self.username: Optional[str] = None
21-
self.connection_credentials: Optional["ConnectionCredentials"] = None
21+
self.connection_credentials: Optional[ConnectionCredentials] = None
22+
self._query_tagging: Optional[bool] = None
2223

2324
@property
2425
def datasource_id(self) -> Optional[str]:
@@ -36,6 +37,22 @@ def id(self) -> Optional[str]:
3637
def connection_type(self) -> Optional[str]:
3738
return self._connection_type
3839

40+
@property
41+
def query_tagging(self) -> Optional[bool]:
42+
return self._query_tagging
43+
44+
@query_tagging.setter
45+
@property_is_boolean
46+
def query_tagging(self, value: Optional[bool]):
47+
# if connection type = hyper, Snowflake, or Teradata, we can't change this value: it is always true
48+
if self._connection_type in ["hyper", "snowflake", "teradata"]:
49+
logger = logging.getLogger("tableauserverclient.models.connection_item")
50+
logger.debug(
51+
"Cannot update value: Query tagging is always enabled for {} connections".format(self._connection_type)
52+
)
53+
return
54+
self._query_tagging = value
55+
3956
def __repr__(self):
4057
return "<ConnectionItem#{_id} embed={embed_password} type={_connection_type} username={username}>".format(
4158
**self.__dict__
@@ -54,6 +71,7 @@ def from_response(cls, resp, ns) -> List["ConnectionItem"]:
5471
connection_item.server_address = connection_xml.get("serverAddress", None)
5572
connection_item.server_port = connection_xml.get("serverPort", None)
5673
connection_item.username = connection_xml.get("userName", None)
74+
connection_item._query_tagging = string_to_bool(connection_xml.get("queryTaggingEnabled", None))
5775
datasource_elem = connection_xml.find(".//t:datasource", namespaces=ns)
5876
if datasource_elem is not None:
5977
connection_item._datasource_id = datasource_elem.get("id", None)
@@ -94,4 +112,4 @@ def from_xml_element(cls, parsed_response, ns) -> List["ConnectionItem"]:
94112

95113
# Used to convert string represented boolean to a boolean type
96114
def string_to_bool(s: str) -> bool:
97-
return s.lower() == "true"
115+
return s is not None and s.lower() == "true"
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from datetime import datetime
2+
3+
from defusedxml import ElementTree
4+
from defusedxml.ElementTree import fromstring, tostring
5+
from typing import Callable, List, Optional
6+
7+
from .exceptions import UnpopulatedPropertyError
8+
from .user_item import UserItem
9+
from .view_item import ViewItem
10+
from .workbook_item import WorkbookItem
11+
from ..datetime_helpers import parse_datetime
12+
13+
14+
class CustomViewItem(object):
15+
def __init__(self, id: Optional[str] = None, name: Optional[str] = None) -> None:
16+
self._content_url: Optional[str] = None # ?
17+
self._created_at: Optional["datetime"] = None
18+
self._id: Optional[str] = id
19+
self._image: Optional[Callable[[], bytes]] = None
20+
self._name: Optional[str] = name
21+
self._shared: Optional[bool] = False
22+
self._updated_at: Optional["datetime"] = None
23+
24+
self._owner: Optional[UserItem] = None
25+
self._view: Optional[ViewItem] = None
26+
self._workbook: Optional[WorkbookItem] = None
27+
28+
def __repr__(self: "CustomViewItem"):
29+
view_info = ""
30+
if self._view:
31+
view_info = " view='{}'".format(self._view.name or self._view.id or "unknown")
32+
wb_info = ""
33+
if self._workbook:
34+
wb_info = " workbook='{}'".format(self._workbook.name or self._workbook.id or "unknown")
35+
owner_info = ""
36+
if self._owner:
37+
owner_info = " owner='{}'".format(self._owner.name or self._owner.id or "unknown")
38+
return "<CustomViewItem id={} name=`{}`{}{}{}>".format(self.id, self.name, view_info, wb_info, owner_info)
39+
40+
def _set_image(self, image):
41+
self._image = image
42+
43+
@property
44+
def content_url(self) -> Optional[str]:
45+
return self._content_url
46+
47+
@property
48+
def created_at(self) -> Optional["datetime"]:
49+
return self._created_at
50+
51+
@property
52+
def id(self) -> Optional[str]:
53+
return self._id
54+
55+
@property
56+
def image(self) -> bytes:
57+
if self._image is None:
58+
error = "View item must be populated with its png image first."
59+
raise UnpopulatedPropertyError(error)
60+
return self._image()
61+
62+
@property
63+
def name(self) -> Optional[str]:
64+
return self._name
65+
66+
@name.setter
67+
def name(self, value: str):
68+
self._name = value
69+
70+
@property
71+
def shared(self) -> Optional[bool]:
72+
return self._shared
73+
74+
@shared.setter
75+
def shared(self, value: bool):
76+
self._shared = value
77+
78+
@property
79+
def updated_at(self) -> Optional["datetime"]:
80+
return self._updated_at
81+
82+
@property
83+
def owner(self) -> Optional[UserItem]:
84+
return self._owner
85+
86+
@owner.setter
87+
def owner(self, value: UserItem):
88+
self._owner = value
89+
90+
@property
91+
def workbook(self) -> Optional[WorkbookItem]:
92+
return self._workbook
93+
94+
@property
95+
def view(self) -> Optional[ViewItem]:
96+
return self._view
97+
98+
@classmethod
99+
def from_response(cls, resp, ns, workbook_id="") -> Optional["CustomViewItem"]:
100+
item = cls.list_from_response(resp, ns, workbook_id)
101+
if not item or len(item) == 0:
102+
return None
103+
else:
104+
return item[0]
105+
106+
@classmethod
107+
def list_from_response(cls, resp, ns, workbook_id="") -> List["CustomViewItem"]:
108+
return cls.from_xml_element(fromstring(resp), ns, workbook_id)
109+
110+
"""
111+
<customView
112+
id="37d015c6-bc28-4c88-989c-72c0a171f7aa"
113+
name="New name 2"
114+
createdAt="2016-02-03T23:35:09Z"
115+
updatedAt="2022-09-28T23:56:01Z"
116+
shared="false">
117+
<view id="8e33ff19-a7a4-4aa5-9dd8-a171e2b9c29f" name="circle"/>
118+
<workbook id="2fbe87c9-a7d8-45bf-b2b3-877a26ec9af5" name="marks and viz types 2"/>
119+
<owner id="cdfe8548-84c8-418e-9b33-2c0728b2398a" name="workgroupuser"/>
120+
</customView>
121+
"""
122+
123+
@classmethod
124+
def from_xml_element(cls, parsed_response, ns, workbook_id="") -> List["CustomViewItem"]:
125+
all_view_items = list()
126+
all_view_xml = parsed_response.findall(".//t:customView", namespaces=ns)
127+
for custom_view_xml in all_view_xml:
128+
cv_item = cls()
129+
view_elem: ElementTree = custom_view_xml.find(".//t:view", namespaces=ns)
130+
workbook_elem: str = custom_view_xml.find(".//t:workbook", namespaces=ns)
131+
owner_elem: str = custom_view_xml.find(".//t:owner", namespaces=ns)
132+
cv_item._created_at = parse_datetime(custom_view_xml.get("createdAt", None))
133+
cv_item._updated_at = parse_datetime(custom_view_xml.get("updatedAt", None))
134+
cv_item._content_url = custom_view_xml.get("contentUrl", None)
135+
cv_item._id = custom_view_xml.get("id", None)
136+
cv_item._name = custom_view_xml.get("name", None)
137+
138+
if owner_elem is not None:
139+
parsed_owners = UserItem.from_response_as_owner(tostring(custom_view_xml), ns)
140+
if parsed_owners and len(parsed_owners) > 0:
141+
cv_item._owner = parsed_owners[0]
142+
143+
if view_elem is not None:
144+
parsed_views = ViewItem.from_response(tostring(custom_view_xml), ns)
145+
if parsed_views and len(parsed_views) > 0:
146+
cv_item._view = parsed_views[0]
147+
148+
if workbook_id:
149+
cv_item._workbook = WorkbookItem(workbook_id)
150+
elif workbook_elem is not None:
151+
parsed_workbooks = WorkbookItem.from_response(tostring(custom_view_xml), ns)
152+
if parsed_workbooks and len(parsed_workbooks) > 0:
153+
cv_item._workbook = parsed_workbooks[0]
154+
155+
all_view_items.append(cv_item)
156+
return all_view_items

tableauserverclient/models/data_alert_item.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import List, Optional, TYPE_CHECKING
1+
from datetime import datetime
2+
from typing import List, Optional
23

34
from defusedxml.ElementTree import fromstring
45

@@ -8,15 +9,6 @@
89
property_is_boolean,
910
)
1011

11-
if TYPE_CHECKING:
12-
from datetime import datetime
13-
14-
15-
from typing import List, Optional, TYPE_CHECKING
16-
17-
if TYPE_CHECKING:
18-
from datetime import datetime
19-
2012

2113
class DataAlertItem(object):
2214
class Frequency:
@@ -30,8 +22,8 @@ def __init__(self):
3022
self._id: Optional[str] = None
3123
self._subject: Optional[str] = None
3224
self._creatorId: Optional[str] = None
33-
self._createdAt: Optional["datetime"] = None
34-
self._updatedAt: Optional["datetime"] = None
25+
self._createdAt: Optional[datetime] = None
26+
self._updatedAt: Optional[datetime] = None
3527
self._frequency: Optional[str] = None
3628
self._public: Optional[bool] = None
3729
self._owner_id: Optional[str] = None
@@ -90,11 +82,11 @@ def recipients(self) -> List[str]:
9082
return self._recipients or list()
9183

9284
@property
93-
def createdAt(self) -> Optional["datetime"]:
85+
def createdAt(self) -> Optional[datetime]:
9486
return self._createdAt
9587

9688
@property
97-
def updatedAt(self) -> Optional["datetime"]:
89+
def updatedAt(self) -> Optional[datetime]:
9890
return self._updatedAt
9991

10092
@property

0 commit comments

Comments
 (0)
0