Skip to content

feat: parse <statusNotes>/<statusNote> into JobItem.status_notes (#1850) - #1852

Merged
stephendeoca merged 1 commit into
developmentfrom
jac/1850-statusnotes
Aug 6, 2026
Merged

stephendeoca merged 1 commit into
developmentfrom
jac/1850-statusnotes

Conversation

@jacalata

@jacalata jacalata commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

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.

Fix

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.

Verification

  • The existing test/assets/job_get_by_id.xml test asset already contains both a <notes> element and a <statusNotes>/<statusNote> block. test_get_by_id now asserts the structured value in addition to the legacy notes list.
  • Live-verified against Tableau Server 2025.1 via 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 prior job.notes was [] for these responses.

Closes #1850.

Test plan

  • test/test_job.py::test_get_by_id augmented to assert the structured status_notes from the existing asset
  • test_status_notes_empty_when_absent — job with no <statusNotes> yields []
  • test_status_notes_multiple_entries — 4-entry list, exercises the text attribute coming back as None when omitted
  • Full test_job.py suite: 16 passed
  • Full TSC test suite: 868 passed, 1 skipped

Discovered while

Planning tabcmd createsiteusers --nowait / --silent-progress work (tableau/tabcmd#35). Switching from the per-user users.add loop to bulk_add + wait_for_job for Classic parity needs per-row status back — which needed this data.

🤖 Generated with Claude Code

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.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
tableauserverclient
   __init__.py50100% 
   config.py150100% 
   datetime_helpers.py2511 96%
   exponential_backoff.py200100% 
   filesys_helpers.py310100% 
   namespace.py2633 88%
tableauserverclient/bin
   __init__.py20100% 
   _version.py358212212 41%
tableauserverclient/helpers
   __init__.py10100% 
   logging.py20100% 
   strings.py3111 97%
tableauserverclient/models
   __init__.py460100% 
   collection_item.py4177 83%
   column_item.py553232 42%
   connection_credentials.py351111 69%
   connection_item.py941414 85%
   custom_view_item.py1442121 85%
   data_acceleration_report_item.py5411 98%
   data_alert_item.py15844 97%
   data_freshness_policy_item.py1551515 90%
   database_item.py2073636 83%
   datasource_item.py3001212 96%
   dqw_item.py10455 95%
   exceptions.py40100% 
   extensions_item.py13244 97%
   extract_item.py4444 91%
   favorites_item.py6988 88%
   fileupload_item.py190100% 
   flow_item.py1491010 93%
   flow_run_item.py710100% 
   group_item.py8966 93%
   groupset_item.py4977 86%
   interval_item.py1823232 82%
   job_item.py1921010 95%
   linked_tasks_item.py7911 99%
   location_item.py2922 93%
   metric_item.py1291313 90%
   oidc_item.py6333 95%
   pagination_item.py3411 97%
   permissions_item.py1111212 89%
   project_item.py2073131 85%
   property_decorators.py1001818 82%
   reference_item.py2622 92%
   revision_item.py5911 98%
   schedule_item.py20966 97%
   server_info_item.py3777 81%
   site_item.py6361313 98%
   subscription_item.py10122 98%
   table_item.py1191818 85%
   tableau_auth.py612525 59%
   tableau_types.py2711 96%
   tag_item.py150100% 
   target.py60100% 
   task_item.py5622 96%
   user_item.py3101818 94%
   view_item.py2201616 93%
   virtual_connection_item.py6488 88%
   webhook_item.py6911 99%
   workbook_item.py3621616 96%
tableauserverclient/server
   __init__.py90100% 
   exceptions.py40100% 
   filter.py2911 97%
   pager.py3311 97%
   query.py1431515 90%
   request_factory.py1335195195 85%
   request_options.py38655 99%
   server.py1882323 88%
   sort.py60100% 
tableauserverclient/server/endpoint
   __init__.py350100% 
   auth_endpoint.py771111 86%
   custom_views_endpoint.py1521212 92%
   data_acceleration_report_endpoint.py210100% 
   data_alert_endpoint.py942323 76%
   databases_endpoint.py1113030 73%
   datasources_endpoint.py3233333 90%
   default_permissions_endpoint.py4433 93%
   dqw_endpoint.py451616 64%
   endpoint.py2122020 91%
   exceptions.py7766 92%
   extensions_endpoint.py310100% 
   favorites_endpoint.py942222 77%
   fileuploads_endpoint.py510100% 
   flow_runs_endpoint.py6299 85%
   flow_task_endpoint.py2122 90%
   flows_endpoint.py1985353 73%
   groups_endpoint.py12699 93%
   groupsets_endpoint.py7277 90%
   jobs_endpoint.py6799 87%
   linked_tasks_endpoint.py370100% 
   metadata_endpoint.py881414 84%
   metrics_endpoint.py5566 89%
   oidc_endpoint.py4211 98%
   permissions_endpoint.py4433 93%
   projects_endpoint.py1782424 87%
   resource_tagger.py1273535 72%
   schedules_endpoint.py1191111 91%
   server_info_endpoint.py361010 72%
   sites_endpoint.py1302727 79%
   subscriptions_endpoint.py561414 75%
   tables_endpoint.py1103636 67%
   tasks_endpoint.py6366 90%
   users_endpoint.py18388 96%
   views_endpoint.py15099 94%
   virtual_connections_endpoint.py1131010 91%
   webhooks_endpoint.py5499 83%
   workbooks_endpoint.py3382222 93%
TOTAL12007142388% 

@stephendeoca
stephendeoca self-requested a review August 6, 2026 17:39
@stephendeoca
stephendeoca merged commit e729e66 into development Aug 6, 2026
31 checks passed
@stephendeoca
stephendeoca deleted the jac/1850-statusnotes branch August 6, 2026 20:20
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JobItem drops statusNotes from job responses (per-row detail lost)

2 participants