themeparks-backfill: --since/--until, incremental reruns, final days only; changes() exposes opening - #33
Merged
Conversation
`history.changes()` yielded the rows of the raw history response and dropped its `opening` object, the state in force at the start of the range. Without it the minutes between midnight and an entity's first change have no known status, so a day rebuilt from raw history disagrees with the daily summary whenever a ride is still running from the night before. `changes()` now returns a `HistoryChanges` iterator: iterating it yields exactly the `(entity id, row)` pairs it always did, and its `opening` attribute is a dict of `HistoryOpening` keyed by entity id, one per entity in the response. The request is still made on first use, and reading `opening` costs no extra request. The async mirror returns `AsyncHistoryChanges`, whose `opening` is readable once the response has arrived (iterate first, or `await .load()`). `HistorySpan.final_through` is the newest day whose daily row will not change again: the earlier of `recorded_to` and `retrievable_through`. A property, so the three-field tuple is unchanged. Tested against a real capture (Space Mountain, 2026-09-26, whose opening is OPERATING): opening plus rows cover the day with no unknown second, and the rebuilt first open and last close match the daily row's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…written Three defects from a customer-style run of 4.0.1, each reproduced first: - No date range. A key that reaches the whole archive downloaded all of it every time. `--since` and `--until` take a real calendar day as YYYY-MM-DD, both inclusive; since after until is refused before any request. - A finished park was finished forever. A rerun printed "already complete" and exited 0 without fetching a new day, so a nightly cron never updated. A finished file is now carried forward from the day after its last one. - The newest rows were partial. A run ended on retrievableThrough, usually today, whose row is the day so far, and the archive records days 2 to 3 behind live data. A run now ends at span().final_through, says so, and the next run adds the held-back days once final. Every row written is final. State files move to version 2 (`end` is now the newest final day). A version 1 file from this SDK is upgraded rather than refused: rows dated within seven days of its old end are removed and fetched again, since which of them were final was never recorded. Kept rows are left byte for byte; a file whose newest row is older than the cut is not rewritten. Fixed on the way, each with a test and a committed mutant: - a finished state file written to another column layout fell through to a fresh start in append mode, writing the archive a second time under a second header, exit 0. Refused now, as an unfinished one already was. - a rerun interrupted before its first page recorded no resume point, and the next run would have started from the top of the archive and appended it again. The run's own start is recorded. - a state file whose data file was deleted was continued, producing a file that starts part-way through its range. The park is fetched again instead. A `--since` inside the existing file is accepted (a fixed or rolling cron line works); one before the file's first day, or one that would leave a gap, is refused with the way out rather than ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README's queue table said `waitTime: int | None`. The API's schema declares it a JSON `number` and the generated models type it `float`, so a raw row dumped to JSON says `45.0`. The table now says `float | None`, explains the `.0` and how to get an int, and a test holds the table to the models' types. README, cookbook and CHANGELOG cover `--since`/`--until`, reruns that bring a file up to date, the final-day boundary, the one-time correction of 4.0 files, and `changes().opening`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uate typing.get_type_hints cannot evaluate the models' `float | None` on 3.9; pydantic already resolved it with the backport the package depends on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le inside the cut A continued file whose next day is older than the key may read (a cron that missed more days than the window, or a lapsed plan) carried on from the key's first day, leaving a gap the state file did not record. It is refused now, exit 1, with the file and state untouched and a message naming --overwrite. A 4.0 file lying wholly inside the seven-day refetch window, as every anonymous 7-day file does, was trimmed to nothing with a state continuing from a day the key could no longer read. It is downloaded again instead. A fixed --since older than the key's window is pinned by a test: the file starts at the key's first day and the same command line succeeds every night. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the cut Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HistoryChanges.close() and AsyncHistoryChanges.aclose() end iteration early, as they did on the generators these replaced. HistoryOpening is exported so callers can annotate against it, and the docstring explains `degraded` and `observedAt`. final_through no longer promises the day will never change: it is the newest day the archive has recorded, the place to stop if each day is fetched once. The server can re-record a past day after a feed repair. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…icates a day An interrupted nightly extension appended the same days twice. The state was written only at the end of a run or on an SDK error, so Ctrl-C, SIGTERM or SIGKILL during an extension left `complete: true` with the old end, and the rerun appended those days again (70 duplicate rows, exit 0). The state is now written before the first request and after every page, with the data file's size at that moment, and the rerun first truncates the file to that size. Anything written after the last checkpoint is discarded and fetched again, so stopping at any point costs at most the page in flight. A file shorter than its checkpoint is refused. State writes are atomic (temp file and rename). Also, each with a test: - `start` is the first day actually written (after the key's window floor) and a new `since` field keeps the start asked for. The same --since keeps working; a different one before the file's first day is refused. - A partial first page resumed from `lastDay`, the newest day ANY entity reached, losing the days of entities behind it. Checkpoints remove the case for new files; an older state with only `lastDay` goes back a whole page. - SIGTERM stops like Ctrl-C and exits 143. - An advisory lock refuses a second run on the same park and --out (POSIX). - A failed trim removes its scratch copy. - An unfinished file with --until before its resume point says so. - The anonymous notice says only final days are written, usually 4 or 5. The committed mutant list gains 14 mutants, including the four a review found surviving; 43/43 are killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…immutable Co-Authored-By: Claude Opus 5.5 (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.
What this fixes
A customer-style run of
themeparks-backfill4.0.1 against a key that reaches the whole archive found four problems. Each was reproduced against the live API before it was fixed.--since YYYY-MM-DDand--until YYYY-MM-DD. Both days are inclusive, each must be a real calendar day (so2025-02-30,2025-1-1and20250101are rejected), and--sinceafter--untilis refused before any request is made.already completeand exited 0 without fetching anything, so a nightly cron looked healthy but never updated. Now a finished file continues from the day after its last one. An interrupted run still resumes at its page boundary.retrievableThrough, which is usually today. Today's row is the day so far, and the API docs say the archive records days 2 to 3 behind live data, so the last few days of every file were written while they could still change. Now:span().final_through(the earlier ofrecordedToandretrievableThrough) and says when it held days back;history.changes()threw awayopening. That is the state in force at the start of the range. It is now exposed without breaking anything: iterating yields the same(entity id, row)pairs as before, and.openingis a dict ofHistoryOpeningkeyed by entity id, costing no extra request. For async, the attribute is readable once you have iterated or afterawait changes.load().waitTimeis anint. The spec says it is anumberand the models usefloat. The README now matches the models, and a test enforces it.Also fixed along the way (each has a test and a committed mutant)
--overwrite.--sinceolder than the key's window keeps working. The file starts at the key's first day, and the same command line succeeds every later night. A test pins this.Interrupted runs (from review)
complete: truewith the old end, and the rerun appended the same days again. The state is now written atomically before the first request and after every page, together with the data file's size. A rerun first truncates the file to that size, so stopping at any point costs at most the page in flight. A file shorter than its checkpoint is refused.--outare refused by an advisory lock (POSIX only).startis the first day actually written, after the key's window floor, and a newsincefield keeps the start that was asked for. The same--sincekeeps working. A different one earlier than the file's first day is refused.lastDay, the newest day any entity had reached. They now go back a whole page, 31 days, instead of resuming there.recordedToare what the archive has recorded, and each is fetched once. The README explains how to fetch a range again if the archive re-records past days.HistoryChanges.close(),AsyncHistoryChanges.aclose(), and an exportedHistoryOpening, whosedegradedfield is now documented.Compatibility
endis now the newest final day, and the state gainssince(the start asked for) andsize(bytes of the file it vouches for).endand fetches those days again. The other rows stay byte for byte, and a file whose newest row is older than that is not rewritten at all. A 4.0 file lying wholly inside that 7-day refetch window, as every anonymous 7-day file does, is simply downloaded again.--sinceon a rerun:--since, or a later one inside the file, is accepted (a fixed or rolling cron line works);backfill_parkgains a keyword-onlywindowargument.changes()now returns an iterator object instead of a generator.Verification
Live, without a key, on Magic Kingdom: a Ctrl-C raised 39 rows into an extension left the state at the previous checkpoint. The rerun discarded the 26,073 bytes written after it and finished with 314 rows, 0 duplicated
(entityId, date)pairs and exit 0. A third run reported up to date. A different--sincebefore the file's real first day was refused with exit 1.pytest tests/unit: 457 passed on 3.9, 3.12 and 3.13.ruff check,ruff format --check,mypy themeparksandmkdocs build --strictare clean.tests/mutation/run.py: 43/43 killed, including 25 new mutants for these defects.Live, without a key, against Magic Kingdom:
recordedTo, and no(entityId, date)pair was duplicated;--since/--untilwrote CSV for exactly those days, and a rerun appended one new final day;changes()returned 72 openings, 47 of themOPERATINGat midnight.Follow-up (not in this PR)
🤖 Generated with Claude Code