Skip to content

fix(invites): read RSVPs through zone changes, not a zone query - #352

Open
MrJarnould wants to merge 2 commits into
timlaing:mainfrom
MrJarnould:fix/invites-rsvps-private-zone
Open

MrJarnould wants to merge 2 commits into
timlaing:mainfrom
MrJarnould:fix/invites-rsvps-private-zone

Conversation

@MrJarnould

Copy link
Copy Markdown

Proposed change

InvitesService.rsvps(event) raises for every event you host. It works only for events shared
with you.

It issues a per-zone records/query, and an event's own zone rejects that:

{
  "serverErrorCode" : "BAD_REQUEST",
  "reason" : "syncToken operations supported only in SyncZone"
}

Against a live account, before:

private  My cool event        participants=1  -> InvitesApiError: Bad Request (400)
private  My Test Event        participants=4  -> InvitesApiError: Bad Request (400)
private  pyicloud test …      participants=2  -> InvitesApiError: Bad Request (400)
shared   Test event           participants=2  -> 2 rsvp(s)

Shared events work because their zones are owned elsewhere; your own event zones do not
support being queried at all.

The changes feed does support them — it is the same mechanism #339 introduced for enumerating
the shared scope, and a per-zone changes call returns an event's EventDetails, RSVP and
cloudkit.share records together. rsvps() now reads that and filters to RSVP.

Two details:

  • desiredRecordTypes asks Apple for the RSVP records alone, and the filter is kept anyway,
    because a projection is a request rather than a guarantee.
  • Paging comes free from iter_changes(), which fix(invites): read the shared scope zone by zone #339 already added and which follows the sync
    token to the end.

Not a regression from #339. This path dates from #246. It was simply unreachable while
events() raised on every account — nobody got far enough to call rsvps().

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New service (thank you!)
  • New feature (which adds functionality to an existing service)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests
  • Documentation or code sample

Additional information

Cut from main, touching only pyicloud/services/invites/service.py and
tests/test_invites.py.

Testing. 936 tests pass on Python 3.10, 3.11, 3.12, 3.13 and 3.14, run locally. Three are
new and one existing test was rewritten onto the new mechanism — its fixture records are the
same shape a changes page returns, so it still describes what Apple sends. Four fail against
main.

Verified live across both scopes, which is the part that matters here, since the whole bug
is a difference between them:

private  Test event           -> 1 rsvp(s)   ['Jacob Arnould']
private  My cool event        -> 1 rsvp(s)   ['Jacob Arnould']
private  My Test Event        -> 2 rsvp(s)   ['Jacob Arnould', 'Mrjarnould']
private  pyicloud test …      -> 2 rsvp(s)   ['Jacob Arnould', 'mrjarnould']
shared   Test event           -> 2 rsvp(s)   ['Jacob Arnould', <guest>]

The shared event returns the same two responses as before, so the path that already worked is
unchanged.

How I found it. While building cancel() and publish() (#351), checking which events had
no guests before testing a write against one. Names in the output above are redacted where they
belong to someone else.

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

  • Documentation added/updated to README

🤖 Generated with Claude Code

`rsvps()` raised for every event you host. It issued a per-zone
`records/query`, and an event's own zone rejects that:

    "syncToken operations supported only in SyncZone"

Only events shared with you worked, because their zones are owned
elsewhere. Live, all four hosted events returned 400 and the one shared
event returned its responses.

The zone's changes feed does support these zones -- it is what timlaing#339 uses to
enumerate the shared scope, and a per-zone changes call returns an event's
EventDetails, RSVP and cloudkit.share records together. `rsvps()` now reads
that and filters to RSVP records, asking for them via desiredRecordTypes
and filtering anyway, since a projection is a request rather than a
guarantee. Paging comes free from iter_changes().

Not a regression from timlaing#339: this path dates from timlaing#246 and was unreachable
while events() still raised on every account, so nobody got far enough to
call it.

Verified live across both scopes: the four hosted events now return their
responses and the shared event returns the same two as before.

Closes timlaing#350

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 11 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: a09b4e23-4133-4882-b0a2-b216df7e8c48

📥 Commits

Reviewing files that changed from the base of the PR and between 86c4bc9 and 2f1260c.

📒 Files selected for processing (2)
  • pyicloud/services/invites/service.py
  • tests/test_invites.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The first commit fixed rsvps() and missed its sibling. `_fetch_event_full`
made the same rejected per-zone query, and swallowed the failure into a
debug log -- so `event()` returned every hosted event with `rsvps = ()` and
nothing said why.

Both now go through one `_rsvp_records()` helper, so there is no second
place to forget.

This turns out to be the root of a bug that looked unrelated. `rsvp()`
locates the existing response in `event.rsvps` to decide between create and
update. With hydration silently empty it never found one, always chose
create, and CloudKit answered CONFLICT -- which was itself discarded (timlaing#353)
and reported as "modify response missing record".

Verified live on an event with no guests: `event()` now hydrates its RSVP,
and `rsvp()` changes the record. GOING -> MAYBE with a message, then back.
Before this, that write did nothing at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant