JobItem.from_response at models/job_item.py:224 parses <notes> elements:
notes = [note.text for note in element.findall(".//t:notes", namespaces=ns)] or None
But the actual REST API response wraps notes in a <statusNotes> container with typed <statusNote> children — none of that is captured:
<statusNotes>
<statusNote type="classifier" value="value" text="note" />
</statusNotes>
This is documented as the Query Job response schema — the doc lists enumerated type values like CountOfUsersAddedToSite, CountOfUsersSkipped, CountOfUsersWithInsufficientLicenses, CountOfUsersInformationUpdated, etc.
Reproducer
Verified live against Tableau Server 2025.1. POST /sites/<id>/users/import returns a job; polling GET /sites/<id>/jobs/<job-id> after completion returns XML like:
<job id="..." type="UserImport" ... finishCode="1">
<statusNotes>
<statusNote type="line" value="0" />
<statusNote type="errorCode" value="1" />
<statusNote type="message" value="..." />
<statusNote type="username" value="unknown" />
</statusNotes>
</job>
TSC's job.notes for this response is [].
Impact
Callers can't tell what happened at the row level. For user-import jobs, that means no way to distinguish "5 added, 1 skipped" from a general "job finished with finishCode=1". Any caller wanting to emit per-row status output has to bypass TSC and parse the raw XML themselves.
Proposed fix
Add a new structured status_notes property on JobItem, populated from <t:statusNotes>/<t:statusNote> with all three attributes preserved:
status_notes = [
{"type": note.get("type"), "value": note.get("value"), "text": note.get("text")}
for note in element.findall(".//t:statusNotes/t:statusNote", namespaces=ns)
]
Leave the existing notes: list[str] untouched for backwards compatibility.
Discovered while
Planning tabcmd createsiteusers --nowait / --silent-progress work (tableau/tabcmd#35). The current tabcmd 2 command prints one line per row imported; switching to bulk_add + wait_for_job for Classic parity needs per-row status back — which requires this data.
JobItem.from_responseatmodels/job_item.py:224parses<notes>elements:But the actual REST API response wraps notes in a
<statusNotes>container with typed<statusNote>children — none of that is captured:This is documented as the Query Job response schema — the doc lists enumerated
typevalues likeCountOfUsersAddedToSite,CountOfUsersSkipped,CountOfUsersWithInsufficientLicenses,CountOfUsersInformationUpdated, etc.Reproducer
Verified live against Tableau Server 2025.1.
POST /sites/<id>/users/importreturns a job; pollingGET /sites/<id>/jobs/<job-id>after completion returns XML like:TSC's
job.notesfor this response is[].Impact
Callers can't tell what happened at the row level. For user-import jobs, that means no way to distinguish "5 added, 1 skipped" from a general "job finished with finishCode=1". Any caller wanting to emit per-row status output has to bypass TSC and parse the raw XML themselves.
Proposed fix
Add a new structured
status_notesproperty onJobItem, populated from<t:statusNotes>/<t:statusNote>with all three attributes preserved:Leave the existing
notes: list[str]untouched for backwards compatibility.Discovered while
Planning tabcmd
createsiteusers --nowait/--silent-progresswork (tableau/tabcmd#35). The current tabcmd 2 command prints one line per row imported; switching tobulk_add+wait_for_jobfor Classic parity needs per-row status back — which requires this data.