Skip to content

Commit 308d1a1

Browse files
committed
Merge branch 'development' into bcantoni/pytest-parallel
2 parents b51baa0 + d61ff8f commit 308d1a1

31 files changed

+2508
-2163
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#ECCN:Open Source
2+
#GUSINFO:Open Source,Open Source Workflow

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ repository = "https://github.com/tableau/server-client-python"
3333

3434
[project.optional-dependencies]
3535
test = ["black==24.8", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests",
36-
"pytest-xdist", "requests-mock>=1.0,<2.0"]
37-
36+
"pytest-xdist", "requests-mock>=1.0,<2.0", "types-requests>=2.32.4.20250913"]
3837
[tool.black]
3938
line-length = 120
4039
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']

samples/create_user.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
####
2+
# This script demonstrates how to create a user using the Tableau
3+
# Server Client.
4+
#
5+
# To run the script, you must have installed Python 3.7 or later.
6+
####
7+
8+
9+
import argparse
10+
import logging
11+
import os
12+
import sys
13+
from typing import Sequence
14+
15+
import tableauserverclient as TSC
16+
17+
18+
def parse_args(args: Sequence[str] | None) -> argparse.Namespace:
19+
"""
20+
Parse command line parameters
21+
"""
22+
if args is None:
23+
args = sys.argv[1:]
24+
parser = argparse.ArgumentParser(description="Creates a sample user group.")
25+
# Common options; please keep those in sync across all samples
26+
parser.add_argument("--server", "-s", help="server address")
27+
parser.add_argument("--site", "-S", help="site name")
28+
parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server")
29+
parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server")
30+
parser.add_argument(
31+
"--logging-level",
32+
"-l",
33+
choices=["debug", "info", "error"],
34+
default="error",
35+
help="desired logging level (set to error by default)",
36+
)
37+
# Options specific to this sample
38+
# This sample has no additional options, yet. If you add some, please add them here
39+
parser.add_argument("--role", "-r", help="Site Role for the new user", default="Unlicensed")
40+
parser.add_argument(
41+
"--user",
42+
"-u",
43+
help="Username for the new user. If using active directory, it should be in the format of SAMAccountName@FullyQualifiedDomainName",
44+
)
45+
parser.add_argument(
46+
"--email", "-e", help="Email address of the new user. If using active directory, this field is optional."
47+
)
48+
49+
return parser.parse_args(args)
50+
51+
52+
def main():
53+
args = parse_args(None)
54+
55+
# Set logging level based on user input, or error by default
56+
logging_level = getattr(logging, args.logging_level.upper())
57+
logging.basicConfig(level=logging_level)
58+
59+
tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
60+
server = TSC.Server(args.server, use_server_version=True, http_options={"verify": False})
61+
with server.auth.sign_in(tableau_auth):
62+
# this code shows 2 different error codes for common mistakes
63+
# 400013: Invalid site role
64+
# 409000: user already exists on site
65+
66+
user = TSC.UserItem(args.user, args.role)
67+
if args.email:
68+
user.email = args.email
69+
user = server.users.add(user)
70+
71+
72+
if __name__ == "__main__":
73+
main()
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
####
2+
# This script demonstrates how to use the metadata API to query information on a published data source
3+
#
4+
# To run the script, you must have installed Python 3.7 or later.
5+
####
6+
7+
import argparse
8+
import logging
9+
from pprint import pprint
10+
11+
import tableauserverclient as TSC
12+
13+
14+
def main():
15+
parser = argparse.ArgumentParser(description="Use the metadata API to get information on a published data source.")
16+
# Common options; please keep those in sync across all samples
17+
parser.add_argument("--server", "-s", help="server address")
18+
parser.add_argument("--site", "-S", help="site name")
19+
parser.add_argument("--token-name", "-n", help="name of the personal access token used to sign into the server")
20+
parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server")
21+
parser.add_argument(
22+
"--logging-level",
23+
"-l",
24+
choices=["debug", "info", "error"],
25+
default="error",
26+
help="desired logging level (set to error by default)",
27+
)
28+
# Options specific to this sample
29+
parser.add_argument(
30+
"datasource_name",
31+
nargs="?",
32+
help="The name of the published datasource. If not present, we query all data sources.",
33+
)
34+
35+
args = parser.parse_args()
36+
37+
# Set logging level based on user input, or error by default
38+
logging_level = getattr(logging, args.logging_level.upper())
39+
logging.basicConfig(level=logging_level)
40+
41+
# Sign in to server
42+
tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
43+
server = TSC.Server(args.server, use_server_version=True)
44+
with server.auth.sign_in(tableau_auth):
45+
# Execute the query
46+
result = server.metadata.query(
47+
"""
48+
# Query must declare that it accepts first and afterToken variables
49+
query paged($first:Int, $afterToken:String) {
50+
workbooksConnection(first: $first, after:$afterToken) {
51+
nodes {
52+
luid
53+
name
54+
projectName
55+
description
56+
}
57+
totalCount
58+
pageInfo {
59+
endCursor
60+
hasNextPage
61+
}
62+
}
63+
}
64+
""",
65+
# "first" adjusts the page size. Here we set it to 5 to demonstrate pagination.
66+
# Set it to a higher number to reduce the number of pages. Including
67+
# first and afterToken is optional, and if not included, TSC will
68+
# use its default page size of 100.
69+
variables={"first": 5, "afterToken": None},
70+
)
71+
72+
# Multiple pages are captured in result["pages"]. Each page contains
73+
# the result of one execution of the query above.
74+
for page in result["pages"]:
75+
# Display warnings/errors (if any)
76+
if page.get("errors"):
77+
print("### Errors/Warnings:")
78+
pprint(result["errors"])
79+
80+
# Print the results
81+
if result.get("data"):
82+
print("### Results:")
83+
pprint(result["data"]["workbooksConnection"]["nodes"])
84+
85+
86+
if __name__ == "__main__":
87+
main()

