Repository navigation
v0.42 Release - #1891
Merged
Merged
v0.42 Release#1891
Conversation
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.
fix: repr for auth objects
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>
* 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>
…n, username case) (#1811)
…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>
…ack target-version (#1839)
…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
approved these changes
Oct 15, 2026
Contributor
|
The license flags are recurring complaints about some internals of mypy and urllib3 which is fine for us. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merging all changes from
developmentintomasterin preparation for creating new release v0.42.