Skip to content

v0.42 Release - #1891

Merged
bcantoni merged 231 commits into
masterfrom
development
Oct 15, 2026
Merged

bcantoni merged 231 commits into
masterfrom
development

Conversation

@bcantoni

Copy link
Copy Markdown
Contributor

Merging all changes from development into master in preparation for creating new release v0.42.

vitorhonna and others added 30 commits May 15, 2025 16:07
Fix typo in update_datasource_data.py
PersonalAccessTokenAuth repr was malformed. Fixing it to be
more representative and show the actual class name used
in cases where the client user decides to subclass.
Also correct the type hints to clarify that it accepts any
Iterable.
chore: refactor XML payload into RequestFactory
…#1638)

Update multiple connections in a single workbook - Takes multiple connection, authType and credentials as input
Update multiple connections in a single datasource - Takes multiple connection, authType and credentials as input

---------

Co-authored-by: Jordan Woods <13803242+jorwoods@users.noreply.github.com>
…nections

Minor fixes on update connections
* feat: enable toggling attribute capture for a site

According to https://help.tableau.com/current/api/embedding_api/en-us/docs/embedding_api_user_attributes.html#:~:text=For%20security%20purposes%2C%20user%20attributes,a%20site%20admin%20(on%20Tableau
setting this site setting to `true` is required to enable use of
user attributes with Tableau Server and embedding workflows.

* chore: fix mypy error

---------

Co-authored-by: Jordan Woods <13803242+jorwoods@users.noreply.github.com>
Closes #1620

Sorting the fields prior to putting them in the query string
assures that '_all_' and '_default_' appear first in the field
list, satisfying the criteria of Tableau Server/Cloud to process
those first. Order of other fields appeared to be irrelevant, so
the test simply ensures their presence.

Co-authored-by: Jordan Woods <13803242+jorwoods@users.noreply.github.com>
* feat: support OIDC endpoints

Add support for remaining OIDC endpoints, including getting an
OIDC configuration by ID, removing the configuration, creating,
and updating configurations.

* feat: add str and repr to oidc item

---------

Co-authored-by: Jordan Woods <13803242+jorwoods@users.noreply.github.com>
* feat: SiteAuthConfiguration str and repr

Gives SiteAuthConfiguration methods for str and repr calls
to ensure consistent display of the object.

* style: black

---------

Co-authored-by: Jordan Woods <13803242+jorwoods@users.noreply.github.com>
Co-authored-by: Jac <jacalata@users.noreply.github.com>
Closes #1626

VirtualConnections leverages the ConnectionItem object to parse
the database connections server response. Most of other endpoints
return "userName" and the VirtualConnections' "Get Database Connections"
endpoint returns "username." Resolves the issue by allowing the
ConnectionItem to read either. Update the test assets to reflect the
actual returned value.

Co-authored-by: Jordan Woods <13803242+jorwoods@users.noreply.github.com>
According to the .xsd schema file, the tags:batchCreate and
tags:batchDelete need a "contentType" attribute on the "content"
elements. This PR adds the missing attribute and checks in the test that
the string is carried through in the request body.

Co-authored-by: Jordan Woods <13803242+jorwoods@users.noreply.github.com>
bcantoni and others added 24 commits August 6, 2026 15:55
* fix: normalize CRLF line endings to LF in six Python files

Several files had CRLF line endings baked in from a prior black-version
bump commit, causing ^M noise in diffs. Also reformats the multi-line
.format() calls in data_alert_item.py and subscription_item.py to match
current black formatting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Run type and style checks on Python 3.13 now

* Black format, run via Python 3.13

* chore: add .gitattributes to enforce LF line endings for Python files

