10000 Push code for 0.25 with custom views by jacalata · Pull Request #1206 · tableau/server-client-python · GitHub
[go: up one dir, main page]

Skip to content

Push code for 0.25 with custom views #1206

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 29 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bf6a0e6
Jac/headers (#1117)
jacalata Sep 22, 2022
a62ad5a
Allow injection of sessions (#1111)
MrwanBaghdad Sep 23, 2022
d71b978
Jac/show server info (#1118)
jacalata Sep 23, 2022
a203a04
Fix bug in exposing ExcelRequestOptions and test (#1123)
jorwoods Sep 26, 2022
af80100
Fix a few pylint errors (#1124)
bcantoni Sep 27, 2022
ca4d79e
fix behavior when url has no protocol (#1125)
jacalata Oct 6, 2022
24a5518
Jac/smoke tests (#1115)
jacalata Oct 6, 2022
bad5db9
Add permission control for Data Roles and Metrics (Issue #1063) (#1120)
TrimPeachu Oct 6, 2022
14d1af6
run black for formatting
jacalata Oct 6, 2022
e91d741
Merge branch 'master' into development
jacalata Oct 8, 2022
173c22a
fix check for being on master
jacalata Oct 14, 2022
0bb9dd5
mypy no-implicit-optional (#1151)
jacalata Dec 13, 2022
504d9d4
add option to pass specific datasources (#1150)
jacalata Dec 15, 2022
16b1bdd
allow user agent to be set by caller (#1166)
jacalata Jan 6, 2023
23d110f
Merge branch 'master' into development
jacalata Jan 6, 2023
7ceed6c
Fix issues with connections publishing workbooks (#1171)
nosnilmot Jan 17, 2023
a8c663e
Allow download to file-like objects (#1172)
nosnilmot Jan 20, 2023
8000
d9f64e1
Add updated_at to JobItem class (#1182)
nosnilmot Jan 24, 2023
47eab0b
fix revision references where xml returned does not match docs (#1176)
jharris126 Feb 14, 2023
06e33fa
Do not create empty connections list (#1178)
jacalata Feb 14, 2023
0ee46b8
Merge branch 'master' into development
jacalata Feb 14, 2023
ad78db4
fix behavior when url has no protocol
jacalata Sep 27, 2022
b184ba1
Jac/absolute imports (#1193)
jacalata Feb 17, 2023
2d0e4e3
Jac/repr for models (#1191)
jacalata Feb 21, 2023
c7d0ba5
Implement custom view objects (#1195)
jacalata Feb 21, 2023
83c216e
Fix bug in update-datasources before 3.15 (#1203)
jacalata Mar 7, 2023
ffa2d49
catch exceptions from ServerInfo (#1204)
jacalata Mar 8, 2023
ec37de2
add query-tagging attribute to connection (#1202)
jacalata Mar 9, 2023
2cc27c6
Merge branch 'master' into development
jacalata Mar 9, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/code-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest]
python-version: ['3.10']

runs-on: ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ classifiers = [
repository = "https://github.com/tableau/server-client-python"

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

[tool.black]
line-length = 120
Expand Down
29 changes: 29 additions & 0 deletions samples/explore_workbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ def main():
if all_workbooks:
# Pick one workbook from the list
sample_workbook = all_workbooks[0]
sample_workbook.name = "Name me something cooler"
sample_workbook.description = "That doesn't work"
updated: TSC.WorkbookItem = server.workbooks.update(sample_workbook)
print(updated.name, updated.description)

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

# get custom views
cvs, _ = server.custom_views.get()
for c in cvs:
print(c)

# for the last custom view in the list

# update the name
# note that this will fail if the name is already changed to this value
changed: TSC.CustomViewItem(id=c.id, name="I was updated by tsc")
verified_change = server.custom_views.update(changed)
print(verified_change)

# export as image. Filters etc could be added here as usual
server.custom_views.populate_image(c)
filename = c.id + "-image-export.png"
with open(filename, "wb") as f:
f.write(c.image)
print("saved to " + filename)

if args.delete:
print("deleting {}".format(c.id))
unlucky = TSC.CustomViewItem(c.id)
server.custom_views.delete(unlucky.id)


if __name__ == "__main__":
main()
39 changes: 1 addition & 38 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,6 @@
from ._version import get_versions
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
from .models import (
BackgroundJobItem,
ColumnItem,
ConnectionCredentials,
ConnectionItem,
DQWItem,
DailyInterval,
DataAlertItem,
DatabaseItem,
DatasourceItem,
FlowItem,
FlowRunItem,
GroupItem,
HourlyInterval,
IntervalItem,
JobItem,
MetricItem,
MonthlyInterval,
PaginationItem,
Permission,
PermissionsRule,
PersonalAccessTokenAuth,
ProjectItem,
RevisionItem,
ScheduleItem,
SiteItem,
SubscriptionItem,
TableItem,
TableauAuth,
Target,
TaskItem,
UnpopulatedPropertyError,
UserItem,
ViewItem,
WebhookItem,
WeeklyInterval,
WorkbookItem,
)
from .models import *
from .server import (
CSVRequestOptions,
ExcelRequestOptions,
Expand Down
3 changes: 3 additions & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from .column_item import ColumnItem
from .connection_credentials import ConnectionCredentials
from .connection_item import ConnectionItem
from .custom_view_item import CustomViewItem
from .data_acceleration_report_item import DataAccelerationReportItem
from .data_alert_item import DataAlertItem
from .database_item import DatabaseItem
from .datasource_item import DatasourceItem
from .dqw_item import DQWItem
from .exceptions import UnpopulatedPropertyError
from .favorites_item import FavoriteItem
from .fileupload_item import FileuploadItem
from .flow_item import FlowItem
from .flow_run_item import FlowRunItem
from .group_item import GroupItem
Expand All @@ -31,6 +33,7 @@
from .table_item import TableItem
from .tableau_auth import Credentials, TableauAuth, PersonalAccessTokenAuth
from .tableau_types import Resource, TableauItem, plural_type
from .tag_item import TagItem
from .target import Target
from .task_item import TaskItem
from .user_item import UserItem
Expand Down
30 changes: 24 additions & 6 deletions tableauserverclient/models/connection_item.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import TYPE_CHECKING, List, Optional
import logging
from typing import List, Optional

from defusedxml.ElementTree import fromstring

from .connection_credentials import ConnectionCredentials

if TYPE_CHECKING:
from tableauserverclient.models.connection_credentials import ConnectionCredentials
from .property_decorators import property_is_boolean


class ConnectionItem(object):
Expand All @@ -18,7 +18,8 @@ def __init__(self):
self.server_address: Optional[str] = None
self.server_port: Optional[str] = None
self.username: Optional[str] = None
self.connection_credentials: Optional["ConnectionCredentials"] = None
self.connection_credentials: Optional[ConnectionCredentials] = None
self._query_tagging: Optional[bool] = None

@property
def datasource_id(self) -> Optional[str]:
Expand All @@ -36,6 +37,22 @@ def id(self) -> Optional[str]:
def connection_type(self) -> Optional[str]:
return self._connection_type

@property
def query_tagging(self) -> Optional[bool]:
return self._query_tagging

@query_tagging.setter
@property_is_boolean
def query_tagging(self, value: Optional[bool]):
# if connection type = hyper, Snowflake, or Teradata, we can't change this value: it is always true
if self._connection_type in ["hyper", "snowflake", "teradata"]:
logger = logging.getLogger("tableauserverclient.models.connection_item")
logger.debug(
"Cannot update value: Query tagging is always enabled for {} connections".format(self._connection_type)
)
return
self._query_tagging = value

def __repr__(self):
return "<ConnectionItem#{_id} embed={embed_password} type={_connection_type} username={username}>".format(
**self.__dict__
Expand All @@ -54,6 +71,7 @@ def from_response(cls, resp, ns) -> List["ConnectionItem"]:
connection_item.server_address = connection_xml.get("serverAddress", None)
connection_item.server_port = connection_xml.get("serverPort", None)
connection_item.username = connection_xml.get("userName", None)
connection_item._query_tagging = string_to_bool(connection_xml.get("queryTaggingEnabled", None))
datasource_elem = connection_xml.find(".//t:datasource", namespaces=ns)
if datasource_elem is not None:
connection_item._datasource_id = datasource_elem.get("id", None)
Expand Down Expand Up @@ -94,4 +112,4 @@ def from_xml_element(cls, parsed_response, ns) -> List["ConnectionItem"]:

# Used to convert string represented boolean to a boolean type
def string_to_bool(s: str) -> bool:
return s.lower() == "true"
return s is not None and s.lower() == "true"
156 changes: 156 additions & 0 deletions tableauserverclient/models/custom_view_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
from datetime import datetime

from defusedxml import ElementTree
from defusedxml.ElementTree import fromstring, tostring
from typing import Callable, List, Optional

from .exceptions import UnpopulatedPropertyError
from .user_item import UserItem
from .view_item import ViewItem
from .workbook_item import WorkbookItem
from ..datetime_helpers import parse_datetime


class CustomViewItem(object):
def __init__(self, id: Optional[str] = None, name: Optional[str] = None) -> None:
self._content_url: Optional[str] = None # ?
self._created_at: Optional["datetime"] = None
self._id: Optional[str] = id
self._image: Optional[Callable[[], bytes]] = None
self._name: Optional[str] = name
self._shared: Optional[bool] = False
self._updated_at: Optional["datetime"] = None

self._owner: Optional[UserItem] = None
self._view: Optional[ViewItem] = None
self._workbook: Optional[WorkbookItem] = None

def __repr__(self: "CustomViewItem"):
view_info = ""
if self._view:
view_info = " view='{}'".format(self._view.name or self._view.id or "unknown")
wb_info = ""
if self._workbook:
wb_info = " workbook='{}'".format(self._workbook.name or self._workbook.id or "unknown")
owner_info = ""
if self._owner:
owner_info = " owner='{}'".format(self._owner.name or self._owner.id or "unknown")
return "<CustomViewItem id={} name=`{}`{}{}{}>".format(self.id, self.name, view_info, wb_info, owner_info)

def _set_image(self, image):
self._image = image

@property
def content_url(self) -> Optional[str]:
return self._content_url

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

@property
def id(self) -> Optional[str]:
return self._id

@property
def image(self) -> bytes:
if self._image is None:
error = "View item must be populated with its png image first."
raise UnpopulatedPropertyError(error)
return self._image()

@property
def name(self) -> Optional[str]:
return self._name

@name.setter
def name(self, value: str):
self._name = value

@property
def shared(self) -> Optional[bool]:
return self._shared

@shared.setter
def shared(self, value: bool):
self._shared = value

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

@property
def owner(self) -> Optional[UserItem]:
return self._owner

@owner.setter
def owner(self, value: UserItem):
self._owner = value

@property
def workbook(self) -> Optional[WorkbookItem]:
return self._workbook

@property
def view(self) -> Optional[ViewItem]:
return self._view

@classmethod
def from_response(cls, resp, ns, workbook_id="") -> Optional["CustomViewItem"]:
item = cls.list_from_response(resp, ns, workbook_id)
if not item or len(item) == 0:
return None
else:
return item[0]

@classmethod
def list_from_response(cls, resp, ns, workbook_id="") -> List["CustomViewItem"]:
return cls.from_xml_element(fromstring(resp), ns, workbook_id)

"""
<customView
id="37d015c6-bc28-4c88-989c-72c0a171f7aa"
name="New name 2"
createdAt="2016-02-03T23:35:09Z"
updatedAt="2022-09-28T23:56:01Z"
shared="false">
<view id="8e33ff19-a7a4-4aa5-9dd8-a171e2b9c29f" name="circle"/>
<workbook id="2fbe87c9-a7d8-45bf-b2b3-877a26ec9af5" name="marks and viz types 2"/>
<owner id="cdfe8548-84c8-418e-9b33-2c0728b2398a" name="workgroupuser"/>
</customView>
"""

@classmethod
def from_xml_element(cls, parsed_response, ns, workbook_id="") -> List["CustomViewItem"]:
all_view_items = list()
all_view_xml = parsed_response.findall(".//t:customView", namespaces=ns)
for custom_view_xml in all_view_xml:
cv_item = cls()
view_elem: ElementTree = custom_view_xml.find(".//t:view", namespaces=ns)
workbook_elem: str = custom_view_xml.find(".//t:workbook", namespaces=ns)
owner_elem: str = custom_view_xml.find(".//t:owner", namespaces=ns)
cv_item._created_at = parse_datetime(custom_view_xml.get("createdAt", None))
cv_item._updated_at = parse_datetime(custom_view_xml.get("updatedAt", None))
cv_item._content_url = custom_view_xml.get("contentUrl", None)
cv_item._id = custom_view_xml.get("id", None)
cv_item._name = custom_view_xml.get("name", None)

if owner_elem is not None:
parsed_owners = UserItem.from_response_as_owner(tostring(custom_view_xml), ns)
if parsed_owners and len(parsed_owners) > 0:
cv_item._owner = parsed_owners[0]

if view_elem is not None:
parsed_views = ViewItem.from_response(tostring(custom_view_xml), ns)
if parsed_views and len(parsed_views) > 0:
cv_item._view = parsed_views[0]

if workbook_id:
cv_item._workbook = WorkbookItem(workbook_id)
elif workbook_elem is not None:
parsed_workbooks = WorkbookItem.from_response(tostring(custom_view_xml), ns)
if parsed_workbooks and len(parsed_workbooks) > 0:
cv_item._workbook = parsed_workbooks[0]

all_view_items.append(cv_item)
return all_view_items
20 changes: 6 additions & 14 deletions tableauserverclient/models/data_alert_item.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import List, Optional, TYPE_CHECKING
from datetime import datetime
from typing import List, Optional

from defusedxml.ElementTree import fromstring

Expand All @@ -8,15 +9,6 @@
property_is_boolean,
)

if TYPE_CHECKING:
from datetime import datetime


from typing import List, Optional, TYPE_CHECKING

if TYPE_CHECKING:
from datetime import datetime


class DataAlertItem(object):
class Frequency:
Expand All @@ -30,8 +22,8 @@ def __init__(self):
self._id: Optional[str] = None
self._subject: Optional[str] = None
self._creatorId: Optional[str] = None
self._createdAt: Optional["datetime"] = None
self._updatedAt: Optional["datetime"] = None
self._createdAt: Optional[datetime] = None
self._updatedAt: Optional[datetime] = None
self._frequency: Optional[str] = None
self._public: Optional[bool] = None
self._owner_id: Optional[str] = None
Expand Down Expand Up @@ -90,11 +82,11 @@ def recipients(self) -> List[str]:
return self._recipients or list()

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

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

@property
Expand Down
Loading
0