-
Notifications
You must be signed in to change notification settings - Fork 110
feat(jmap): add caldav/jmap — JMAP calendar and task client #625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
8fa05b5
feat(jmap): add JMAP calendar client foundation
SashankBhamidi 762c507
style: apply ruff formatting fixes
SashankBhamidi 1d2cdd1
feat(jmap): add context manager support to JMAPClient
SashankBhamidi 8bd2c5c
fix(jmap): correct RFC attributions in constants.py; add get_jmap_cli…
SashankBhamidi 91cacd4
fix(jmap): exclude server-set fields from JMAPCalendar.to_jmap()
SashankBhamidi 5e16ceb
feat(jmap): add JMAPEvent dataclass and CalendarEvent method builders
SashankBhamidi 4ad24be
fix(jmap): resolve relative apiUrl in fetch_session against base URL
SashankBhamidi 858cdd9
style(jmap): remove redundant section-label comment from JMAPClient
SashankBhamidi 383030d
fix(jmap): handle null collection fields in JMAPEvent.from_jmap; expa…
SashankBhamidi 4fb7105
feat(jmap): add iCalendar ↔ JSCalendar conversion layer
SashankBhamidi 9e24561
fix(deps): ignore pytz in deptry DEP003 check
SashankBhamidi d3c609d
fix(jmap): correct parse_event_set return type; handle int byMonth va…
SashankBhamidi de8f733
style: apply ruff-format to jscal_to_ical byMonth fix
SashankBhamidi b88adbd
refactor(jmap): deduplicate participant imip extraction
SashankBhamidi 25f062f
feat(jmap): add event CRUD methods to JMAPClient
SashankBhamidi 1f6d66f
fix(jmap): add timeout to fetch_session; drop unused variable in test
SashankBhamidi 4ef9b13
feat(jmap): add JMAPClient.search_events with batched query+get
SashankBhamidi 37dbb01
fix(jmap): narrow broad exception catch in pytz timezone handling
SashankBhamidi b42efc4
feat(jmap): add get_sync_token and get_objects_by_sync_token
SashankBhamidi d1e8462
feat(jmap): add Task and TaskList support via RFC 9553
SashankBhamidi c53022e
style: remove section-label comments from JMAP test files
SashankBhamidi 064d90f
feat(jmap): add AsyncJMAPClient mirroring public methods as coroutines
SashankBhamidi 04a4549
fix(jmap): replace deprecated pytz with stdlib zoneinfo in jscal_to_ical
SashankBhamidi f1c9145
fix(jmap): replace deprecated pytz with stdlib zoneinfo in jscal_to_ical
SashankBhamidi 591554d
fix(jmap): remove duplicate pytz block left by partial suggestion apply
SashankBhamidi 364b250
feat(jmap): add event integration tests; fix UTC start encoding in ic…
SashankBhamidi c1be8aa
fix(jmap): remove unused _format_local_dt import from jscal_to_ical
SashankBhamidi 432299a
fix(jmap): collapse double participants loop; fix _start_to_dtstart d…
SashankBhamidi 3ab9078
docs(jmap): add JMAP usage documentation and autodoc stubs
SashankBhamidi eedaac0
fix(jmap): prefer primaryAccounts for account selection; fix test sec…
SashankBhamidi 49b9c8d
style(jmap): fix ruff-format blank line after fallback import in sess…
SashankBhamidi 39ce181
fix(jmap): guard create_event against malformed server response; pres…
SashankBhamidi fd71e1d
docs(jmap): add JMAP client entry to CHANGELOG
tobixen 2eba687
fix(jmap): correct RFC attributions for task support; remove unverifi…
SashankBhamidi 3a3225d
docs(jmap): remove unverified Fastmail claim; drop incorrect RFC 9553…
SashankBhamidi 4a15d0c
feat(jmap): align API with CalDAV v3 — calendar-scoped search/get/add…
SashankBhamidi d96f1a0
docs(jmap): clarify JMAPTask.from_jmap docstring — JSCalendar not jCal
SashankBhamidi ce0b14b
refactor(jmap): replace JMAPEvent/JMAPTask/JMAPTaskList dataclasses w…
SashankBhamidi 6a7ee7c
refactor(jmap): rename methods/ to _methods/ to signal internal API
SashankBhamidi fa9cef1
feat(jmap): return JMAPCalendarObject from search/get; deduplicate se…
SashankBhamidi 2b61f2b
fix(jmap): update integration tests for JMAPCalendarObject return type
SashankBhamidi 0b48bc6
refactor(jmap): eliminate duplication and shadow-builtin parameter names
SashankBhamidi 8bc2adf
feat(jmap): add @type Event, Stalwart Docker test server, session URL…
SashankBhamidi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -206,6 +206,7 @@ def resolve_features(features): | |
| "features", | ||
| "enable_rfc6764", | ||
| "require_tls", | ||
| "protocol", | ||
| ] | ||
| ) | ||
|
|
||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| """ | ||
| JMAP calendar support for python-caldav. | ||
|
|
||
| Provides synchronous and asynchronous JMAP clients with the same public API as | ||
| the CalDAV client, so user code works regardless of server protocol. | ||
|
|
||
| Basic usage:: | ||
|
|
||
| from caldav.jmap import get_jmap_client | ||
|
|
||
| client = get_jmap_client( | ||
| url="https://jmap.example.com/.well-known/jmap", | ||
| username="alice", | ||
| password="secret", | ||
| ) | ||
| calendars = client.get_calendars() | ||
|
|
||
| Async usage:: | ||
|
|
||
| from caldav.jmap import get_async_jmap_client | ||
|
|
||
| async with get_async_jmap_client( | ||
| url="https://jmap.example.com/.well-known/jmap", | ||
| username="alice", | ||
| password="secret", | ||
| ) as client: | ||
| calendars = await client.get_calendars() | ||
| """ | ||
|
|
||
| from caldav.jmap.async_client import AsyncJMAPClient | ||
| from caldav.jmap.client import JMAPClient | ||
| from caldav.jmap.error import ( | ||
| JMAPAuthError, | ||
| JMAPCapabilityError, | ||
| JMAPError, | ||
| JMAPMethodError, | ||
| ) | ||
| from caldav.jmap.objects.calendar import JMAPCalendar | ||
| from caldav.jmap.objects.calendar_object import JMAPCalendarObject | ||
|
|
||
| _JMAP_KEYS = {"url", "username", "password", "auth", "auth_type", "timeout"} | ||
|
|
||
|
|
||
| def get_jmap_client(**kwargs) -> JMAPClient | None: | ||
| """Create a :class:`JMAPClient` from configuration. | ||
|
|
||
| Configuration is read from the same sources as :func:`caldav.get_davclient`: | ||
|
|
||
| 1. Explicit keyword arguments (``url``, ``username``, ``password``, …) | ||
| 2. Environment variables (``CALDAV_URL``, ``CALDAV_USERNAME``, …) | ||
| 3. Config file (``~/.config/caldav/calendar.conf`` or equivalent) | ||
|
|
||
| Returns ``None`` if no configuration is found, matching the behaviour | ||
| of :func:`caldav.get_davclient`. | ||
|
|
||
| Example:: | ||
|
|
||
| client = get_jmap_client(url="https://jmap.example.com/.well-known/jmap", | ||
| username="alice", password="secret") | ||
| """ | ||
| from caldav.config import get_connection_params | ||
|
|
||
| conn_params = get_connection_params(**kwargs) | ||
| if conn_params is None: | ||
| return None | ||
| return JMAPClient(**{k: v for k, v in conn_params.items() if k in _JMAP_KEYS}) | ||
|
|
||
|
|
||
| def get_async_jmap_client(**kwargs) -> AsyncJMAPClient | None: | ||
| """Create an :class:`AsyncJMAPClient` from configuration. | ||
|
|
||
| Accepts the same arguments and reads configuration from the same sources | ||
| as :func:`get_jmap_client`. Returns ``None`` if no configuration is found. | ||
|
|
||
| Example:: | ||
|
|
||
| async with get_async_jmap_client( | ||
| url="https://jmap.example.com/.well-known/jmap", | ||
| username="alice", password="secret" | ||
| ) as client: | ||
| calendars = await client.get_calendars() | ||
| """ | ||
| from caldav.config import get_connection_params | ||
|
|
||
| conn_params = get_connection_params(**kwargs) | ||
| if conn_params is None: | ||
| return None | ||
| return AsyncJMAPClient(**{k: v for k, v in conn_params.items() if k in _JMAP_KEYS}) | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "JMAPClient", | ||
| "AsyncJMAPClient", | ||
| "get_jmap_client", | ||
| "get_async_jmap_client", | ||
| "JMAPError", | ||
| "JMAPCapabilityError", | ||
| "JMAPAuthError", | ||
| "JMAPMethodError", | ||
| "JMAPCalendar", | ||
| "JMAPCalendarObject", | ||
| ] |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| def parse_set_response(response_args: dict) -> tuple[dict, dict, list[str], dict, dict, dict]: | ||
| """Parse the arguments dict from any JMAP ``*/set`` method response. | ||
|
|
||
| Returns a 6-tuple ``(created, updated, destroyed, not_created, not_updated, not_destroyed)``. | ||
| """ | ||
| created: dict = response_args.get("created") or {} | ||
| updated: dict = response_args.get("updated") or {} | ||
| destroyed: list[str] = response_args.get("destroyed") or [] | ||
| not_created: dict = response_args.get("notCreated") or {} | ||
| not_updated: dict = response_args.get("notUpdated") or {} | ||
| not_destroyed: dict = response_args.get("notDestroyed") or {} | ||
| return created, updated, destroyed, not_created, not_updated, not_destroyed |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| """ | ||
| JMAP Calendar method builders and response parsers. | ||
|
|
||
| These are pure functions — no HTTP, no state. They build the request | ||
| tuples that go into a ``methodCalls`` list, and parse the corresponding | ||
| ``methodResponses`` entries. | ||
|
|
||
| Method shapes follow RFC 8620 §3.3 (get), §3.4 (changes), §3.5 (set); Calendar-specific | ||
| properties are defined in the JMAP Calendars specification. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from caldav.jmap.objects.calendar import JMAPCalendar | ||
|
|
||
|
|
||
| def build_calendar_get( | ||
| account_id: str, | ||
| ids: list[str] | None = None, | ||
| properties: list[str] | None = None, | ||
| ) -> tuple: | ||
| """Build a ``Calendar/get`` method call tuple. | ||
|
|
||
| Args: | ||
| account_id: The JMAP accountId to query. | ||
| ids: List of calendar IDs to fetch, or ``None`` to fetch all. | ||
| properties: List of property names to return, or ``None`` for all. | ||
|
|
||
| Returns: | ||
| A 3-tuple ``("Calendar/get", arguments_dict, call_id)`` suitable | ||
| for inclusion in a ``methodCalls`` list. | ||
| """ | ||
| args: dict = {"accountId": account_id, "ids": ids} | ||
| if properties is not None: | ||
| args["properties"] = properties | ||
| return ("Calendar/get", args, "cal-get-0") | ||
|
|
||
SashankBhamidi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def parse_calendar_get(response_args: dict) -> list[JMAPCalendar]: | ||
| """Parse the arguments dict from a ``Calendar/get`` method response. | ||
|
|
||
| Args: | ||
| response_args: The second element of a ``methodResponses`` entry | ||
| whose method name is ``"Calendar/get"``. | ||
|
|
||
| Returns: | ||
| List of :class:`~caldav.jmap.objects.calendar.JMAPCalendar` objects. | ||
| Returns an empty list if ``"list"`` is absent or empty. | ||
| """ | ||
| return [JMAPCalendar.from_jmap(item) for item in response_args.get("list", [])] | ||
|
|
||
|
|
||
| def build_calendar_changes(account_id: str, since_state: str) -> tuple: | ||
| """Build a ``Calendar/changes`` method call tuple. | ||
|
|
||
| Args: | ||
| account_id: The JMAP accountId to query. | ||
| since_state: The ``state`` string from a previous ``Calendar/get`` | ||
| or ``Calendar/changes`` response. | ||
|
|
||
| Returns: | ||
| A 3-tuple ``("Calendar/changes", arguments_dict, call_id)``. | ||
| """ | ||
| return ( | ||
| "Calendar/changes", | ||
| {"accountId": account_id, "sinceState": since_state}, | ||
| "cal-changes-0", | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.