Prevents a repeat of the CRLF regression fixed in the prior commit by
normalizing line endings at commit time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
…criptionFooter (#1849) (#1851)

* fix: preserve case of customSubscriptionEmail and customSubscriptionFooter (#1849)

RequestFactory.Site.update_req and create_req called `str(value).lower()`
on customSubscriptionEmail and customSubscriptionFooter when serializing to
XML, silently mangling caller intent:

    site.custom_subscription_email = "Sales@Company.com"
    site.custom_subscription_footer = "Sent by Tableau -- Confidential."
    server.sites.update(site)
    # server received: customSubscriptionEmail="sales@company.com"
    #                  customSubscriptionFooter="sent by tableau -- confidential."

Impact:
- Email: cosmetic only (SMTP treats mailboxes case-insensitively in
  practice), but recipients see the wrong-case "from" address.
- Footer: functionally broken. The footer is displayed verbatim in
  outgoing subscription emails, so lowercasing removes company-name
  casing, sentence capitalization, brand terms, etc. Anyone setting a
  customer-facing footer via TSC got a mangled result with no workaround
  short of the web UI.

Drop the .lower() on both string values in both code paths. The paired
*Enabled boolean attributes still get .lower()d (they need to serialize
as "true"/"false"), just the string values are now sent verbatim.

Discovered while implementing tabcmd editsite parity
(tableau/tabcmd#437, tabcmd PR #452).

Fixes #1849.

* Refactor subscription case-preservation tests to parse XML

Copilot noted that substring-matching the serialized XML is brittle:
attribute quoting (single vs double), whitespace, and entity escaping
can vary without changing meaning. Parse the request with defusedxml
(matching the existing test_encoding_attr_capture style at line 361)
and assert on the parsed attribute value instead.

The load-bearing check is still "the value is not lowercased", which is
what site_elem.attrib["customSubscriptionEmail"] == "Sales@Company.com"
verifies. Test now survives serialization-format changes and still
fails if the .lower() regression comes back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1848)

* fix: preserve POST body across 3xx redirects (#1127, #1828)

`requests` follows 301/302/303 by converting POST to GET and dropping the
request body. Any TSC write hitting a server behind a redirect (users.add,
workbooks.publish, addusers, etc.) returned 405 Method Not Allowed because
the server saw a GET where it expected a POST.

Disable requests' auto-redirect and walk the chain manually in
Endpoint._make_request, keeping the original method and body across every
hop. Hop count bounded by session.max_redirects (default 30, same as
requests).

Also close two nearby gaps:
- Refuse HTTPS -> HTTP scheme downgrades. Silently following them would
  send auth material over plaintext; no legitimate server behaviour
  requires this. Raises RedirectError with the original and target URLs.
- Raise RedirectError (with URL, method, status code) when a 3xx response
  has no Location header, replacing the bare KeyError('location') that
  requests emits deep in its internals.

Sign-in retains its own single-hop 301 handler in auth_endpoint.py for
backwards compatibility; the new path is additive.

Test coverage: 8 new tests in test_redirect_handling.py covering POST body
preservation, multi-hop chains, relative Location headers, scheme
downgrade refusal, missing Location, and hop-cap enforcement. Existing
866-test suite unchanged.

Fixes #1127. Fixes #1828.

* Address #1848 review: fix max_redirects=0, unify signin with base redirect handling, restructure tests

Fixes from Claude review pass:

1. `_follow_redirect_if_any`: move the "not a redirect?" early-return
   outside the loop, so a 200 response returns immediately even when
   session.max_redirects=0 (previously fell straight to
   "Exceeded 0 redirect hops" error). Also switch to
   getattr(method, "__name__", "REQUEST") to survive functools.partial
   or other callable wrappers.

2. `auth_endpoint.sign_in`: replace the inline session.post + 301 handler
   with `_make_request`, so signin now inherits multi-hop chain support,
   the HTTPS -> HTTP scheme guard, the missing-Location diagnostic, and
   the hop limit. This resolves the divergent behavior between signin
   and every other endpoint (signin previously refused to follow 302 and
   had no security guards).

3. `test_redirect_handling.py`: rewrite all tests to drive real endpoint
   calls (`server.auth.sign_in`, `server.workbooks.get`) through
   `requests_mock`, exercising `_make_request` end-to-end rather than
   calling `_follow_redirect_if_any` in isolation. Add parametrized
   coverage for all 5 followed redirect codes (301/302/303/307/308) and
   the 4 non-followed ones (300/304/305/306). Add tests for header
   preservation (X-Tableau-Auth reaches the redirect target), HTTP->HTTPS
   upgrade allowed, cross-host redirect followed, second-hop HTTPS->HTTP
   downgrade caught, and max_redirects=1 error path. Document why
   max_redirects=0 isn't tested (`requests` refuses to complete any 3xx
   response when max_redirects=0, regardless of `allow_redirects`, so
   the response never reaches our code).

Full test suite: 888 passed, 1 skipped.

* Promote server address to https on http->https redirect

When the server redirects http://host to https://host on the same host,
update `server._server_address` so subsequent requests skip the redirect
round-trip. Recovers an older idea from the abandoned
`jac/handle-https-better` branch, now that the manual-redirect handler
from #1848 provides the right hook point.

Only rewrites the stored address when:
- current scheme is http, next scheme is https (upgrade, not downgrade
  which is already refused above)
- current and next netloc match (same host, just scheme change) -- avoids
  the failure mode where a redirect to a completely unrelated https
  server silently repoints every future call at it.

Two tests: one verifies the address is promoted on a same-host http->https
redirect, the other verifies it is NOT promoted on a cross-host redirect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: justify auth-material forwarding across cross-host redirects

Add a code comment in `_handle_redirects` explaining why the X-Tableau-
Auth header and session cookies are intentionally preserved on cross-
host redirects, rather than stripped as a generic library would.

The concern was raised in a security-focused fresh-eyes review: same-
scheme cross-host redirects normally leak bearer tokens to whoever
controls the redirect target, and RFC 7235 recommends stripping auth
on cross-origin hops for that reason. But TSC is a client for a
specific server the caller has already trusted, and Tableau Server is
routinely deployed behind reverse proxies, load balancers, and SSO
front-ends that redirect between hosts within the same infrastructure
(tableau.corp.example -> east.tableau.corp.example, SSO IdP -> auth
callback endpoint on a different subdomain, etc.). Stripping auth
material there would break sign-in against every such deployment.

The HTTPS -> HTTP downgrade guard (line 208) is the load-bearing
security boundary: once the caller connects over HTTPS, the token
cannot leave TLS regardless of which host receives the redirect.

Comment only. No code change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: justify uniform method preservation on 303 responses

Add a code comment noting the deliberate deviation from RFC 7231 §6.4.4,
which says 303 SHOULD change the method to GET on retry. TSC preserves
the method and body on 303 the same as on the other redirect codes.

Rationale: Tableau Server doesn't emit 303 for POST endpoints in normal
operation, and PR #1848's goal is to preserve method+body across the
common proxy/HA cases. If a deployment ever starts emitting 303 for
writes, revisit. Marking it as a conscious deviation so a future reader
doesn't submit a "fix" that reintroduces the bug we just fixed.

Comment only. No code change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* respect explicit allow_redirects override from http_options

Change `parameters["allow_redirects"] = False` to
`parameters.setdefault("allow_redirects", False)`. Default behavior is
unchanged: with no override, TSC walks the redirect chain itself and
preserves method+body across every hop.

If a caller has a specific reason to override -- a security policy that
requires failing loudly on any redirect rather than silently following
one, or a test harness that wants requests' default behavior -- they
can pass allow_redirects=True or =False on the Server's http_options
and have it respected. The manual redirect walker in
_follow_redirect_if_any short-circuits on non-3xx responses, so
requests handling the redirect first and returning a 200 is safe.

The 24 existing redirect tests all pass unchanged; none of them
override allow_redirects, and both the enforced-redirect and refused-
redirect paths still exercise the correct code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* populate response.history when walking the redirect chain manually

`requests`' native follower populates `Response.history` with the
intermediate 3xx responses in receipt order; the final non-3xx response
is what's returned, not in history. PR #1848 short-circuited the native
follower by setting `allow_redirects=False` and walking the chain in
`_follow_redirect_if_any`, which meant `.history` came back as an empty
list even after a multi-hop chain. Fine for internal callers (nothing
in TSC reads .history), but a silent behavior change for external
consumers who forensically inspect responses.

Collect each intermediate 3xx response in a local list and assign it
to `response.history` on the final non-3xx response before returning.
Matches the shape callers get from requests' native follower.

Two tests:
- test_response_history_populated_across_multi_hop_chain confirms the
  intermediate 301 and 302 land in .history in order after a 3-hop
  chain terminating in 200.
- test_response_history_empty_when_no_redirect confirms the no-redirect
  short-circuit still returns .history=[] as requests would have.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot review findings on #1848

- endpoint.py: replace `old_address[7:].startswith(current_parsed.netloc)`
  with exact `old_parsed.netloc == current_parsed.netloc` comparison for
  the http->https address promotion. The startswith form was correct in
  the reviewed attacker scenarios but wrong for the common corporate case:
  an unqualified hostname like `TSC.Server("http://tableau")` where DNS
  search paths / split-horizon resolve `tableau` to different actual
  hosts. A redirect from `http://tableau/` to `https://tableau.other/`
  would previously promote `_server_address` to `https://tableau` even
  though the redirected netloc was `tableau.other`. Exact-netloc match
  kills that.

- endpoint.py: change RedirectError message from "last Location was
  {current_url}" to "last URL attempted was {current_url}". current_url
  is the resolved URL of the last attempted hop, not the raw Location
  header value; the old phrasing was misleading during redirect-loop
  diagnostics.

- test_redirect_handling.py: tighten
  test_non_followed_3xx_codes_pass_through to raise `ServerResponseError`
  specifically instead of `(ServerResponseError, Exception)`. The mocked
  XML error body deterministically produces ServerResponseError via
  _check_status; the broader assertion could mask an unrelated failure.

- The fourth Copilot finding (namespace detection lost on the new signin
  path) is tracked separately as #1866; addressing there so it does not
  block this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: add docstrings to exponential_backoff.py
* docs: add docstrings to datetime_helpers.py
* docs: add docstrings to namespace.py
* docs: add docstrings to filesys_helpers.py
* docs: name CHUNK_SIZE_MB default and get_file_type's ValueError
* docs: expand RequestOptions.vf() with vf_ wire-format rules
* docs: drop irrelevant filter= reference from vf() docstring
* docs: document backslash as vf_ escape character
* docs: rephrase vf_ backslash bullet to lead with examples
* docs: clarify vf_ escaping is only for ',' and '\'
* docs: clarify vf() percent-encoding vs escaping 
* docs: label empirical vf() claims and tighten wildcard note

The vf_ view filter has several server-side quirks that aren't obvious
from the current one-line docstring. Document what values actually mean
on the wire: exact match by default, comma as OR-list, backslash-escape
for literal comma, empty-value override behavior, no wildcards, no
operators, boolean 'true'/'false' only.

This surfaces the behavior verified end-to-end against Tableau Cloud
while working through --filter parsing issues in tabcmd, so callers who
build vf_ requests through TSC know what the server will accept without
having to reverse-engineer it.
Bumps [mypy](https://github.com/python/mypy) from 2.3.0 to 2.3.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](python/mypy@v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 2.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
#1856)

* docs: add pull-request template with Motivation / Behavior / Test plan

Reviewers of PRs to this repo have to guess "why now" for most changes,
whether a PR is a behavior change for downstream users, and which
claims in the body are covered by tests vs. verified manually. Adding
a template with three prompts lets contributors surface those answers
without me nagging in every review.

The sections are opinionated but small: Motivation, Behavior change,
and Test plan. Each has a comment explaining the intent. Contributors
can delete sections that don't apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address fresh-eyes review on PR template

Two nits from review:
- Swap literal `--` inside HTML comments for single `-`. GitHub renders
  both fine, but double-dash is invalid SGML inside a comment and some
  markdown linters flag it.
- Add a one-line pointer to contributing.md and CHANGELOG.md so external
  contributors don't have to hunt for the CHANGELOG expectation.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FOSSA flagged CVE-2026-21441 (GHSA-38jv-5279-wg99, 8.9 High) against
urllib3 2.6.0. The fix landed in urllib3 2.6.3: streaming decompression
safeguards were bypassed when HTTP redirects were followed.

The existing `urllib3>=2.6.0,<3` specifier already permits 2.6.3, but
because 2.6.0 remains a resolvable install, Dependabot did not open a
range-bump PR, and this repo did not have the security-updates side of
Dependabot expressed in its config either. This change:

  * Raises the floor in pyproject.toml to `urllib3>=2.6.3,<3` so fresh
    resolves cannot land on a vulnerable release.
  * Adds `open-pull-requests-limit: 10` to both dependabot ecosystems so
    security PRs are not squeezed out by the default cap of 5, and
    documents on the pip block that security advisories fire
    independently of the weekly schedule as long as "Dependabot
    security updates" is enabled under Settings -> Code security.
  * Adds a CHANGELOG entry naming the CVE and GHSA.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… _evaluate_site_role (#1812)

* fix: CSVImport AUTH column, case-insensitive validation, username preservation; add find_by_name

Fixes for UserItem.CSVImport (issue #1809):
- MAX=8 (was 7=AUTH index): 8-column lines with auth type no longer
  rejected as "too many columns"
- create_user_from_line no longer lowercases the whole line before
  splitting — username case is preserved
- _validate_import_line_or_throw normalizes license/admin/publisher to
  lowercase and auth to canonical form before comparison, so 'Viewer',
  'Creator', 'SAML', 'tableauidwithmfa' etc. are all accepted
- Add TableauIDWithMFA to valid auth values in validation (was missing)
- 5 new tests covering each fix

Add QuerysetEndpoint.find_by_name(name) (issue #1810):
- Thin wrapper over .filter(name=name) returning a list
- Available on all content-item endpoints (workbooks, datasources,
  views, users, projects, groups)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add auth column to create_users_csv, fix create_from_file extension check and debug prints

- create_users_csv was producing 7-column CSV, silently dropping
  auth_setting; bulk_add roundtrip would lose auth type
- create_from_file extension check used filepath.find("csv") which
  evaluates as falsy only when "csv" is at index 0, letting all other
  paths through; fixed to "csv" not in filepath
- remove two debug print() calls left in create_from_file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: extract _decompose_site_role into CSVImport, symmetrical with _evaluate_site_role

Moves the ad-hoc site role → (license, admin_level, publish) logic from
create_users_csv into UserItem.CSVImport._decompose_site_role, making it
the explicit inverse of _evaluate_site_role.

Also fixes pre-existing bugs in the decomposition:
- ExplorerCanPublish was emitted as license="ExplorerCanPublish" (not a
  valid CSV license value); now correctly "Explorer" with publish=1
- SiteAdministrator (legacy role) was emitting license="" via
  str.replace; now maps to ("Explorer", "Site", "1")
- Non-admin roles now emit admin_level="None" (explicit CSV spec value)
  rather than "" (empty string); both are accepted by the server but
  "None" is consistent with the spec and _evaluate_site_role input

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: decompose unsupported site roles to license="Invalid" not "Unlicensed"

Legacy values in UserItem.Roles (UnlicensedWithPublish, ViewerWithPublish,
Guest, SupportUser) have never been accepted by the server-side CSV license
parser (workgroup: CsvLicenseRoleTypeConverter). The initial _decompose_site_role
default of ("Unlicensed", "None", "0") silently coerced these to a valid but
semantically wrong Unlicensed user, replacing an old server-side per-row
rejection with silent success.

Default to license="Invalid" instead so the server continues to reject rows
with USER_CSV_INVALID_LICENSE, preserving the pre-refactor observable
behavior for callers who inspect job results for per-row failures. Batch
resilience is unaffected: bad rows fail, good rows succeed, no client-side
throw takes down the whole bulk_add call.

Follow-up: deprecate UnlicensedWithPublish/ViewerWithPublish from
UserItem.Roles (never worked on any code path); see forthcoming issue.

* docs: clarify _decompose_site_role's asymmetric legacy-role handling

The role map handles legacy UserItem.Roles values in two ways depending
on whether the server has a modern equivalent. SiteAdministrator,
Publisher, Interactor, and ReadOnly map to their current-model
equivalents; UnlicensedWithPublish, ViewerWithPublish, Guest, and
SupportUser fall to license="Invalid" so the server rejects the row.

Reviewers otherwise read the code and see "why does Publisher get
mapped but Guest doesn't?" - the answer is server behavior, not
arbitrary choice. Docstring now says so.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: accept "1"/"0" and "true"/"false" for publisher, add round-trip test

Two related changes to make `_evaluate_site_role` and
`_decompose_site_role` a real inverse pair:

- `_evaluate_site_role` now accepts "1", "true", and "yes" for the
  publisher column (and, symmetrically, treats anything else as "no").
  Previously only "yes" was accepted, which meant `_decompose_site_role`
  emitting "1" for Creator/ExplorerCanPublish would round-trip as
  Explorer. The set of publisher values mirrors what
  `_valid_attributes[publisher]` already documents as legal
  (["yes", "true", "1", "no", "false", "0"]), so this brings the two
  code paths in agreement.

- Added a parametrized `test_decompose_then_evaluate_round_trips`
  covering every entry in `_role_map` plus the two documented label
  asymmetries (ServerAdministrator -> SiteAdministrator on the way
  back; legacy roles Publisher/Interactor/ReadOnly/SiteAdministrator
  folded into their modern equivalents). If either function drifts,
  a specific input names the broken case rather than the whole loop
  dying on the first mismatch.

- `_decompose_site_role` docstring updated with a Round-trip note
  naming the two intentional asymmetries so readers do not have to
  reason about them from the mapping table.

Full suite still passes: 884 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address fresh-eyes review nits before un-drafting

- Strip five non-ASCII characters (arrow + em-dashes) from docstrings
  and comments; project convention is ASCII-only.
- create_user_from_line now passes an unknown auth value through
  unchanged (previously silently .get'd to None). _validate_import_
  line_or_throw on the same input raises, so the two entry points
  no longer disagree on what counts as valid.

Not addressed here (per fresh-eyes review, non-blocking):
- Deprecated create_from_file's "csv" substring check remains loose
  (matches report.csv.bak, rejects USERS.CSV); pre-existing on a
  @deprecated method, out of scope for this refactor.
- PR body test-plan checkbox that's actually complete: tick before
  un-drafting.

59 user tests pass.

* Warn (don't raise) on unknown auth values during CSV import

Post-merge follow-up on #1811 -- the strict behavior it introduced
(create_user_from_line raises ValueError on any auth value not in
_AUTH_CANONICAL) would block CSV imports against newer servers as
soon as Tableau ships an auth type TSC's hardcoded list doesn't yet
know about. This is the same category of stale-list problem the
_set_values enum-guard bypass exists to avoid on the server-parse
path.

Both entry points now warn and pass the value through:

- create_user_from_line: unknown values raise a UserWarning naming
  the value and known set, then get assigned to auth_setting as-is.
  If the value really is a typo, the server rejects the row when
  the request posts -- a slightly-later error, but the import stays
  possible against forward-compatible servers.

- _validate_import_line_or_throw: same shape. Skips the allowlist
  check for the AUTH column when the value isn't in _AUTH_CANONICAL,
  so validate_file_for_import doesn't return the row as invalid.

Server-version-aware validation would be the cleaner long-term fix
here (and for the enum-guard bypass in _set_values); noted for
planning, not filing an issue.

Tests updated: two former "raises ValueError" cases now assert
pytest.warns(UserWarning) and confirm the raw value round-trips
onto UserItem.auth_setting.

* Strip password-mask line: it belongs to #1862, not this PR

Inadvertently pulled the password-column mask
(safe_value = "***" if column == PASS else value) into this PR while
restructuring the validation loop for the warn-on-unknown-auth
change. The mask is #1862's core content and shouldn't sneak in
through the site-role decompose PR -- reviewers on either PR would
see mysterious overlap. #1862's full feature (log mask + INFO->DEBUG
downgrade + _redact_password_column helper for invalid_lines
sanitization) stays where it belongs, on jac/csv-import-privacy.

The `column = ColumnType(i)` local rename stays because it's used
by the log line's `{column.name}` format and by the AUTH branch's
comparison; that's plain cleanup and doesn't overlap with #1862.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci: shield issues with an open PR from the stale bot

`actions/stale` only considers issue-level events when it decides whether
to mark or close an issue. A PR that references an issue via
`Closes #NNN` / `Fixes #NNN` does not reset the issue's stale timer or
remove the `stale` label, so an issue can be auto-closed by the bot even
while a PR that closes it is in review.

Add a companion workflow that reacts to PR opens/edits and stamps the
`in-progress` label on every referenced issue. The existing stale workflow
already exempts `in-progress`. When a PR closes without being merged, the
label is removed so a genuinely abandoned effort does not keep its
referenced issues shielded forever. Merged PRs auto-close the referenced
issues via GitHub's usual behavior, so leaving the label on them is
harmless (they're closed).

Uses `pull_request_target` for permissions on external-contributor PRs.
The script only reads `pr.body`, extracts decimal issue numbers with a
fixed regex, and passes those numbers to the REST API -- body content
never reaches a `run:` step or a shell, so there is no command-injection
surface even though the trigger runs with write permissions.

Companion to #1841.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: unlabel on edited body, cap ref count for pull_request_target safety

Two review findings:

- On `edited` events the script now diffs `payload.changes.body.from`
  against the new body and removes 'in-progress' from any issue whose
  Closes/Fixes/Resolves reference was deleted. Previously, editing a PR
  to drop `Closes #42` left #42 shielded from the stale bot indefinitely.

- Cap the number of references processed per event at 50 (`MAX_REFS`).
  `pull_request_target` runs on PRs from forks, so an accidental or
  malicious PR body with thousands of matches would burn the repo's
  REST budget on labeling calls.

Also adds a one-line comment noting that cross-repo refs (owner/repo#N)
are intentionally out of scope; this workflow only labels issues in
the current repo.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: exempt server-side-gap label from stale bot

Issues blocked on server-side REST API work have no client-side activity
to accumulate, so the stale bot would repeatedly flag them by default.
Exempt the `server-side-gap` label (used to mark issues that need a
server-side fix and cannot be resolved in TSC alone) so those items
stay open until the underlying server work lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fixup: use existing 'Server-Side Enhancement' label instead of new one

The repo already has a 'Server-Side Enhancement' label applied to
issues blocked on server-side REST API work (e.g. #1716, #1658, #1537,
#1508). Adding a parallel 'server-side-gap' label would have been
redundant. Point the stale-bot exemption at the existing label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: make Server thread-safe with per-thread sessions

* fix torn auth-state reads exposed by free-threaded Python

* restore Server._session as a backwards-compatible property shim
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@v7...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* docs: numpy-style docstrings on Favorites endpoint (15 methods)

Adds a class-level docstring plus 15 method docstrings on
Favorites (get, add_favorite, 6x add_favorite_<type>, delete_favorite,
6x delete_favorite_<type>). Content ported from docs/api-ref.md's
Favorites section on gh-pages so that the Sphinx pipeline in #1832 will
produce equivalent output once wired up.

Part of the api-ref -> Sphinx migration; see the migration audit
report for the full list of methods still needing docstrings. Favorites
was the largest single chunk in the "needs_docstring" bucket
(15/41 methods).

No behavior change. Docs only.

* docs: address fresh-eyes review on Favorites docstrings

- Class docstring: add missing "collections" FavoriteType (7, not 6).
- add_favorite: drop misleading #add_workbook_to_favorites anchor on
  polymorphic method; link to Favorites Methods page root.
- add_favorite_metric: add REST API link (#add_metric_to_favorites).
- delete_favorite: add REST API link; polymorphic, so page root.
- delete_favorite_metric: add REST API link; no per-type anchor exists,
  so page root.
- delete_favorite_flow: fix anchor drift (#delete_flow_from_favorites
  -> #ref_delete_flow_from_favorites).
…ecial-char behavior (#1854)

* docs: clarify username formats, ProjectLeader-as-group, and filter special-char behavior

Bundles three long-standing docs gaps flagged in stale issues:

#993 - users.add example used a fake short username with no explanation
that `name` is the server auth identifier (email on Cloud, SAM@Domain on
AD-backed Server), not the person's display name. Rewrote the users.add
docstring intro to describe the required formats per auth scheme, split
the example into a Cloud invocation and an AD invocation, and added
matching guidance to UserItem.name.

#1067 - projects.update_permissions had no examples, so callers had no
guide for the common "assign group as Project Leader" case. Added an
example that shows constructing a PermissionsRule with grantee=<group>
and capability=ProjectLeader, plus a note that update_permissions is a
full replacement (call populate_permissions first if preserving
existing rules).

#1200 - the REST filter grammar treats ',', '&', ':', '[' and ']' as
delimiters and the server does not support escaping, so
`filter(name="T(L-F,SZ&V-MY)")` fails with 400065 for any name
containing those characters. Documented the constraint on the Filter
class docstring and on QuerySet.filter, along with the working
wildcard workaround (Equals with '*' substituted).

No behavior change. Docs only.

Closes #993, #1067, #1200.

* docs: cite wildcard-filter server version requirement

The Equals-operator wildcard workaround for special characters requires
Tableau Cloud May 2023 or Tableau Server 2022.1.14 (per the REST API
"Filtering and Sorting" docs). Older servers reject `*` as a literal, so
readers on those need to know before copying the example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: ASCII em dashes, drop dead docs link, soften AD claim

Three fresh-eyes findings:
- Replace Unicode em dashes (U+2014) in filter.py and users_endpoint.py
  docstrings with ASCII `--` or `;`, matching the repo's ASCII-only
  convention that other docstrings follow.
- query.py referenced docs/filter-sort.md, which lives on the gh-pages
  branch and does not exist in an installed package. Swap for the
  published URL so a `help()` reader can actually reach the doc.
- Soften the AD username claim in users_endpoint.py: a bare
  SAMAccountName's resolution depends on the AD configuration, so
  "typically will not resolve" was stated more strongly than the
  surrounding server-behavior guidance warranted.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ir (#1844)

* feat: add sample for converting connection auth to Snowflake keypair

Adds samples/update_connection_to_keypair.py demonstrating how to
convert an existing Tableau Cloud datasource or workbook connection
from username/password to Snowflake keypair authentication using the
REST API v3.27 flow. Documents the prerequisite that the private key
must first be saved under Site Settings -> Saved Credentials for Data
Sources on the site.

Refs #1602

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tighten embed_password comment and note flow-connection version

The prior comment said embed_password=True "tells the server to use the
pre-saved credential" which was vague. Server code (RestApiAppService
updateConnectionImpl -> embedOAuthUserKeychainConnections) looks up the
credential by (dbClass, username, role, auth) and binds it to the
connection; without that pre-saved match, the update still writes
'auth-keypair' into metadata but subsequent extract refreshes fail.
Spell that out so a copy-paste reader knows what "pre-saved" actually
requires.

Also add a note that flow connections gained the same capability in
REST API v3.28 (datasources and workbook connections were v3.27).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fold keypair sample into existing update_connection_auth

update_connection_auth.py already covers exactly this workflow (change
a connection's auth_type + credentials via the update-connection
endpoint). The prior keypair-specific sample added no new code path --
only Snowflake-keypair context that belongs in the shared sample's
header.

Delete the dedicated keypair sample; expand update_connection_auth's
header comment to list common authentication_type values (including
auth-keypair) and to spell out the pre-saved-credential prerequisite
that applies to any embed_password=True conversion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(samples): fix wire values, add security callout, add workbook REST ref

Round-1 review of update_connection_auth.py header caught three real
inaccuracies and one missing security note:

- "Username Password" was not the wire value; it's "UsernamePassword"
  (confirmed against test/assets/datasource_connections_update_no_auth.xml,
  test/assets/workbook_update_connections_no_auth.xml, and the
  UsernamePassword assertions in test_datasource.py and test_workbook.py).
- "oauth" had no grep-able backing as an authentication_type wire value
  on this endpoint. Dropped.
- "sqlserver" is a connection type (type=), not an authentication_type
  wire value. Dropped.
- v3.27 (datasource/workbook) and v3.28 (flow) version claim had no
  code backing -- @api decorators on update_connection are 2.3 for
  datasource/workbook and 3.3 for flow. Dropped rather than restating.
- Added a SECURITY callout: datasource_password is a positional CLI
  argument, so private-key material passed there leaks to shell
  history, ps output, and audit logs.
- Added the workbooks_and_views REST reference alongside the
  data-sources one; this sample updates both.
- Softened the credential lookup-key description ("attributes
  including..." instead of asserting a specific tuple).

Docs-only. samples/update_connection_auth.py parses cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Correct authentication_type wire values in the sample header

The prior list included "UsernamePassword" (one word) as a canonical
value. Grepping the monolith source of truth shows that string is
not a valid authentication_type -- it does not appear in
codegen/constants.data, in the connection.authentication values that
Tableau Desktop writes into .tds/.twb, or in the REST reference docs.
It is only present in TSC's hand-authored test fixtures, where it
survives round-trip tests because TSC doesn't validate the string --
so a sample-user who copies it and posts to a real server hits an
unhelpful rejection.

Replaced with the actual documented set from codegen/constants.data
and the public REST reference:

  - auth-user-pass      (canonical username+password)
  - username-password   (SAP HANA / Sybase ASE / NetWeaver / Denodo / Salesforce)
  - auth-keypair        (Snowflake keypair)
  - oauth
  - auth-none
  - AD Service Principal
  - Azure AD Password

TSC's `UsernamePassword` test fixtures are a separate follow-up --
they don't break tests, but they steer future readers wrong.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ons samples (#1843)

* samples: add shared credential resolver that avoids the command line

Introduces samples/_shared.py with resolve_credentials(args), which fills
missing sign-in values from env vars (TABLEAU_SERVER, TABLEAU_TOKEN_NAME,
etc.) or a .env-style file, and falls back to interactive getpass so
secrets never touch shell history. CLI args still work for CI use.

Wires the new helper into login.py, publish_workbook.py, and
publish_datasource.py to establish the pattern; the remaining samples
still accept the same CLI args and continue to work as before.

Addresses #1551 item 1.

* samples: fix mispagination in samples that treated a single page as all

Several samples called `server.<endpoint>.get()` and named the result
`all_workbooks`, `all_datasources`, etc. This only returns the first page
(default 100 items); if the item of interest was not on that page it was
silently missed and the sample failed with a "not found" message.

Replace those calls with `TSC.Pager(server.<endpoint>)` so every page is
walked. Where a total count was being displayed we still make one plain
`.get()` up front so the total_available field is available without
paging through the whole site twice.

Also corrects an unrelated typo in getting_started/3_hello_universe.py
where the "workbooks" section actually queried datasources.

Addresses #1551 item 2 (and #1531).

* samples: add list_jobs and manage_subscriptions for coverage gaps

The existing samples cover workbooks, datasources, schedules, extracts,
projects, users, groups, favorites, and webhooks, but there was no
sample for two frequently asked-about endpoints:

  * list_jobs.py -- lists background jobs (extract refreshes, publishes,
    flow runs, etc.), demonstrating the .filter() queryset with
    date/status/type filters and the wait_for_job helper.
  * manage_subscriptions.py -- list/create/delete site subscriptions,
    demonstrating the SubscriptionItem + Target pattern and paginated
    listing with TSC.Pager.

Both samples use the new samples/_shared.py credential resolver so the
sign-in pattern matches the rest of the samples.

Addresses #1551 item 3.

* samples: align sign-in short flags with tabcmd

Restore -t for --site, -u for --username, -p for --password; drop short
flags on --token-name and --token-value. This matches tabcmd's canonical
short flags in tabcmd/execution/parent_parser.py so users running both
tools have one convention to remember.

The initial refactor picked new short flags without noticing that the
old samples/login.py already followed tabcmd's convention (-p was
--password, -t was --site). Reassigning -p to --token-name meant
`python login.py -p <password>` silently sent the password as a token
name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: expose refreshExtractTriggered on SubscriptionItem (#1658)

The Tableau REST API supports a `refreshExtractTriggered="true"` attribute
on subscription payloads that makes the subscription fire when its referenced
schedule's extract refresh completes, rather than on the schedule's time
trigger. On Tableau Cloud, this is the wire form of an "On Extract Refresh"
subscription. TSC never exposed this attribute; users trying to create these
subscriptions were passing `schedule_id=None` and hitting a confusing wire
error deep in the endpoint layer.

Changes:
- `SubscriptionItem.on_extract_refresh(...)` classmethod factory constructs
  a subscription with an extract-refresh schedule id and the flag set.
- `refresh_extract_triggered` exposed as a property with a docstring
  covering the two ways the server surprises callers (server rejects True
  with a non-extract schedule; server silently clears the flag when a
  schedule change is included in an update).
- `Subscriptions.create()` and `.update()` now raise `ValueError` up front
  when `schedule_id` is missing, so the wire error becomes an actionable
  client-side message.
- `create_req` emits `refreshExtractTriggered="true"` only when set;
  `update_req` emits both true and false so callers can turn the flag off
  on an existing subscription.
- `_parse_element` reads the attribute back into the property; parse
  continues to accept inline-schedule responses (schedule_id=None).

Tests cover: factory sets flag + schedule id; default false; create_req
emit-when-set/omit-when-false; update_req always emits; parse round-trip
for both true and missing; parse of inline-schedule responses; create()
and update() reject missing schedule_id.

Related to #1658.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address fresh-eyes review on refreshExtractTriggered subscriptions

- Docstring on `refresh_extract_triggered` now warns about the manual-
  build update() footgun: because every subscriptions.update() payload
  carries the attribute, a caller who builds a fresh SubscriptionItem
  locally, stamps _id, and updates will silently flip an existing
  on-extract-refresh subscription off. Fetch first.
- Soften create()'s "schedule_id is required" error so someone who just
  forgot to set schedule_id on a time-based subscription doesn't get
  steered exclusively toward SubscriptionItem.on_extract_refresh(...);
  the factory is now mentioned as a conditional pointer.
- __init__'s schedule_id parameter is now typed str | None, matching the
  real state: _parse_element sets it to None on inline-schedule
  responses. Drop the two `# type: ignore` markers in
  test/test_subscription.py that were papering over the earlier lie.
- create_req asserts schedule_id non-None to satisfy mypy after the
  parameter widening; subscriptions.create() already guards this path
  before request emission.
- Add samples/create_extract_refresh_subscription.py demonstrating the
  full flow: sign in, resolve view/workbook and user by name, pick an
  extract-refresh schedule from the schedules list, build the
  subscription via on_extract_refresh(), post it. Highest-leverage
  discoverability artifact for callers searching "on extract refresh".
- CHANGELOG entry.

* samples: add shared credential resolver that avoids the command line

Introduces samples/_shared.py with resolve_credentials(args), which fills
missing sign-in values from env vars (TABLEAU_SERVER, TABLEAU_TOKEN_NAME,
etc.) or a .env-style file, and falls back to interactive getpass so
secrets never touch shell history. CLI args still work for CI use.

Wires the new helper into login.py, publish_workbook.py, and
publish_datasource.py to establish the pattern; the remaining samples
still accept the same CLI args and continue to work as before.

Addresses #1551 item 1.

* samples: fix mispagination in samples that treated a single page as all

Several samples called `server.<endpoint>.get()` and named the result
`all_workbooks`, `all_datasources`, etc. This only returns the first page
(default 100 items); if the item of interest was not on that page it was
silently missed and the sample failed with a "not found" message.

Replace those calls with `TSC.Pager(server.<endpoint>)` so every page is
walked. Where a total count was being displayed we still make one plain
`.get()` up front so the total_available field is available without
paging through the whole site twice.

Also corrects an unrelated typo in getting_started/3_hello_universe.py
where the "workbooks" section actually queried datasources.

Addresses #1551 item 2 (and #1531).

* samples: add list_jobs and manage_subscriptions for coverage gaps

The existing samples cover workbooks, datasources, schedules, extracts,
projects, users, groups, favorites, and webhooks, but there was no
sample for two frequently asked-about endpoints:

  * list_jobs.py -- lists background jobs (extract refreshes, publishes,
    flow runs, etc.), demonstrating the .filter() queryset with
    date/status/type filters and the wait_for_job helper.
  * manage_subscriptions.py -- list/create/delete site subscriptions,
    demonstrating the SubscriptionItem + Target pattern and paginated
    listing with TSC.Pager.

Both samples use the new samples/_shared.py credential resolver so the
sign-in pattern matches the rest of the samples.

Addresses #1551 item 3.

* samples: align sign-in short flags with tabcmd

Restore -t for --site, -u for --username, -p for --password; drop short
flags on --token-name and --token-value. This matches tabcmd's canonical
short flags in tabcmd/execution/parent_parser.py so users running both
tools have one convention to remember.

The initial refactor picked new short flags without noticing that the
old samples/login.py already followed tabcmd's convention (-p was
--password, -t was --site). Reassigning -p to --token-name meant
`python login.py -p <password>` silently sent the password as a token
name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* samples: fix argparse blocker + 7 bugs, add JWT + on-extract-refresh

Round of fixes for the sample-scripts refactor after fresh-eyes review.

Blocker: publish_workbook.py reused `-u` for --thumbnails-user-id while
_shared.py add_common_arguments already binds `-u` to --username, so
argparse raised ArgumentError on module load and the script would not
start. Renamed to `-U`.

Real bugs:
- _shared.py .env search now checks cwd, samples/, and repo root (in that
  order) so the docstring stops lying about "next to the sample or cwd."
- resolve_credentials now gates input()/getpass on sys.stdin.isatty()
  as the docstring already promised, so piped/CI invocations no longer
  hang forever.
- manage_subscriptions.py --attach-image switched to
  argparse.BooleanOptionalAction so users can actually pass
  --no-attach-image; the previous store_true+default=True made the flag
  a permanent True.
- Header docstring in _shared.py no longer claims "no existing command
  line breaks" (which was false: -p migrated from --token-name to
  --password in an earlier commit). Documented the tabcmd-aligned short
  flags instead.
- Corrected Python-version headers on login.py, list_jobs.py,
  manage_subscriptions.py, publish_workbook.py, refresh_tasks.py,
  move_workbook_sites.py, publish_datasource.py, and
  update_workbook_data_freshness_policy.py -- repo floor is 3.10 per
  pyproject.toml.
- list_jobs._wait_for_job: reordered excepts so JobCancelledException
  (a subclass of JobFailedException) is caught first, otherwise
  cancelled jobs were reported as failed with the wrong exit code.
- login.py sign-in banner now branches on JWTAuth as well, so JWT
  logins no longer print "Username: None". Header env-var list updated
  to include TABLEAU_JWT / TABLEAU_JWT_FILE.

New JWT support: _shared.py add_common_arguments now exposes --jwt and
--jwt-file, resolves TABLEAU_JWT / TABLEAU_JWT_FILE from env, reads a
JWT file path into args.jwt during resolve_credentials, and returns
TSC.JWTAuth from build_auth when a JWT is present. JWT takes priority
over PAT and username/password.

Extract-refresh subscription: manage_subscriptions.py create now
accepts --on-extract-refresh, which calls
SubscriptionItem.on_extract_refresh() to construct a subscription that
fires when the referenced extract-refresh schedule completes (the flow
introduced in #1861). Rebased this branch onto
jac/subscription-refresh-extract-triggered so the flag lands on top of
the new API without conflicts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* samples: address remaining fresh-eyes review followups (#1843)

Follow-up round of fixes on top of the fresh-eyes review pass. Each
change maps to a specific finding from that review.

Migrate stragglers to _shared (M4). Eight samples still had their own
inline argparse and inline PersonalAccessTokenAuth construction:
explore_{datasource,favorites,webhooks,workbook}.py, extracts.py,
move_workbook_sites.py, refresh_tasks.py, and
update_workbook_data_freshness_policy.py. All now call
_shared.add_common_arguments and _shared.build_auth so the
tabcmd-aligned short-flag convention (-s -t -u -p -l) applies
uniformly and any future credential-handling fix lives in one place.

Skip getting_started/3_hello_universe.py: intentionally a hardcoded
starter with no argparse, aimed at teaching new users to edit the
source directly. Different pedagogy from the CLI samples.

Fix explore_favorites empty-site handling (L8). The favorite-datasource
add and delete calls used to run unconditionally with my_datasource
initialized to None, so on an empty site the sample failed partway.
Both calls are now guarded (add inside the existing
`if all_datasource_items:` block, delete under a new
`if my_datasource is not None:` check).

Drop verify=False TLS bypass (L11). Removed http_options={"verify": False}
from publish_workbook.py and the equivalent
server.add_http_options({"verify": False}) pattern from extracts.py and
update_workbook_data_freshness_policy.py. A sample teaching users to
bypass TLS validation is the wrong first impression; TSC defaults to
verify=True, which is what a paved-path deployment expects. Users on
self-signed dev servers can still set the option at their own call
site.

Delete dead _shared.sign_in() helper (L9). It was not called by any
migrated sample: they all use resolve_credentials + build_auth +
`with server.auth.sign_in(auth):` for the auto-signout context
manager. The helper did not compose with `with` because it returned
a Server object rather than a context manager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Defer subscription refreshExtractTriggered to #1861

Restore subscription_item, subscriptions_endpoint, request_factory, and
test_subscription to origin/development state, and drop the matching
"On Extract Refresh subscriptions" bullet from CHANGELOG's Unreleased
section. That work is being landed via #1861 so it does not need to
ride along in this samples-focused PR.

Leaves this PR as a pure samples/CHANGELOG-free contribution: shared
credential resolver, pagination fixes, list_jobs, manage_subscriptions,
and the small samples cleanups already staged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop create_extract_refresh_subscription sample, defer to #1861

samples/create_extract_refresh_subscription.py depends on
SubscriptionItem.on_extract_refresh(), which is added by #1861 and
was already removed from this PR's diff along with the rest of the
refreshExtractTriggered work. #1861's branch already carries the
same sample byte-identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot + fresh-eyes review on samples

Copilot round-2 (2026-09-17) findings:
- _shared.py: build_auth now validates args.server so non-TTY callers
  hit a clear ValueError instead of TSC.Server(None, ...) downstream.
- explore_favorites.py: favorite-delete cleanup moved inside the
  `with server.auth.sign_in(...)` block; each delete guarded by
  `if my_workbook is not None:` etc. to match the add-side.
- update_workbook_data_freshness_policy.py: all_workbooks[2] -> [0]
  with a follow-up comment; argparse description corrected.

Fresh-eyes findings this pass caught:
- manage_subscriptions.py: drop the --on-extract-refresh path
  entirely (docstring, code branch, argparse flag). That relies on
  SubscriptionItem.on_extract_refresh which lands with #1861 and is
  not present on this branch after the earlier subscription revert.
- extracts.py: `all_workbooks[3]` -> `[0]`; guard the create/delete
  branches against `wb is None` so `--datasource ... --create` no
  longer AttributeErrors on `wb.name`; --workbook/--datasource made
  mutually exclusive to match how the sample is meant to be used.
- publish_datasource.py: raise a clear "no project named X" error
  when the project filter matches zero; fix a swapped-argument print
  so the datasource id no longer prefixes the "Datasource published"
  message with the timestamp reading as the id.
- refresh_tasks.py: subparsers marked required=True so running the
  sample with no subcommand prints usage instead of AttributeError.

Not fixed in this PR (pre-existing, flagged for follow-up):
- explore_workbook.py:120-149 has three latent bugs (missing `=` on
  `changed`, `c` referenced outside its loop, `--delete` not in this
  script's argparse). This PR only adds the _shared import; the
  bugs pre-date it and belong in a separate cleanup PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot rounds 3/4 remaining findings

Round 3 nits:
- publish_workbook.py: `-U` comment now says the conflict would happen
  when the parser is built at run time inside main(), not "at import".
- update_workbook_data_freshness_policy.py: `first_page` was assigned
  but unused; renamed to `_`.

Round 4 (after last push):
- _shared.py: added the two missing partial-credential branches so a
  user who supplies TABLEAU_TOKEN_VALUE without TABLEAU_TOKEN_NAME is
  prompted for the (non-secret) name, and one who supplies a password
  without a username is prompted for the username. Previously both
  fell through to the "fully unspecified" PAT prompt.
- _shared.py: `--jwt` help text now describes the JWT > PAT >
  username/password precedence build_auth actually implements, rather
  than claiming a mutual exclusion that argparse doesn't enforce.
- list_jobs.py: `--hours` now passes the tz-aware datetime directly to
  QuerySet.filter(created_at__gte=...) rather than `.isoformat()`. TSC
  serializes it as UTC with a trailing Z; the raw isoformat string
  could produce `+00:00` offsets that older Tableau Server versions
  reject.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix publish_workbook empty-projects guard + move_workbook_sites help text

publish_workbook.py: mirror the empty-projects guard that landed in
publish_datasource.py earlier this PR. A --project filter that matches
zero results would have slipped past `if len(projects) > 1` and hit
`projects[0].id` with an IndexError; now raises a clear ValueError.

move_workbook_sites.py: argparse description used implicit string
concatenation with missing spaces at the boundaries, so --help printed
"...from thedefault project of the default site tothe default project
of another site." Reflowed as a parenthesized single-string so the
sentence reads correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update publish_datasource header comment for build_auth

Comment claimed the sample "uses personal access tokens" for sign-in,
but the file now delegates to build_auth() which supports JWT, PAT,
and username/password. Reword so users see the full auth surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1883)

PR #1852 (parse <statusNotes> into JobItem.status_notes) added a
structured list[dict] surface for modern job types, but this sample
still only printed the legacy list[str] `notes` attribute. Modern
jobs (extract refresh v2 shapes and later) surface their diagnostics
via `status_notes`; the sample now prints both so running
`list_jobs.py --wait <job_id>` against a real server exercises the
#1852 parser end to end.

Zero library changes.
…level (#1870)

* feat: expose JobFailedException and JobCancelledException at the top level

Both were previously reachable only via
`tableauserverclient.server.endpoint.exceptions`, which is an internal
module path. Callers waiting on a background job with
`server.jobs.wait_for_job()` need to catch these to distinguish a
failed job from a cancelled one (JobCancelledException is a subclass
of JobFailedException, so the order in an except-chain matters), and
reaching into a `server.endpoint` sub-package to do so is a bad
pattern to teach.

Re-export them alongside the other exception types already exposed at
the top level (FailedSignInError, MissingRequiredFieldError,
NotSignedInError, ServerResponseError). Existing internal imports in
tests and endpoints remain unchanged; this is purely additive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Also expose FlowRunFailedException and FlowRunCancelledException at top level

Same shape and same private-path problem as the Job exceptions this PR is
already fixing. flow_runs.wait_for_flow_run() raises these; users need to
catch them without importing from server.endpoint.exceptions.

Add regression tests for both pairs so a future refactor of __init__.py
can't silently break the public surface.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Brian Cantoni <bcantoni@salesforce.com>
* Fix three latent bugs in samples/explore_workbook.py

Fresh-eyes review on #1843 (2026-09-17) surfaced three pre-existing
bugs that the samples/_shared.py migration in that PR did not
introduce but did make more visible:

1. Line 127 used a bare type annotation `changed: TSC.CustomViewItem(
   id=c.id, name=...)` instead of an assignment, so `changed` was never
   bound and the following `server.custom_views.update(changed)` would
   NameError.
2. `c` was defined inside the custom-views loop and then referenced
   outside it. On a site with zero custom views the loop never runs
   and every subsequent reference NameErrors.
3. `if args.delete:` at line 145 referred to a flag that was never
   defined in the sample's argparse, so any invocation reaching that
   line raised AttributeError.

Fixes:
- Add `=` on line 127 to bind `changed`.
- Collect the custom-views iterator into a list, wrap the update/
  export block in `if custom_views:`, and pick the last entry
  explicitly (`c = custom_views[-1]`) — matches the intent of the
  original "for the last custom view in the list" comment.
- Add a `--delete` argparse flag with `action="store_true"` and a
  clear help string. Guard the delete block so it prints a clear
  no-op message when the site has no custom views.

No other behavior change. Sample compiles and imports cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Initialize custom views before workbook guard

Co-authored-by: bcantoni <58372+bcantoni@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bcantoni <58372+bcantoni@users.noreply.github.com>
…#1880)

* feat(samples): add inspect_connection_details.py for reading connector-specific attrs

The REST API's <connection> element only surfaces generic fields (id, type,
serverAddress, serverPort, userName, embedPassword, authenticationType,
queryTaggingEnabled). Connector-specific attributes -- Oracle service, Snowflake
schema, Teradata query-band, initial SQL, etc. -- live inside the workbook or
datasource file itself. This sample downloads the file via TSC and parses it
with the community `tableaudocumentapi` library to reach the extra attributes.

Verified against a live Tableau Cloud site: on a SQL Server datasource, the
sample returns dbname/schema values that are None on the REST ConnectionItem.

Addresses the recurring backlog around connector-specific connection metadata
(#1807, #1571, #569, #353, #160) by pointing users at the existing
download-and-parse path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Adopt _shared.py credential helper in inspect_connection_details

Fold the inline sign-in argparse block into the shared helper landed
in #1843. Users of this sample now get env / .env / interactive-prompt
credential resolution, JWT + username/password auth in addition to PAT,
and a clear ValueError when --server is missing under non-TTY, instead
of an opaque 400/401 inside TSC.

Also update the header's Python-version claim from 3.7 to 3.10 to match
`pyproject.toml`'s `requires-python = ">=3.10"`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix inspect connection sample review comments

Co-authored-by: bcantoni <58372+bcantoni@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Brian Cantoni <bcantoni@salesforce.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bcantoni <58372+bcantoni@users.noreply.github.com>
Bumps [black](https://github.com/psf/black) from 26.5.1 to 26.10.1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](psf/black@26.5.1...26.10.1)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 26.10.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@jacalata

Copy link
Copy Markdown
Contributor

The license flags are recurring complaints about some internals of mypy and urllib3 which is fine for us.

@bcantoni
bcantoni merged commit b7c5866 into master Oct 15, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.