tableauserverclient/models/datasource_item.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,13 +535,15 @@ def _parse_element(datasource_xml: ET.Element, ns: dict) -> tuple:
535535

536536
project_id = None
537537
project_name = None
538+
project = None
538539
project_elem = datasource_xml.find(".//t:project", namespaces=ns)
539540
if project_elem is not None:
540541
project = ProjectItem.from_xml(project_elem, ns)
541542
project_id = project_elem.get("id", None)
542543
project_name = project_elem.get("name", None)
543544

544545
owner_id = None
546+
owner = None
545547
owner_elem = datasource_xml.find(".//t:owner", namespaces=ns)
546548
if owner_elem is not None:
547549
owner = UserItem.from_xml(owner_elem, ns)

tableauserverclient/models/group_item.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Callable, Optional, TYPE_CHECKING
22

33
from defusedxml.ElementTree import fromstring
4+
from typing_extensions import Self
45

56
from .exceptions import UnpopulatedPropertyError
67
from .property_decorators import property_not_empty, property_is_enum
@@ -157,3 +158,8 @@ def from_response(cls, resp, ns) -> list["GroupItem"]:
157158
@staticmethod
158159
def as_reference(id_: str) -> ResourceReference:
159160
return ResourceReference(id_, GroupItem.tag_name)
161+
162+
def to_reference(self: Self) -> ResourceReference:
163+
if self.id is None:
164+
raise ValueError(f"{self.__class__.__qualname__} must have id to be converted to reference")
165+
return ResourceReference(self.id, self.tag_name)

tableauserverclient/models/groupset_item.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import xml.etree.ElementTree as ET
33

44
from defusedxml.ElementTree import fromstring
5+
from typing_extensions import Self
56

67
from tableauserverclient.models.group_item import GroupItem
78
from tableauserverclient.models.reference_item import ResourceReference
@@ -59,3 +60,8 @@ def get_group(group_xml: ET.Element) -> GroupItem:
5960
@staticmethod
6061
def as_reference(id_: str) -> ResourceReference:
6162
return ResourceReference(id_, GroupSetItem.tag_name)
63+
64+
def to_reference(self: Self) -> ResourceReference:
65+
if self.id is None:
66+
raise ValueError(f"{self.__class__.__qualname__} must have id to be converted to reference")
67+
return ResourceReference(self.id, self.tag_name)
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from typing_extensions import Self
2+
3+
14
class ResourceReference:
2-
def __init__(self, id_, tag_name):
5+
def __init__(self, id_: str | None, tag_name: str) -> None:
36
self.id = id_
47
self.tag_name = tag_name
58

6-
def __str__(self):
9+
def __str__(self) -> str:
710
return f"<ResourceReference id={self._id} tag={self._tag_name}>"
811

912
__repr__ = __str__
@@ -13,18 +16,21 @@ def __eq__(self, other: object) -> bool:
1316
return False
1417
return (self.id == other.id) and (self.tag_name == other.tag_name)
1518

19+
def __hash__(self: Self) -> int:
20+
return hash((self.id, self.tag_name))
21+
1622
@property
17-
def id(self):
23+
def id(self) -> str | None:
1824
return self._id
1925

2026
@id.setter
21-
def id(self, value):
27+
def id(self, value: str | None) -> None:
2228
self._id = value
2329

2430
@property
25-
def tag_name(self):
31+
def tag_name(self) -> str:
2632
return self._tag_name
2733

2834
@tag_name.setter
29-
def tag_name(self, value):
35+
def tag_name(self, value: str) -> None:
3036
self._tag_name = value

tableauserverclient/models/user_item.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Optional, TYPE_CHECKING
66

77
from defusedxml.ElementTree import fromstring
8+
from typing_extensions import Self
89

910
from tableauserverclient.datetime_helpers import parse_datetime
1011
from tableauserverclient.models.site_item import SiteAuthConfiguration
@@ -377,6 +378,11 @@ def _parse_xml(cls, element_name, resp, ns):
377378
def as_reference(id_) -> ResourceReference:
378379
return ResourceReference(id_, UserItem.tag_name)
379380

381+
def to_reference(self: Self) -> ResourceReference:
382+
if self.id is None:
383+
raise ValueError(f"{self.__class__.__qualname__} must have id to be converted to reference")
384+
return ResourceReference(self.id, self.tag_name)
385+
380386
@staticmethod
381387
def _parse_element(user_xml, ns):
382388
id = user_xml.get("id", None)

tableauserverclient/server/endpoint/flow_task_endpoint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def baseurl(self) -> str:
1818
return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks/flows"
1919

2020
@api(version="3.22")
21-
def create(self, flow_item: TaskItem) -> TaskItem:
21+
def create(self, flow_item: TaskItem) -> bytes:
2222
if not flow_item:
2323
error = "No flow provided"
2424
raise ValueError(error)

0 commit comments

Comments
 (0)