GeezerVison is showing github.com — fix: parse Tableau Cloud's inlined subscription <schedule> (#1627) by jacalata · Pull Request #1875 · tableau/server-client-python
Conversation
jacalata
commented
Aug 26, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
Motivation
Related to #1627. Tableau Server references a schedule by id inside a <subscription>:
<schedule id="cfb2..." name="Weekday mornings"/>
Tableau Cloud inlines the schedule instead — no id, but a frequency, nextRunAt, and nested <frequencyDetails>. SubscriptionItem only parsed the Server shape, so on Cloud every subscription came back with schedule_id is None and no structured access to the schedule metadata. Client code doing [s for s in subs if s.schedule_id == target] silently returned [] on Cloud.
Behavior change
Read side (server response → model):
SubscriptionItem.schedule (new) is populated for both shapes with a ScheduleItem. On Server: schedule.id and .name are set. On Cloud: schedule.frequency, schedule.next_run_at, and schedule.interval_item are set; schedule_id remains None on Cloud because the API doesn't send one.
ScheduleItem.frequency (new property): the underlying XML attribute was already being read to pick the interval type but was discarded — now exposed.
parse_datetime accepts Cloud's %Y-%m-%dT%H:%M:%S%z form (e.g. 2026-08-29T16:55:00-0700) in addition to Server's ...Z. Unparseable input still returns None.
- Malformed Cloud interval data (missing
start attribute, or <interval hours="3"/> outside IntervalItem.VALID_INTERVALS) no longer crashes subscriptions.get(); the offending schedule degrades to interval_item = None with a warning log, sibling schedules are unaffected.
Write side (user assignment → model):
property_is_datetime (setter decorator) now raises ValueError when a str value fails to parse. Previously the attribute was silently set to None. This affects direct user assignment to typed datetime properties on model classes; server-response parsing is unchanged.
Testing Run
- New
test/assets/subscription_get_cloud.xml mirrors the shape observed on a live Cloud site (API 3.29). Companion parse tests verify Server and Cloud paths.
- Three malformed-Cloud fixtures + tests cover
_parse_interval_item's graceful-degradation paths (missing <frequencyDetails>, empty <intervals/>, <interval hours="3"/>).
- Existing subscription tests extended to cover sub 2 and
get_subscription_by_id.
- New
test/test_datetime_helpers.py directly covers parse_datetime (None / "" / garbage / Server / Cloud / colon-offset / microseconds) and property_is_datetime (valid / bad / non-str inputs).
test/test_schedule.py::test_get asserts the new frequency property on the standard /schedules endpoint.
- Live verification (not in-tree): raw XML captured from
main-windows.tsi.lan (API 3.31, Server shape) and stage-dp1 (API 3.29, Cloud shape) — matches the two fixture shapes.
- Full suite: 936 passed, 1 skipped.
black --check and mypy clean.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Tableau Server references a schedule by id inside a <subscription>:
<schedule id="cfb2..." name="Weekday mornings"/>
Tableau Cloud inlines the schedule instead -- no id attribute, but
a `frequency`, `nextRunAt` and a nested `<frequencyDetails>`:
<schedule frequency="Daily" nextRunAt="2026-08-29T16:55:00-0700">
<frequencyDetails start="16:55:00" end="16:55:00">
<intervals>
<interval hours="24"/>
<interval weekDay="Saturday"/>
</intervals>
</frequencyDetails>
</schedule>
Previously `SubscriptionItem` only pulled `id`/`name` off `<schedule>`,
so on Cloud every subscription came back with `schedule_id == None`
and no structured way to see what the API sent. Client code filtering
`[s for s in subs if s.schedule_id == target]` silently returned `[]`
on Cloud. This is the bug described in issue #1627.
Model changes:
* `SubscriptionItem` now populates a `schedule: ScheduleItem` attribute
for both shapes. On Server, `schedule.id`/`.name` are set (and
`schedule_id` remains populated for back-compat). On Cloud,
`schedule.frequency`, `schedule.next_run_at`, and
`schedule.interval_item` are set. `schedule_id` is `None` on Cloud --
the API does not send one, unavoidable. Docstring calls out the
Cloud-vs-Server discriminator and warns callers who filter by
`schedule_id`. Fixes a latent bug where the previous Cloud branch
assigned a list (return of `ScheduleItem.from_element`) to
`sub.schedule` instead of a single item.
* `ScheduleItem` gains a `frequency` property. The XML attribute was
already being read to select the interval type but was discarded;
now it's exposed so callers can distinguish the Cloud shape without
reaching into the interval object. The class `Attributes` docstring
is rewritten to cover every public property and to note that only
`frequency` / `next_run_at` / `interval_item` are populated when the
item comes from an inlined Cloud subscription schedule.
* `_parse_interval_item` is defensive against malformed Cloud data:
a `<frequencyDetails>` without a `start` attribute no longer crashes
on `strptime(None, ...)`, and an out-of-range `<interval hours="3"/>`
(or unknown weekDay / monthDay) no longer raises `ValueError` out of
`IntervalItem` and poisons sibling schedules in the same page. Bad
data degrades to `interval_item = None` with a warning log.
Datetime plumbing:
* `parse_datetime` learns the Tableau Cloud `%Y-%m-%dT%H:%M:%S%z` form
(e.g. `2026-08-29T16:55:00-0700`) in addition to the Server `...Z`
form. Unparseable input still returns `None` on the read path
(preserving the pre-change contract that a malformed server-side
date cannot crash a page-through of unrelated data).
* `property_is_datetime` now raises `ValueError` when `parse_datetime`
returns `None` for a non-empty str value. Bad *user* input is
surfaced at the assignment site instead of silently nulling the
attribute.
* `TABLEAU_CLOUD_DATE_FORMAT` is public, matching the existing
`TABLEAU_DATE_FORMAT`.
Tests:
* New `test/assets/subscription_get_cloud.xml` mirroring the shape
observed on `stage-dp1` (API 3.29) and matching parse tests for
both Cloud and Server shapes.
* Three edge-case Cloud fixtures + tests: no `<frequencyDetails>`,
empty `<intervals/>`, out-of-set `<interval hours="3"/>`.
* Existing subscription tests extended to cover sub 2 and
`get_subscription_by_id`.
* New `test/test_datetime_helpers.py` with direct coverage of
`parse_datetime` (None / "" / garbage / Server / Cloud / colon-offset
/ microseconds) and `property_is_datetime` (valid / bad / non-str).
* `test/test_schedule.py::test_get` asserts the new
`ScheduleItem.frequency` property on the standard /schedules endpoint.
Refs: #1627
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|

