Repository navigation
feat: parse <statusNotes>/<statusNote> into JobItem.status_notes (#1850) - #1852
Merged
Merged
Conversation
The REST Query Job response schema documents a structured status-notes
block:
<statusNotes>
<statusNote type="CountOfUsersAddedToSite" value="5" text="..." />
<statusNote type="CountOfUsersSkipped" value="1" text="..." />
</statusNotes>
JobItem was parsing only the sibling legacy `<notes>` element (emitted
by some job types like extractRefreshJob), missing the modern
statusNotes entirely. For UserImport jobs and any other multi-row job
where individual rows have distinct outcomes, `job.notes` came back as
an empty list even when the server had sent detailed structured status.
Add `JobItem.status_notes: list[dict]`, each dict with keys `type`,
`value`, `text` (any of which may be None if the server omitted them).
The legacy `notes: list[str]` attribute is unchanged for backwards
compatibility -- it still parses the `<notes>` element still emitted by
extract-refresh and similar older job types.
Verified against the public REST doc:
https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobs_tasks_and_schedules.htm#query_job
The existing job_get_by_id.xml test asset already contained a
statusNotes block; the get_by_id test now asserts the structured value
in addition to the legacy notes list. Two new tests cover the absent
case (yields []) and the multi-note case with attribute omissions.
Discovered while planning tabcmd createsiteusers nowait / silent-progress
work (tableau/tabcmd#35); a live probe against Tableau Server 2025.1
confirmed the server emits this schema for UserImport jobs.
Fixes #1850.
stephendeoca
self-requested a review
August 6, 2026 17:39
stephendeoca
approved these changes
Aug 6, 2026
bcantoni
pushed a commit
that referenced
this pull request
Oct 14, 2026
…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.
bcantoni
pushed a commit
to tableau/tabcmd
that referenced
this pull request
Oct 27, 2026
Unblocks #455 (createsiteusers --nowait/--silent-progress), which depends on JobItem.status_notes (tableau/server-client-python#1852), shipped in TSC v0.42 (released 2026-10-15). Co-authored-by: Claude Code <noreply@anthropic.com>
jacalata
added a commit
to tableau/tabcmd
that referenced
this pull request
Nov 2, 2026
* Fix #35: --nowait / --silent-progress on createsiteusers (needs TSC release) tabcmd Classic accepts --nowait and --silent-progress on createsiteusers; tabcmd 2 was missing both flags. Adding them meaningfully requires more than argparse: Classic sends the CSV as a single bulk-import job and polls the server-side JobItem, while tabcmd 2 has been walking the CSV row by row and calling users.add per user. With no server-side job to poll, --nowait had nothing to skip. This PR switches createsiteusers to server.users.bulk_add + a single JobItem, and wires the two flags. Behavior changes: - Default: submit bulk_add, wait for the JobItem to complete via server.jobs.wait_for_job, print a summary derived from the completed job's status_notes (CountOfUsersAddedToSite / CountOfUsersSkipped / CountOfUsersProcessed) plus any per-row error notes. Same shape as Classic's default output. - --nowait: submit bulk_add and return immediately. Job ID is printed before return so callers can query it via a separate call. - --silent-progress: still waits for the job, but suppresses the status header, the queued-job line, and the per-row summary. Framework logging (errors, debug) is unaffected. - --complete / --no-complete: Classic used a ClientXML with_transaction param that the REST endpoint doesn't expose, so we approximate with client-side pre-flight validation: - --complete (default): validate_file_for_import(strict=True), any malformed row aborts the whole import before submission. - --no-complete: validate leniently, submit whatever parses. Doesn't cover server-side row failures mid-import; those fall through to job.status_notes on the completed job. Flag spelling: Classic uses --nowait (one word). The existing set_no_wait_option helper defined --no-wait but had zero callers, so switched the spelling to Classic's without a compatibility alias. Depends on TSC >= (next release), which will ship JobItem.status_notes (tableau/server-client-python#1852, merged). Draft while awaiting a tagged TSC release + tabcmd's TSC pin bump. Fixes #35. * fix: import JobFailedException from its module TSC top-level doesn't re-export JobFailedException; import directly from tableauserverclient.server.endpoint.exceptions. Fixes the mypy attr-defined error in CI. * style: apply black 22 to new test files * Fail loudly on older TSC; document --continue-if-exists is a no-op The status_notes fallback previously used getattr(..., None) or [] which meant an older TSC would print added=0, skipped=0, processed=len(input) with no indication anything was wrong. Detect the attribute missing and exit with a message pointing at the pinned TSC version. --continue-if-exists is a global flag used across the create-* commands to downgrade 409 conflicts to INFO. bulk_add is inherently tolerant of duplicate users (they get counted under CountOfUsersSkipped) so the flag is a no-op here. Keep the flag on the parser for consistency, log the no-op at DEBUG, and cover with a test so a future refactor doesn't accidentally rely on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop redundant --continue-if-exists no-op test test_continue_if_exists_is_a_noop_documented_in_debug just re-ran the default happy path with the flag set. bulk_add is called and wait_for_job is called either way, so this asserts nothing that the base test doesn't. The behavioral comment lives on the code path in create_site_users.py already. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Bump tableauserverclient to 0.42 and drop pre-status_notes guard in createsiteusers 0.42 exposes JobItem.status_notes, so the hasattr guard, its needs_newer_tsc string and the test for it are dead code. --continue-if-exists is silently ignored by createsiteusers (bulk_add tolerates existing users), so the debug-only no-op message is removed too. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: createsiteusers numeric timeout, failed-job diagnostics - pass --timeout to wait_for_job as an int (it arrives as a string and TSC compares it with elapsed seconds) - on JobFailedException, log status_notes/notes before exiting nonzero - tests for both Co-Authored-By: Claude Code <noreply@anthropic.com> * localize: regenerate English catalog for createsiteusers messages Co-Authored-By: Claude Code <noreply@anthropic.com> * style: format test_create_site_users.py with black 26.5.1 Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: createsiteusers timeout message, job id under --nowait, bulk_add errors - on wait timeout, say the import is still running and print the job id - always print the job id under --nowait, even with --silent-progress - route any bulk_add failure (not just ServerResponseError) through exit_with_error - tests for the first two Co-Authored-By: Claude Code <noreply@anthropic.com> * createsiteusers: save per-row import errors to a file (--errors-file, default per job id) Under --silent-progress rejected rows are written to createsiteusers-errors-<jobid>.txt instead of being dropped; --errors-file overrides the path and also enables the file without --silent-progress. Co-Authored-By: Claude Code <noreply@anthropic.com> * createsiteusers: fix rejected-row detail to use job.notes, not merged status_notes The server does not emit one statusNote per rejected row for a UserImport job. RestApiHelper.getStatusNotesErrors merges each error field (line/username/message) across ALL failed rows into a single statusNote per field, joined by ";", and omits the CountOf* aggregates entirely once there are errors. The actual one-line-per-row detail is the legacy <notes> text built by UserManagementService.getHumanReadableCSVImportSummary, which TSC surfaces as job.notes. _job_detail_lines now prefers job.notes and only falls back to non-CountOf* status_notes if notes is empty. Added tests/assets/createsiteusers_job_with_errors.xml as a fixture matching the real response shape, parsed via TSC.JobItem.from_response in _rejected_job() instead of a hand-rolled status_notes mock. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
The REST Query Job response schema documents a structured status-notes block:
JobItemwas parsing only the sibling legacy<notes>element (emitted by some job types likeextractRefreshJob), missing the modernstatusNotesentirely. ForUserImportjobs and any other multi-row job where individual rows have distinct outcomes,job.notescame back as an empty list even when the server had sent detailed structured status.Fix
Add
JobItem.status_notes: list[dict], each dict with keystype,value,text(any of which may beNoneif the server omitted them). The legacynotes: list[str]attribute is unchanged for backwards compatibility — it still parses the<notes>element still emitted by extract-refresh and similar older job types.Verification
test/assets/job_get_by_id.xmltest asset already contains both a<notes>element and a<statusNotes>/<statusNote>block.test_get_by_idnow asserts the structured value in addition to the legacy notes list.POST /sites/<id>/users/import+GET /sites/<id>/jobs/<job-id>— the server sends<statusNote type="line" value="0" />,<statusNote type="errorCode" value="1" />, etc. for failures. The priorjob.noteswas[]for these responses.Closes #1850.
Test plan
test/test_job.py::test_get_by_idaugmented to assert the structuredstatus_notesfrom the existing assettest_status_notes_empty_when_absent— job with no<statusNotes>yields[]test_status_notes_multiple_entries— 4-entry list, exercises thetextattribute coming back asNonewhen omittedtest_job.pysuite: 16 passedDiscovered while
Planning tabcmd
createsiteusers --nowait/--silent-progresswork (tableau/tabcmd#35). Switching from the per-userusers.addloop tobulk_add+wait_for_jobfor Classic parity needs per-row status back — which needed this data.🤖 Generated with Claude Code