Skip to content

Commit 371e4af

Browse files
authored
Merge branch 'development' into jac/csv-import-privacy
2 parents 33361af + e83d67a commit 371e4af

17 files changed

Lines changed: 676 additions & 26 deletions

‎.github/workflows/publish-pypi.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ jobs:
4747
uses: pypa/gh-action-pypi-publish@release/v1 # license BSD-2
4848
with:
4949
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
50-
repository_url: https://test.pypi.org/legacy/
51-
skip_existing: true
50+
repository-url: https://test.pypi.org/legacy/
51+
skip-existing: true
5252

5353
smoke-test-pypi:
5454
name: Smoke test from Test PyPI

‎.github/workflows/run-tests.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
os: [ubuntu-latest, macos-latest, windows-latest]
16-
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.14t']
16+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.14t', '3.15']
1717

1818
runs-on: ${{ matrix.os }}
1919

‎CHANGELOG.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,38 @@
1111
username instead of a raw slice, and the password column is replaced with
1212
`***` before any line reaches a log handler or the invalid-lines list.
1313
Fixes #1829.
14+
* Added Workload Identity Federation (WIF) support to the connection update
15+
endpoints. `ConnectionItem` has two new string properties,
16+
`workgroup_auth_mode` and `auth_federation_service_account`, that are
17+
emitted on the `<connection>` payload by `workbooks.update_connection`,
18+
`workbooks.update_connections`, `datasources.update_connection`,
19+
`datasources.update_connections`, and `flows.update_connection`. The two
20+
bulk endpoints also accept matching `workgroup_auth_mode` and
21+
`auth_federation_service_account` keyword arguments and apply them to
22+
every connection LUID in the request. Set `workgroup_auth_mode` to one of
23+
the values on the new nested value set
24+
`ConnectionItem.WorkgroupAuthMode`:
25+
- `ConnectionItem.WorkgroupAuthMode.WIF` =
26+
`"workload-identity-federation"` — direct WIF, e.g. Snowflake
27+
- `ConnectionItem.WorkgroupAuthMode.WIF_IMPERSONATION` =
28+
`"workload-identity-federation-impersonation"` — impersonation WIF,
29+
e.g. Google BigQuery
30+
Usage rules:
31+
- Resend `workgroup_auth_mode` on every subsequent update of a WIF
32+
connection; omitting it on a later update returns HTTP 400.
33+
- Leave `auth_type` unset (or `"oauth"`) when sending a WIF mode; the
34+
server derives the stored auth type from the mode.
35+
- Set `auth_federation_service_account` only with `WIF_IMPERSONATION`.
36+
The server rejects a service account with the direct `WIF` mode. TSC
37+
does not validate this combination client-side.
38+
- The server does not echo these attributes on GET responses today, so
39+
`ConnectionItem.workgroup_auth_mode` and
40+
`ConnectionItem.auth_federation_service_account` parsed from a response
41+
will typically be `None` even when the connection is WIF-configured.
42+
Requires Tableau Server REST API 3.30+ (Tableau Server release 26.3) with
43+
WIF configured and enabled on the site. Older servers silently ignore
44+
unknown connection attributes, so these fields are safe to leave unset on
45+
pre-3.30 targets.
1446
* Bumped the urllib3 floor to 2.6.3 to pick up the fix for CVE-2026-21441
1547
(GHSA-38jv-5279-wg99, 8.9 High): urllib3's streaming decompression
1648
safeguards were bypassed when HTTP redirects were followed. TSC's manual

‎CONTRIBUTORS.md‎

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,43 @@ The following people have contributed to this project to make it possible, and w
44

55
## Contributors
66

