Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/reference/objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ The data objects calendaring-jmap's methods return: calendars, calendar events,
.. automodule:: calendaring_jmap.objects.contact
:members:

.. automodule:: calendaring_jmap.objects.event_search_result
:members:

See :doc:`errors` for the exception hierarchy.
1 change: 1 addition & 0 deletions news/+account-scoped-sync-tokens.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``get_sync_token``, ``get_objects_by_sync_token``, ``get_task_sync_token``, and ``get_tasks_by_sync_token`` now accept an optional ``account_id``, for syncing a calendar or task list shared with you instead of only your own account.
1 change: 1 addition & 0 deletions news/+search-events-paging.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``search_events`` now accepts ``position``, ``limit``, and ``calculate_total`` for paging through large result sets, and returns an ``EventSearchResult`` instead of a bare list. Use ``.events`` for the matching events.
1 change: 1 addition & 0 deletions news/+sync-max-changes.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``get_objects_by_sync_token`` and ``get_tasks_by_sync_token`` now accept an optional ``max_changes`` to cap how many changes the server returns in one page.
1 change: 1 addition & 0 deletions news/68.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``create_event``, ``send_invite``, ``update_event``, and ``delete_event`` now accept an optional ``if_in_state`` argument. The server rejects the write with ``JMAPMethodError`` (``error_type == "stateMismatch"``) if the account's state has moved since, instead of silently applying over a concurrent change.
1 change: 1 addition & 0 deletions news/69.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``get_objects_by_sync_token`` and ``get_tasks_by_sync_token`` now return a 6-tuple ``(added, modified, deleted, old_state, new_state, has_more_changes)`` instead of a 4-tuple, and no longer raise when the server truncates a page (``hasMoreChanges: true``). Call the method again with ``new_state`` to fetch the next page.
11 changes: 11 additions & 0 deletions src/calendaring_jmap/_methods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def build_query(
sort: list[dict] | None = None,
position: int = 0,
limit: int | None = None,
calculate_total: bool = False,
) -> tuple:
"""Build a ``<Object>/query`` method call tuple.

Expand All @@ -61,6 +62,14 @@ def build_query(
ordering.
position: Zero-based index of the first result to return.
limit: Maximum number of IDs to return. ``None`` means no limit.
calculate_total: If true, asks the server to include the total
number of matching results in its response
(RFC 8620 section 5.5). Omitted from the request entirely
when false, matching the spec's own default, rather than sent
as an explicit ``false``: confirmed live that Cyrus includes a
``total`` in its response either way, so a caller reading the
response must still gate on its own ``calculate_total`` value,
not on whether ``total`` happens to be present.

Returns:
A 3-tuple ``(method, arguments_dict, call_id)``.
Expand All @@ -72,6 +81,8 @@ def build_query(
args["sort"] = sort
if limit is not None:
args["limit"] = limit
if calculate_total:
args["calculateTotal"] = calculate_total
return (method, args, call_id)


Expand Down
87 changes: 60 additions & 27 deletions src/calendaring_jmap/_methods/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def build_event_query(
limit: int | None = None,
expand_recurrences: bool = False,
time_zone: str | None = None,
calculate_total: bool = False,
) -> tuple:
"""Build a ``CalendarEvent/query`` method call tuple.