Coverage Report
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
A few API/docs/type-safety inconsistencies were introduced (notably around Cloud schedules where schedule_id can be None), and should be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates the client’s subscription and schedule parsing to correctly handle Tableau Cloud’s inline <schedule> shape (no schedule id, but frequency, nextRunAt, and nested <frequencyDetails>), while preserving Tableau Server behavior and improving datetime parsing/validation.
Changes:
- Populate a structured
SubscriptionItem.schedule for both Server (id/name reference) and Cloud (inline frequency/next-run/intervals) schedule shapes.
- Expose
ScheduleItem.frequency and make interval parsing degrade gracefully (warn + interval_item=None) on malformed Cloud interval payloads.
- Expand
parse_datetime to accept Cloud offset timestamps and make property_is_datetime setters raise on unparseable strings; add comprehensive fixtures and tests.
File summaries
| File |
Description |
| test/test_subscription.py |
Adds coverage for Cloud inline schedule parsing and malformed interval degradation, plus asserts structured schedule parsing for Server shape. |
| test/test_schedule.py |
Asserts the new ScheduleItem.frequency property on /schedules responses. |
| test/test_datetime_helpers.py |
New direct tests for parse_datetime formats and property_is_datetime strict setter behavior. |
| test/assets/subscription_get_cloud.xml |
Cloud-shaped subscription fixture with inline schedule and frequency details. |
| test/assets/subscription_get_cloud_no_frequency_details.xml |
Cloud fixture with schedule missing <frequencyDetails>. |
| test/assets/subscription_get_cloud_empty_intervals.xml |
Cloud fixture with empty <intervals/>. |
| test/assets/subscription_get_cloud_bad_hours.xml |
Cloud fixture with invalid interval hours that should degrade without breaking siblings. |
| tableauserverclient/models/subscription_item.py |
Adds schedule attribute parsing and documents Server vs Cloud schedule shapes. |
| tableauserverclient/models/schedule_item.py |
Adds frequency property and makes interval parsing resilient with warning logs on malformed data. |
| tableauserverclient/models/property_decorators.py |
Makes datetime setter decorator strict by raising on unparseable strings. |
| tableauserverclient/datetime_helpers.py |
Extends datetime parsing to accept Cloud %z offsets while staying lenient for server-response parsing. |
Review details
- Files reviewed: 11/11 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Widen SubscriptionItem.__init__.schedule_id to Optional[str]; Cloud
inline <schedule> elements have no id and _parse_element passes None
- Rewrite property_is_datetime docstring: parse_datetime accepts both
trailing-Z and explicit-offset forms; setter now raises ValueError on
unparseable strings
- Correct the SubscriptionItem docstring's Cloud-vs-Server discriminator:
the reliable check is (schedule is not None and schedule.id is None),
not (interval_item is not None) - Cloud can omit <frequencyDetails>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Previously parse_datetime returned Cloud datetimes with the wire's offset
attached (e.g. tzinfo == timezone(-7:00)), so the same field had different
tzinfo depending on cell type. Callers using .strftime("...Z") got wrong
output on Cloud; callers comparing to a UTC constant with == got False even
on the same instant.
Convert the Cloud branch's strptime result via .astimezone(utc), so every
next_run_at returned from TSC is uniformly tzinfo == utc regardless of
whether the wire used trailing Z or an explicit numeric offset. Callers
who want the site's local zone can inspect the wire elsewhere (schedule
name, admin API); nobody was relying on the offset today.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Cosmetic — reads cleaner as a single literal.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
jacalata
added a commit
that referenced
this pull request
Sep 11, 2026
- update() now falls back to subscription.schedule.id when schedule_id
is None. Fixes Cloud fetch-then-update (schedule_id is always None on
Cloud inline-schedule responses per #1875), which the property
docstring recommended but the guard rejected.
- request_factory create_req: replace assert with if-raise so python -O
and direct-import callers get an actionable error instead of an
unhelpful ElementTree TypeError.
- Add regression tests: (a) manually-constructed SubscriptionItem
through update() emits the intended refreshExtractTriggered value,
and (b) fetch-parse-mutate-update round-trip preserves the flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Cloud updates still send an empty schedule, and two schedule-parsing paths can return incorrect frequency or interval data.
4 open findings
3 resolved since last review
🧠 Review effort: Lite
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Comment on lines
365
to
+367
|
|
start_time = parsed_response.get("start", None)
|
|
|
start_time = datetime.strptime(start_time, "%H:%M:%S").time()
|
|
|
if start_time is not None:
|
|
|
start_time = datetime.strptime(start_time, "%H:%M:%S").time()
|
Uh oh!
There was an error while loading. Please reload this page.
|
|
schedule = None
|
|
|
schedule: Optional[ScheduleItem] = None
|
|
|
if schedule_element is not None:
|
|
|
schedule_id = schedule_element.get("id", None)
|
|
@jacalata copilot found several things that I think you'll want to check here.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
- ScheduleItem._parse_common_tags discarded the frequency returned by
_parse_element, so a schedule returned from schedules.update() had
frequency None. Capture it and pass it to _set_values, as from_element does.
- _parse_interval_item left hourly hours="N" as a string, so an invalid
value such as 3 passed HourlyInterval validation instead of degrading to
interval_item None. Convert with float(), matching the Daily branch.
- Add tests for <frequencyDetails> without start and for hourly hours
validation; update test_create_hourly to expect numeric intervals.
Co-Authored-By: Claude Code <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 99bad08. _parse_common_tags now keeps the frequency from _parse_element and passes it to _set_values, as from_element does.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added test_get_subscriptions_cloud_frequency_details_without_start in 99bad08. Its fixture has <frequencyDetails> with intervals but no start. It asserts subscriptions.get() succeeds and that interval_item is None.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 99bad08. The hourly branch now uses float(i[1]), like the Daily branch. hours="3" now raises ValueError, is caught, and degrades to interval_item=None. Added test_get_subscriptions_cloud_hourly_hours_are_numeric. test_create_hourly previously asserted the string ("8",), so I changed it to (8.0,).
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Schedule 'null' could not be found error on update needs a server-side fix: TSC sends an empty <schedule/>, and the server has to accept a subscription update that carries no schedule id. This PR only covers parsing the inline Cloud schedule into .schedule, so I'm changing Fixes #1627 to Related to #1627.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cloud subscription updates still serialize an empty schedule because schedule_id remains unset.
2 open issues
2 resolved since last review
Review effort: Lite
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
The review found no active blocking issues in the changes or their supporting tests.
2 open issues
Review effort: Lite
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This branch has not been deployed
No deployments
Reviewers
At least 1 approving review is required to merge this pull request.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
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.
|