7-
* [jacalata](https://github.com/jacalata)
87
* [jorwoods](https://github.com/jorwoods)
8+
* [jacalata](https://github.com/jacalata)
99
* [t8y8](https://github.com/t8y8)
1010
* [bcantoni](https://github.com/bcantoni)
1111
* [shinchris](https://github.com/shinchris)
12+
* [vchavatapalli](https://github.com/vchavatapalli)
1213
* [vogelsgesang](https://github.com/vogelsgesang)
1314
* [lbrendanl](https://github.com/lbrendanl)
1415
* [LGraber](https://github.com/LGraber)
16+
* [dependabot[bot]](https://github.com/apps/dependabot)
1517
* [gaoang2148](https://github.com/gaoang2148)
16-
* [benlower](https://github.com/benlower)
1718
* [liu-rebecca](https://github.com/liu-rebecca)
19+
* [benlower](https://github.com/benlower)
1820
* [guodah](https://github.com/guodah)
19-
* [jdomingu](https://github.com/jdomingu)
2021
* [kykrueger](https://github.com/kykrueger)
22+
* [renoyjohnm](https://github.com/renoyjohnm)
23+
* [jdomingu](https://github.com/jdomingu)
2124
* [jz-huang](https://github.com/jz-huang)
2225
* [opus-42](https://github.com/opus-42)
23-
* [markm-io](https://github.com/markm-io)
24-
* [graysonarts](https://github.com/graysonarts)
26+
* [securehst](https://github.com/securehst)
2527
* [d45](https://github.com/d45)
26-
* [preguraman](https://github.com/preguraman)
28+
* [graysonarts](https://github.com/graysonarts)
2729
* [sotnich](https://github.com/sotnich)
30+
* [casey-crawford-cfa](https://github.com/casey-crawford-cfa)
31+
* [preguraman](https://github.com/preguraman)
2832
* [mmuttreja-tableau](https://github.com/mmuttreja-tableau)
29-
* [dependabot[bot]](https://github.com/apps/dependabot)
33+
* [vitorhonna](https://github.com/vitorhonna)
34+
* [TrimPeachu](https://github.com/TrimPeachu)
3035
* [scuml](https://github.com/scuml)
3136
* [ovinis](https://github.com/ovinis)
32-
* [FFMMM](https://github.com/FFMMM)
3337
* [martinbpeters](https://github.com/martinbpeters)
3438
* [talvalin](https://github.com/talvalin)
39+
* [FFMMM](https://github.com/FFMMM)
40+
* [Copilot](https://github.com/apps/copilot-swe-agent)
3541
* [dzucker-tab](https://github.com/dzucker-tab)
3642
* [a-torres-2](https://github.com/a-torres-2)
43+
* [valsarajnivea](https://github.com/valsarajnivea)
3744
* [nnevalainen](https://github.com/nnevalainen)
3845
* [mbren](https://github.com/mbren)
3946
* [wolkiewiczk](https://github.com/wolkiewiczk)
@@ -51,32 +58,41 @@ The following people have contributed to this project to make it possible, and w
5158
* [annematronic](https://github.com/annematronic)
5259
* [rshide](https://github.com/rshide)
5360
* [VathsalaAchar](https://github.com/VathsalaAchar)
54-
* [TrimPeachu](https://github.com/TrimPeachu)
55-
* [ajbosco](https://github.com/ajbosco)
61+
* [toomyem](https://github.com/toomyem)
62+
* [ma7tcsp](https://github.com/ma7tcsp)
63+
* [bossenti](https://github.com/bossenti)
64+
* [TableauKyle](https://github.com/TableauKyle)
5665
* [jimbodriven](https://github.com/jimbodriven)
66+
* [joelclark](https://github.com/joelclark)
5767
* [ltiffanydev](https://github.com/ltiffanydev)
5868
* [martydertz](https://github.com/martydertz)
5969
* [r-richmond](https://github.com/r-richmond)
6070
* [sfarr15](https://github.com/sfarr15)
71+
* [sparklingSky](https://github.com/sparklingSky)
6172
* [tagyoureit](https://github.com/tagyoureit)
6273
* [tjones-commits](https://github.com/tjones-commits)
6374
* [yoshichan5](https://github.com/yoshichan5)
6475
* [wlodi83](https://github.com/wlodi83)
76+
* [ajbosco](https://github.com/ajbosco)
77+
* [AlbertWangXu](https://github.com/AlbertWangXu)
6578
* [anipmehta](https://github.com/anipmehta)
79+
* [BereketBirbo](https://github.com/BereketBirbo)
6680
* [cmtoomey](https://github.com/cmtoomey)
81+
* [dxdc](https://github.com/dxdc)
82+
* [KulykDmytro](https://github.com/KulykDmytro)
83+
* [ecederstrand](https://github.com/ecederstrand)
6784
* [pes-magic](https://github.com/pes-magic)
6885
* [illonage](https://github.com/illonage)
86+
* [Der-Henning](https://github.com/Der-Henning)
6987
* [jayvdb](https://github.com/jayvdb)
7088
* [jorgeFons](https://github.com/jorgeFons)
7189
* [Kovner](https://github.com/Kovner)
7290
* [LarsBreddemann](https://github.com/LarsBreddemann)
7391
* [lboynton](https://github.com/lboynton)
92+
* [LehmD](https://github.com/LehmD)
7493
* [maddy-at-leisure](https://github.com/maddy-at-leisure)
7594
* [narcolino-tableau](https://github.com/narcolino-tableau)
7695
* [PatrickfBraz](https://github.com/PatrickfBraz)
7796
* [paulvic](https://github.com/paulvic)
97+
* [RenaSpb](https://github.com/RenaSpb)
7898
* [shrmnk](https://github.com/shrmnk)
79-
* [TableauKyle](https://github.com/TableauKyle)
80-
* [bossenti](https://github.com/bossenti)
81-
* [ma7tcsp](https://github.com/ma7tcsp)
82-
* [toomyem](https://github.com/toomyem)

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ Use the Tableau Server Client (TSC) library to increase your productivity as you
99
* Create users and groups.
1010
* Query projects, sites, and more.
1111

12-
This repository contains Python source code for the library and sample files showing how to use it. As of September 2024, support for Python 3.7 and 3.8 will be dropped - support for older versions of Python aims to match https://devguide.python.org/versions/
12+
This repository contains Python source code for the library and sample files showing how to use it. As of October 2026, support for Python 3.9 has been dropped; support for older versions of Python aims to match https://devguide.python.org/versions/
1313

1414
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.
1515

1616
For more information on installing and using TSC, see the documentation:
1717
<https://tableau.github.io/server-client-python/docs/>
1818

19-
To contribute, see our [Developer Guide](https://tableau.github.io/server-client-python/docs/dev-guide). A list of all our contributors to date is in [CONTRIBUTORS.md].
19+
To contribute, see our [Developer Guide](https://tableau.github.io/server-client-python/docs/dev-guide). A list of all our contributors to date is in [CONTRIBUTORS.md](CONTRIBUTORS.md).
2020

2121
## License
2222
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftableau%2Fserver-client-python.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftableau%2Fserver-client-python?ref=badge_large)

‎getcontributors.py‎

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,46 @@
1-
import json
1+
import os
2+
import subprocess
3+
24
import requests
35

6+
URL = "https://api.github.com/repos/tableau/server-client-python/contributors"
7+
8+
9+
def github_token():
10+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
11+
if token:
12+
return token
13+
try:
14+
return subprocess.check_output(["gh", "auth", "token"], text=True, stderr=subprocess.DEVNULL).strip() or None
15+
except (OSError, subprocess.CalledProcessError):
16+
return None
17+
18+
19+
def fetch_contributors():
20+
headers = {"Accept": "application/vnd.github+json", "User-Agent": "getcontributors"}
21+
token = github_token()
22+
if token:
23+
headers["Authorization"] = f"Bearer {token}"
24+
25+
contributors = []
26+
page = 1
27+
while True:
28+
response = requests.get(URL, headers=headers, params={"per_page": 100, "page": page}, timeout=30)
29+
payload = response.json()
30+
if not response.ok or not isinstance(payload, list):
31+
message = payload.get("message", payload) if isinstance(payload, dict) else payload
32+
raise SystemExit(f"GitHub API error {response.status_code}: {message}")
33+
if not payload:
34+
break
35+
contributors.extend(payload)
36+
if len(payload) < 100:
37+
break
38+
page += 1
39+
return contributors
40+
441

5-
logins = json.loads(
6-
requests.get("https://api.github.com/repos/tableau/server-client-python/contributors?per_page=200").text
7-
)
8-
for login in logins:
9-
print(f"* [{login["login"]}]({login["html_url"]})")
42+
for person in fetch_contributors():
43+
login = person.get("login")
44+
html_url = person.get("html_url")
45+
if login and html_url:
46+
print(f"* [{login}]({html_url})")

‎pyproject.toml‎

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

3535
[project.optional-dependencies]
36-
test = ["black==26.5.1", "build", "mypy==2.3.1", "pytest>=7.0", "pytest-cov", "pytest-subtests",
36+
test = ["black==26.10.1", "build", "mypy==2.3.1", "pytest>=7.0", "pytest-cov", "pytest-subtests",
3737
"pytest-xdist", "requests-mock>=1.0,<2.0", "types-requests>=2.32.4.20250913"]
3838

3939
[tool.setuptools.package-data]

‎tableauserverclient/models/connection_item.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,50 @@ class ConnectionItem:
5050
5151
database_name: str
5252
The name of the database for the connection.
53+
54+
workgroup_auth_mode: str
55+
Workload Identity Federation (WIF) auth mode. Set to one of the
56+
values on ``ConnectionItem.WorkgroupAuthMode`` to switch an existing
57+
connection to WIF on an update. Requires Tableau Server 26.3 / REST
58+
API 3.30+ with WIF configured and enabled on the site. See the
59+
``WorkgroupAuthMode`` nested class for usage notes. The server does
60+
not echo this attribute on GET responses today, so values parsed
61+
from a response are typically ``None``.
62+
63+
auth_federation_service_account: str
64+
Service-account email required by the server for the
65+
``WorkgroupAuthMode.WIF_IMPERSONATION`` mode (e.g. Google BigQuery)
66+
and rejected for the direct ``WorkgroupAuthMode.WIF`` mode (e.g.
67+
Snowflake). Also requires REST API 3.30+. Not echoed on GET today,
68+
so typically ``None`` when parsed from a response.
5369
"""
5470

71+
class WorkgroupAuthMode:
72+
"""
73+
Workload Identity Federation (WIF) auth-mode values for
74+
:attr:`ConnectionItem.workgroup_auth_mode`.
75+
76+
Requires Tableau Server 26.3 / REST API 3.30+ with WIF configured
77+
and enabled on the site. Older servers silently ignore unknown
78+
connection attributes, so emitting these against an older server is
79+
a no-op rather than an error.
80+
81+
Usage notes:
82+
83+
- Send ``workgroup_auth_mode`` on every subsequent update of a WIF
84+
connection. Once a connection has been switched to WIF, omitting
85+
the attribute on a later update returns HTTP 400.
86+
- Leave :attr:`ConnectionItem.auth_type` unset (or ``"oauth"``) when
87+
sending a WIF mode; the server derives the stored auth type from
88+
the mode.
89+
- Do not set :attr:`ConnectionItem.auth_federation_service_account`
90+
with the direct ``WIF`` mode (the server rejects it). Set it only
91+
with ``WIF_IMPERSONATION``.
92+
"""
93+
94+
WIF = "workload-identity-federation"
95+
WIF_IMPERSONATION = "workload-identity-federation-impersonation"
96+
5597
def __init__(self):
5698
self._datasource_id: str | None = None
5799
self._datasource_name: str | None = None
@@ -66,6 +108,8 @@ def __init__(self):
66108
self._query_tagging: bool | None = None
67109
self._auth_type: str | None = None
68110
self._database_name: str | None = None
111+
self._workgroup_auth_mode: str | None = None
112+
self._auth_federation_service_account: str | None = None
69113

70114
@property
71115
def datasource_id(self) -> str | None:
@@ -106,6 +150,22 @@ def auth_type(self) -> str | None:
106150
def auth_type(self, value: str | None):
107151
self._auth_type = value
108152

153+
@property
154+
def workgroup_auth_mode(self) -> str | None:
155+
return self._workgroup_auth_mode
156+
157+
@workgroup_auth_mode.setter
158+
def workgroup_auth_mode(self, value: str | None):
159+
self._workgroup_auth_mode = value
160+
161+
@property
162+
def auth_federation_service_account(self) -> str | None:
163+
return self._auth_federation_service_account
164+
165+
@auth_federation_service_account.setter
166+
def auth_federation_service_account(self, value: str | None):
167+
self._auth_federation_service_account = value
168+
109169
@property
110170
def database_name(self) -> str | None:
111171
return self._database_name
@@ -136,6 +196,8 @@ def from_response(cls, resp, ns) -> list["ConnectionItem"]:
136196
string_to_bool(s) if (s := connection_xml.get("queryTagging", None)) else None
137197
)
138198
connection_item._auth_type = connection_xml.get("authenticationType", None)
199+
connection_item._workgroup_auth_mode = connection_xml.get("workgroupAuthMode", None)
200+
connection_item._auth_federation_service_account = connection_xml.get("authFederationServiceAccount", None)
139201
# The REST API GET /connections response uses "dbName" for the database
140202
# name attribute. This is different from the publish request body, which
141203
# uses "databaseName" (see _add_connections_element in request_factory.py).

‎tableauserverclient/server/endpoint/datasources_endpoint.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ def update_connections(
396396
username: str | None = None,
397397
password: str | None = None,
398398
embed_password: bool | None = None,
399+
workgroup_auth_mode: str | None = None,
400+
auth_federation_service_account: str | None = None,
399401
) -> list[ConnectionItem]:
400402
"""
401403
Bulk updates one or more datasource connections by LUID.
@@ -424,6 +426,20 @@ def update_connections(
424426
embed_password : bool, optional
425427
Whether to embed the password.
426428
429+
workgroup_auth_mode : str, optional
430+
Workload Identity Federation (WIF) auth mode. See
431+
``ConnectionItem.WorkgroupAuthMode`` for allowed values and
432+
usage notes. Applied to every connection LUID in this call.
433+
Requires Tableau Server REST API 3.30+ with WIF configured on
434+
the site.
435+
436+
auth_federation_service_account : str, optional
437+
Service-account email for WIF impersonation. Required with
438+
``ConnectionItem.WorkgroupAuthMode.WIF_IMPERSONATION`` and
439+
rejected by the server with the direct
440+
``ConnectionItem.WorkgroupAuthMode.WIF`` mode. TSC does not
441+
validate the combination client-side.
442+
427443
Returns
428444
-------
429445
Iterable of str
@@ -438,6 +454,8 @@ def update_connections(
438454
username=username,
439455
password=password,
440456
embed_password=embed_password,
457+
workgroup_auth_mode=workgroup_auth_mode,
458+
auth_federation_service_account=auth_federation_service_account,
441459
)
442460
server_response = self.put_request(url, request_body)
443461
connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)

‎tableauserverclient/server/endpoint/flows_endpoint.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,16 @@ def update_connection(self, flow_item: FlowItem, connection_item: ConnectionItem
264264
265265
REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_flow.htm#update_flow_connection
266266
267+
Setting ``connection_item.workgroup_auth_mode`` and
268+
``connection_item.auth_federation_service_account`` (see
269+
``ConnectionItem.WorkgroupAuthMode``) will forward the Workload
270+
Identity Federation attributes to this endpoint via the shared
271+
``RequestFactory.Connection.update_req`` builder. Server-side
272+
handling of these attributes on flow connections is governed by
273+
the flow endpoint and may differ from the workbook / datasource
274+
endpoints — review the REST reference for flow-specific semantics
275+
before relying on it in production.
276+
267277
Parameters
268278
----------
269279
flow_item: FlowItem

0 commit comments

Comments
 (0)