Expand Down Expand Up @@ -151,6 +152,7 @@ def build_event_query(
fall in the window.
time_zone: The time zone for ``after``/``before``, defaults to
``Etc/UTC`` server-side if omitted.
calculate_total: See ``build_query``.

Returns:
A 3-tuple ``("CalendarEvent/query", arguments_dict, call_id)``.
Expand All @@ -163,6 +165,7 @@ def build_event_query(
sort,
position,
limit,
calculate_total,
)
if expand_recurrences:
args["expandRecurrences"] = expand_recurrences
Expand Down Expand Up @@ -194,10 +197,34 @@ def build_event_get_by_query_result(account_id: str, properties: list[str] | Non
)


def parse_event_query(response_args: dict) -> tuple[int, int | None]:
"""Parse the arguments dict from a ``CalendarEvent/query`` response.

Args:
response_args: The second element of a ``methodResponses`` entry
whose method name is ``"CalendarEvent/query"``.

Returns:
A 2-tuple ``(position, total)``:

- ``position``: Zero-based index of the first id in this response
within the complete result set (RFC 8620 section 5.5).
- ``total``: Total number of matching results, or ``None`` if the
server didn't include one. Callers that care whether this is
genuinely absent versus the server ignoring ``calculateTotal``
should gate on their own request, not on this return value:
confirmed live that Cyrus includes ``total`` unconditionally,
which RFC 8620 section 5.5 says the server "MUST" omit when
``calculateTotal`` wasn't requested.
"""
return response_args.get("position", 0), response_args.get("total")


def build_event_set_create(
account_id: str,
events: dict[str, dict],
send_scheduling_messages: bool = False,
if_in_state: str | None = None,
) -> tuple:
"""Build a ``CalendarEvent/set`` method call for creating events.

Expand All @@ -209,25 +236,30 @@ def build_event_set_create(
batch call.
send_scheduling_messages: If true, the server sends iTIP scheduling
messages to the event's participants (draft-ietf-jmap-calendars section 5.9).
if_in_state: A state string from a prior ``CalendarEvent/get`` or
``CalendarEvent/changes`` response. If given and the account's
current state has moved past it, the server rejects the whole
call with a ``stateMismatch`` error instead of applying it
(RFC 8620 section 5.3). ``None`` means apply unconditionally.

Returns:
A 3-tuple ``("CalendarEvent/set", arguments_dict, call_id)``.
"""
return (
"CalendarEvent/set",
{
"accountId": account_id,
"create": dict(events),
"sendSchedulingMessages": send_scheduling_messages,
},
"ev-set-create-0",
)
args: dict = {
"accountId": account_id,
"create": dict(events),
"sendSchedulingMessages": send_scheduling_messages,
}
if if_in_state is not None:
args["ifInState"] = if_in_state
return ("CalendarEvent/set", args, "ev-set-create-0")


def build_event_set_update(
account_id: str,
updates: dict[str, dict],
send_scheduling_messages: bool = False,
if_in_state: str | None = None,
) -> tuple:
"""Build a ``CalendarEvent/set`` method call for updating events.

Expand All @@ -240,25 +272,26 @@ def build_event_set_update(
send_scheduling_messages: If true, the server sends iTIP scheduling
messages to the event's participants, or back to the organizer
if this account isn't the event's origin (draft-ietf-jmap-calendars section 5.9).
if_in_state: See ``build_event_set_create``.

Returns:
A 3-tuple ``("CalendarEvent/set", arguments_dict, call_id)``.
"""
return (
"CalendarEvent/set",
{
"accountId": account_id,
"update": updates,
"sendSchedulingMessages": send_scheduling_messages,
},
"ev-set-update-0",
)
args: dict = {
"accountId": account_id,
"update": updates,
"sendSchedulingMessages": send_scheduling_messages,
}
if if_in_state is not None:
args["ifInState"] = if_in_state
return ("CalendarEvent/set", args, "ev-set-update-0")


def build_event_set_destroy(
account_id: str,
ids: list[str],
send_scheduling_messages: bool = False,
if_in_state: str | None = None,
) -> tuple:
"""Build a ``CalendarEvent/set`` method call for destroying events.

Expand All @@ -268,19 +301,19 @@ def build_event_set_destroy(
send_scheduling_messages: If true, and this account is the event's
origin, the server sends an iTIP CANCEL to the event's
participants (draft-ietf-jmap-calendars section 5.9.2.2).
if_in_state: See ``build_event_set_create``.

Returns:
A 3-tuple ``("CalendarEvent/set", arguments_dict, call_id)``.
"""
return (
"CalendarEvent/set",
{
"accountId": account_id,
"destroy": ids,
"sendSchedulingMessages": send_scheduling_messages,
},
"ev-set-destroy-0",
)
args: dict = {
"accountId": account_id,
"destroy": ids,
"sendSchedulingMessages": send_scheduling_messages,
}
if if_in_state is not None:
args["ifInState"] = if_in_state
return ("CalendarEvent/set", args, "ev-set-destroy-0")


def parse_event_set(
Expand Down
Loading
Loading