Skip to content

Commit abddd50

Browse files
jacalataclaude
andcommitted
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>
1 parent 96aab10 commit abddd50

7 files changed

Lines changed: 67 additions & 74 deletions

samples/_shared.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,17 @@ def build_auth(args: argparse.Namespace) -> TSC.TableauAuth | TSC.PersonalAccess
232232
Priority is JWT > PAT > username/password: a script that has a JWT
233233
minted for a specific session should never fall back to a longer-lived
234234
credential if the JWT-adjacent fields were left set by accident.
235+
236+
Also validates that `--server` is set. `resolve_credentials` skips prompting
237+
in non-interactive contexts (CI, piped stdin), so a missing server URL would
238+
otherwise reach `TSC.Server(None, ...)` and fail with a confusing error;
239+
catching it here gives the caller a clear message.
235240
"""
241+
if not getattr(args, "server", None):
242+
raise ValueError(
243+
"No Tableau server URL. Provide --server, set the TABLEAU_SERVER env "
244+
"var, or run in an interactive terminal to be prompted."
245+
)
236246
site = getattr(args, "site", None) or ""
237247
if getattr(args, "jwt", None):
238248
return TSC.JWTAuth(args.jwt, site_id=site)

samples/explore_favorites.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,22 @@ def main():
5959
)
6060
)
6161

62-
server.favorites.delete_favorite_workbook(user, my_workbook)
63-
print(f"Workbook deleted from favorites. Workbook Name: {my_workbook.name}, Workbook ID: {my_workbook.id}")
62+
# Cleanup — delete the favorites we just created. Must stay inside the
63+
# `with server.auth.sign_in(...)` block; a delete after sign-out fails
64+
# with a not-signed-in error. Each guard mirrors the "add" check above
65+
# so we do not try to delete a favorite we never created.
66+
if my_workbook is not None:
67+
server.favorites.delete_favorite_workbook(user, my_workbook)
68+
print(f"Workbook deleted from favorites. Workbook Name: {my_workbook.name}, Workbook ID: {my_workbook.id}")
6469

65-
server.favorites.delete_favorite_view(user, my_view)
66-
print(f"View deleted from favorites. View Name: {my_view.name}, View ID: {my_view.id}")
70+
if my_view is not None:
71+
server.favorites.delete_favorite_view(user, my_view)
72+
print(f"View deleted from favorites. View Name: {my_view.name}, View ID: {my_view.id}")
6773

68-
if my_datasource is not None:
69-
server.favorites.delete_favorite_datasource(user, my_datasource)
70-
print(
71-
"Datasource deleted from favorites. Datasource Name: {}, Datasource ID: {}".format(
72-
my_datasource.name, my_datasource.id
74+
if my_datasource is not None:
75+
server.favorites.delete_favorite_datasource(user, my_datasource)
76+
print(
77+
"Datasource deleted from favorites. Datasource Name: {}, Datasource ID: {}".format(
78+
my_datasource.name, my_datasource.id
79+
)
7380
)
74-
)

samples/extracts.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ def main():
1818
parser.add_argument("--create", action="store_true")
1919
parser.add_argument("--delete", action="store_true")
2020
parser.add_argument("--refresh", action="store_true")
21-
parser.add_argument("--workbook", required=False)
22-
parser.add_argument("--datasource", required=False)
21+
# --workbook / --datasource are mutually exclusive; if neither is passed we
22+
# fall back to picking the first workbook on the site (see below).
23+
target = parser.add_mutually_exclusive_group()
24+
target.add_argument("--workbook")
25+
target.add_argument("--datasource")
2326
args = parser.parse_args()
2427

2528
resolve_credentials(args)
@@ -47,13 +50,17 @@ def main():
4750
print([workbook.name for workbook in all_workbooks])
4851

4952
if all_workbooks:
50-
# Pick one workbook from the list
51-
wb = all_workbooks[3]
53+
# Fall back to the first workbook on the site. For a real run,
54+
# pass --workbook <id> for a workbook you know has an extract.
55+
wb = all_workbooks[0]
5256

5357
if args.create:
54-
print("create extract on wb ", wb.name)
55-
extract_job = server.workbooks.create_extract(wb, includeAll=True)
56-
print(extract_job)
58+
if wb is None:
59+
print("no workbook selected to create an extract on")
60+
else:
61+
print(f"create extract on workbook {wb.name}")
62+
extract_job = server.workbooks.create_extract(wb, includeAll=True)
63+
print(extract_job)
5764

5865
if args.refresh:
5966
extract_job = None
@@ -69,9 +76,12 @@ def main():
6976
print(extract_job)
7077

7178
if args.delete:
72-
print("delete extract on wb ", wb.name)
73-
jj = server.workbooks.delete_extract(wb)
74-
print(jj)
79+
if wb is None:
80+
print("no workbook selected to delete an extract from")
81+
else:
82+
print(f"delete extract on workbook {wb.name}")
83+
jj = server.workbooks.delete_extract(wb)
84+
print(jj)
7585

7686

7787
if __name__ == "__main__":

samples/manage_subscriptions.py

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,6 @@
1818
# --schedule-id <schedule_id> \
1919
# --subject "Daily sales snapshot"
2020
#
21-
# # Create an "On Extract Refresh" subscription (fires when the referenced
22-
# # extract-refresh schedule completes, rather than on the schedule's time
23-
# # trigger). --schedule-id must reference an extract-refresh schedule.
24-
# python samples/manage_subscriptions.py create \
25-
# --target-type view \
26-
# --target-id <view_id> \
27-
# --schedule-id <extract_refresh_schedule_id> \
28-
# --subject "Snapshot when refresh finishes" \
29-
# --on-extract-refresh
30-
#
3121
# # Delete an existing subscription.
3222
# python samples/manage_subscriptions.py delete --id <subscription_id>
3323
#
@@ -67,34 +57,19 @@ def handle_create(server, args):
6757
# The REST API expects lowercase content types ("workbook" or "view").
6858
target = TSC.Target(args.target_id, args.target_type.lower())
6959

70-
if args.on_extract_refresh:
71-
# Extract-refresh-triggered: the subscription fires when the referenced
72-
# extract-refresh schedule finishes running the refresh. On Tableau
73-
# Cloud this shows up as schedule type "On Extract Refresh" in the UI.
74-
# `SubscriptionItem.on_extract_refresh` wires up schedule_id and the
75-
# refreshExtractTriggered flag together so the server accepts the
76-
# payload; --schedule-id must reference an extract-refresh schedule.
77-
new_sub = TSC.SubscriptionItem.on_extract_refresh(
78-
subject=args.subject,
79-
extract_refresh_schedule_id=args.schedule_id,
80-
user_id=user_id,
81-
target=target,
82-
)
83-
else:
84-
new_sub = TSC.SubscriptionItem(
85-
subject=args.subject,
86-
schedule_id=args.schedule_id,
87-
user_id=user_id,
88-
target=target,
89-
)
60+
new_sub = TSC.SubscriptionItem(
61+
subject=args.subject,
62+
schedule_id=args.schedule_id,
63+
user_id=user_id,
64+
target=target,
65+
)
9066
if args.message:
9167
new_sub.message = args.message
9268
new_sub.attach_image = args.attach_image
9369
new_sub.attach_pdf = args.attach_pdf
9470

9571
created = server.subscriptions.create(new_sub)
96-
trigger = "on-extract-refresh" if args.on_extract_refresh else "on-schedule"
97-
print(f"Created {trigger} subscription {created.id} " f"for user {created.user_id} against {created.target}")
72+
print(f"Created subscription {created.id} for user {created.user_id} against {created.target}")
9873

9974

10075
def handle_delete(server, args):
@@ -139,17 +114,6 @@ def main():
139114
default=False,
140115
help="Also attach a PDF snapshot (default: off).",
141116
)
142-
create_p.add_argument(
143-
"--on-extract-refresh",
144-
action="store_true",
145-
default=False,
146-
help=(
147-
"Fire this subscription when the referenced extract-refresh schedule "
148-
"completes, rather than on the schedule's time trigger. --schedule-id "
149-
"must reference an extract-refresh schedule (see create_extract_refresh_"
150-
"subscription.py for the fully worked example)."
151-
),
152-
)
153117
create_p.set_defaults(func=handle_create)
154118

155119
delete_p = subcommands.add_parser("delete", help="Delete a subscription by ID.")

samples/publish_datasource.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def main():
6666
TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, args.project)
6767
)
6868
projects = list(TSC.Pager(server.projects, req_options))
69+
if not projects:
70+
raise ValueError(f"No project named {args.project!r} on this site.")
6971
if len(projects) > 1:
7072
raise ValueError("The project name is not unique")
7173
project_id = projects[0].id
@@ -97,11 +99,8 @@ def main():
9799
new_datasource, args.file, publish_mode, connection_credentials=new_conn_creds
98100
)
99101
print(
100-
(
101-
"{}Datasource published. Datasource ID: {}".format(
102-
new_datasource.id, tableauserverclient.datetime_helpers.timestamp()
103-
)
104-
)
102+
f"[{tableauserverclient.datetime_helpers.timestamp()}] "
103+
f"Datasource published. Datasource ID: {new_datasource.id}"
105104
)
106105
print("\t\tClosing connection")
107106

samples/refresh_tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def main():
3333
parser = argparse.ArgumentParser(description="Get all of the refresh tasks available on a server")
3434
add_common_arguments(parser)
3535
# Options specific to this sample
36-
subcommands = parser.add_subparsers()
36+
subcommands = parser.add_subparsers(dest="command", required=True)
3737

3838
list_arguments = subcommands.add_parser("list")
3939
list_arguments.set_defaults(func=handle_list)

samples/update_workbook_data_freshness_policy.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717

1818
def main():
19-
parser = argparse.ArgumentParser(description="Creates sample schedules for each type of frequency.")
19+
parser = argparse.ArgumentParser(
20+
description="Update a workbook's data freshness policy across the supported schedule types."
21+
)
2022
add_common_arguments(parser)
2123
# Options specific to this sample:
2224
# This sample has no additional options, yet. If you add some, please add them here
@@ -37,10 +39,12 @@ def main():
3739
print([workbook.name for workbook in all_workbooks])
3840

3941
if all_workbooks:
40-
# Pick 1 workbook that has live datasource connection.
41-
# Assuming 1st workbook met the criteria for sample purposes
42-
# Data Freshness Policy is not available on extract & file-based datasource.
43-
sample_workbook = all_workbooks[2]
42+
# Pick 1 workbook that has a live datasource connection. Data
43+
# freshness policy is not available on extract or file-based
44+
# datasources, so this sample will print a warning below if the
45+
# chosen workbook has none. Adjust the index (or add a lookup by
46+
# name) for a workbook on your site with a live connection.
47+
sample_workbook = all_workbooks[0]
4448

4549
# Get more info from the workbook selected
4650
# Troubleshoot: if sample_workbook_extended.data_freshness_policy.option returns with AttributeError

0 commit comments

Comments
 (0)