Finish adopting UTCDateTime, and add created_at to TrackResponse - #82
Merged
Conversation
…sponse
ADR-0007's follow-up called for typing "the 49 timestamp fields still declared as
plain `string`". The survey found something better: `UTCDateTime` already exists in
`api/schemas/common.py` — `Annotated[datetime, PlainSerializer(to_rfc3339),
WithJsonSchema({format: date-time})]` — built for exactly this and adopted by
three files. This finishes the job for the generated surface.
**Wire-compatible by construction, not by luck.** The producers already called
`to_rfc3339` by hand at 47 sites; the annotation moves that call into the
serialiser. The bytes are identical, which `tests/test_timestamp_wire_format.py`
now pins: naive and aware both emit `...Z`, any offset normalises to UTC, and the
serialization-mode schema still carries `format: date-time` — the last being the
thing `PlainSerializer` would otherwise silently degrade.
Fields typed as date-time went from 8 to 27.
**One real bug found on the way.** `CuratedPromptsResponse.generated_at` was
`utcnow().isoformat()` — naive, no offset. Verified live: `2026-08-03T11:15:48.143190`.
That is precisely what `to_rfc3339`'s docstring warns about: Swift's decoder
rejects it and JavaScript reads it as *local* time. It is on the `library` tag, so
it reaches the generated client.
**Deliberately not migrated.** `library_artists.release_date` is a release date
from a scraped dict, not an instant, and `library_sync.started_at` comes from a
progress dict. Typing either would declare a format their values do not keep — the
hazard that makes this a classification job rather than a mechanical pass.
`TrackResponse.created_at` is new: ADR-0021's `dateAdded` column sorts server-side
already, and the column was built, seen blank on every row, and removed for want
of this field.
**Known asymmetry:** `favorited_at` becomes nullable, because the response is built
by `model_validate(track)` and only then assigned. The server always populates it
in the one endpoint that returns it; the web's TS type still says `string`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
…matting Caught by CI, reproduced locally against a real database. The migration unwrapped every `to_rfc3339(x)` call in the files it touched, and I checked the receiving fields with a pattern matching `_at`, `_date` and `timestamp`. `PendingGroupResponse.earliest_scan` matches none of those, so it kept its `str` annotation while its producer began handing it a `datetime` — `test_pending_tracks_with_data` failed with a pydantic string_type error. Re-checked properly this time: every field whose `to_rfc3339` call was removed, by name, against its annotation. `earliest_scan` was the only one left behind. Also fixes the import ordering ruff flagged in library_discover.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
…ted_at `created_at=mt.created_at if mt.created_at else ""` was fine while the field was a `str`. Now that it is a `UTCDateTime` the fallback is a type error, and mypy said so — the column is non-nullable, so the branch was already unreachable. CI reported this as "Backend Lint", which runs ruff *and* mypy; ruff was clean locally, which is why the first fix missed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
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.
Phase 3. The Swift half is
familiar-apple#57.The follow-up was better than it looked
ADR-0007 asked for typing "the 49 timestamp fields still declared as plain
string". The survey found thatUTCDateTimealready exists inapi/schemas/common.py—Annotated[datetime, PlainSerializer(to_rfc3339), WithJsonSchema({format: date-time})]— purpose-built for this, and adopted by three files. So this isn't "type 49 fields", it's "finish adopting the type that was already built."Fields typed as
date-time: 8 → 27.Wire-compatible by construction, not by luck
The producers already called
to_rfc3339by hand at 47 sites. The annotation just moves that call into the serialiser, so the bytes are identical.tests/test_timestamp_wire_format.pypins it: naive and aware both emit...Z, any offset normalises to UTC,NonestaysNone, and the serialization-mode schema still carriesformat: date-time— the last being exactly whatPlainSerializerwould otherwise silently degrade to a bare string.That mattered: naively switching these to
datetimewould have emitted naive ISO without theZ, which Swift rejects outright and JavaScript reads as local time.A real bug found on the way
CuratedPromptsResponse.generated_atwasutcnow().isoformat(). Verified against the live server:That is the exact defect
to_rfc3339's docstring exists to describe, on an endpoint carried by thelibrarytag — so it reaches the generated client. Now aUTCDateTime.Deliberately not migrated
This is a classification job, not a mechanical pass:
library_artists.release_date— a release date out of a scraped dict, not an instant.library_sync.started_at— comes from a progress dict.Typing either would declare a format their values don't keep.
favorites.favorited_atwas the third hazard — it defaulted to"", which is not a valid date-time and would have broken a strict decoder at runtime.created_atonTrackResponseADR-0021 records that the
dateAddedcolumn was built, seen blank on every row, and removed becauseTrackResponsecarried no such field. It does now, and the generated Swift hascreatedAt: Foundation.Date?. Restoring the column is Phase 4.Known asymmetry, stated rather than hidden
favorited_atbecomes nullable, because the response is built bymodel_validate(track)and only then assigned — a required field would fail validation there. The server always populates it in the one endpoint that returns it, but the web's TS type still saysstring. Worth reconciling; not silently.Verification
ruffclean.make openapiand vendored tofamiliar-apple, where both schemes build and 507 Swift tests pass against the regenerated client.🤖 Generated with Claude Code
https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW