Commit b7134a1
samples: credential helper, pagination fixes, and new jobs/subscriptions 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>1 parent a1a7ed4 commit b7134a1
15 files changed
Lines changed: 750 additions & 325 deletions
File tree
- samples
- getting_started
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
17 | 19 | | |
18 | 20 | | |
19 | 21 | | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
| 22 | + | |
32 | 23 | | |
33 | 24 | | |
34 | 25 | | |
35 | 26 | | |
36 | 27 | | |
37 | 28 | | |
38 | | - | |
39 | | - | |
40 | | - | |
| 29 | + | |
| 30 | + | |
41 | 31 | | |
42 | | - | |
43 | | - | |
| 32 | + | |
44 | 33 | | |
45 | 34 | | |
46 | | - | |
47 | | - | |
48 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
49 | 39 | | |
50 | 40 | | |
51 | 41 | | |
| |||
59 | 49 | | |
60 | 50 | | |
61 | 51 | | |
62 | | - | |
63 | | - | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
64 | 56 | | |
| 57 | + | |
65 | 58 | | |
66 | 59 | | |
67 | 60 | | |
| |||
0 commit comments