calendaring-jmap 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calendaring_jmap/__init__.py +103 -0
- calendaring_jmap/_config.py +91 -0
- calendaring_jmap/_http.py +100 -0
- calendaring_jmap/_methods/__init__.py +16 -0
- calendaring_jmap/_methods/calendar.py +71 -0
- calendaring_jmap/_methods/event.py +271 -0
- calendaring_jmap/_methods/task.py +162 -0
- calendaring_jmap/_version.py +24 -0
- calendaring_jmap/async_client.py +474 -0
- calendaring_jmap/client.py +810 -0
- calendaring_jmap/constants.py +19 -0
- calendaring_jmap/convert/__init__.py +15 -0
- calendaring_jmap/convert/_fixup.py +135 -0
- calendaring_jmap/convert/_patch.py +36 -0
- calendaring_jmap/convert/_utils.py +144 -0
- calendaring_jmap/convert/ical_to_jscal.py +519 -0
- calendaring_jmap/convert/jscal_to_ical.py +465 -0
- calendaring_jmap/error.py +96 -0
- calendaring_jmap/objects/__init__.py +2 -0
- calendaring_jmap/objects/calendar.py +258 -0
- calendaring_jmap/objects/calendar_object.py +110 -0
- calendaring_jmap/py.typed +0 -0
- calendaring_jmap/session.py +156 -0
- calendaring_jmap/tests/__init__.py +2 -0
- calendaring_jmap/tests/test_jmap_integration.py +356 -0
- calendaring_jmap/tests/test_jmap_unit.py +4466 -0
- calendaring_jmap-1.0.0.dist-info/METADATA +114 -0
- calendaring_jmap-1.0.0.dist-info/RECORD +30 -0
- calendaring_jmap-1.0.0.dist-info/WHEEL +4 -0
- calendaring_jmap-1.0.0.dist-info/licenses/LICENSE +235 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 calendaring-jmap contributors
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Calendar operations over JMAP (RFC 8620 + JMAP Calendars).
|
|
6
|
+
|
|
7
|
+
Provides synchronous and asynchronous JMAP clients for calendar listing,
|
|
8
|
+
event CRUD, incremental sync, and task CRUD.
|
|
9
|
+
|
|
10
|
+
Basic usage::
|
|
11
|
+
|
|
12
|
+
from calendaring_jmap import get_jmap_client
|
|
13
|
+
|
|
14
|
+
client = get_jmap_client(
|
|
15
|
+
url="https://jmap.example.com/.well-known/jmap",
|
|
16
|
+
username="alice",
|
|
17
|
+
password="secret",
|
|
18
|
+
)
|
|
19
|
+
calendars = client.get_calendars()
|
|
20
|
+
|
|
21
|
+
Async usage::
|
|
22
|
+
|
|
23
|
+
from calendaring_jmap import get_async_jmap_client
|
|
24
|
+
|
|
25
|
+
async with get_async_jmap_client(
|
|
26
|
+
url="https://jmap.example.com/.well-known/jmap",
|
|
27
|
+
username="alice",
|
|
28
|
+
password="secret",
|
|
29
|
+
) as client:
|
|
30
|
+
calendars = await client.get_calendars()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from calendaring_jmap._config import get_connection_params
|
|
34
|
+
from calendaring_jmap._version import __version__
|
|
35
|
+
from calendaring_jmap.async_client import AsyncJMAPClient
|
|
36
|
+
from calendaring_jmap.client import JMAPClient
|
|
37
|
+
from calendaring_jmap.error import (
|
|
38
|
+
JMAPAuthError,
|
|
39
|
+
JMAPCapabilityError,
|
|
40
|
+
JMAPError,
|
|
41
|
+
JMAPMethodError,
|
|
42
|
+
)
|
|
43
|
+
from calendaring_jmap.objects.calendar import JMAPCalendar
|
|
44
|
+
from calendaring_jmap.objects.calendar_object import JMAPCalendarObject
|
|
45
|
+
|
|
46
|
+
_JMAP_KEYS = {"url", "username", "password", "auth_type", "timeout"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_jmap_client(**kwargs) -> JMAPClient | None:
|
|
50
|
+
"""Create a :class:`JMAPClient` from configuration.
|
|
51
|
+
|
|
52
|
+
Configuration is read in priority order:
|
|
53
|
+
|
|
54
|
+
1. Explicit keyword arguments (``url``, ``username``, ``password``, …)
|
|
55
|
+
2. Environment variables (``JMAP_URL``, ``JMAP_USERNAME``, …)
|
|
56
|
+
3. A YAML config file (``~/.config/calendaring-jmap/calendar.yaml`` or equivalent)
|
|
57
|
+
|
|
58
|
+
Returns ``None`` if no configuration is found, rather than raising.
|
|
59
|
+
|
|
60
|
+
Example::
|
|
61
|
+
|
|
62
|
+
client = get_jmap_client(url="https://jmap.example.com/.well-known/jmap",
|
|
63
|
+
username="alice", password="secret")
|
|
64
|
+
"""
|
|
65
|
+
conn_params = get_connection_params(**kwargs)
|
|
66
|
+
if conn_params is None:
|
|
67
|
+
return None
|
|
68
|
+
return JMAPClient(**{k: v for k, v in conn_params.items() if k in _JMAP_KEYS})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_async_jmap_client(**kwargs) -> AsyncJMAPClient | None:
|
|
72
|
+
"""Create an :class:`AsyncJMAPClient` from configuration.
|
|
73
|
+
|
|
74
|
+
Accepts the same arguments and reads configuration from the same sources
|
|
75
|
+
as :func:`get_jmap_client`. Returns ``None`` if no configuration is found.
|
|
76
|
+
|
|
77
|
+
Example::
|
|
78
|
+
|
|
79
|
+
async with get_async_jmap_client(
|
|
80
|
+
url="https://jmap.example.com/.well-known/jmap",
|
|
81
|
+
username="alice", password="secret"
|
|
82
|
+
) as client:
|
|
83
|
+
calendars = await client.get_calendars()
|
|
84
|
+
"""
|
|
85
|
+
conn_params = get_connection_params(**kwargs)
|
|
86
|
+
if conn_params is None:
|
|
87
|
+
return None
|
|
88
|
+
return AsyncJMAPClient(**{k: v for k, v in conn_params.items() if k in _JMAP_KEYS})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"AsyncJMAPClient",
|
|
93
|
+
"JMAPAuthError",
|
|
94
|
+
"JMAPCalendar",
|
|
95
|
+
"JMAPCalendarObject",
|
|
96
|
+
"JMAPCapabilityError",
|
|
97
|
+
"JMAPClient",
|
|
98
|
+
"JMAPError",
|
|
99
|
+
"JMAPMethodError",
|
|
100
|
+
"__version__",
|
|
101
|
+
"get_async_jmap_client",
|
|
102
|
+
"get_jmap_client",
|
|
103
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 calendaring-jmap contributors
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""Standalone configuration loading for calendaring-jmap.
|
|
5
|
+
|
|
6
|
+
Resolves connection parameters in priority order:
|
|
7
|
+
|
|
8
|
+
1. Explicit keyword arguments
|
|
9
|
+
2. Environment variables (``JMAP_URL``, ``JMAP_USERNAME``, ``JMAP_PASSWORD``,
|
|
10
|
+
``JMAP_AUTH_TYPE``, ``JMAP_TIMEOUT``)
|
|
11
|
+
3. A YAML config file (``JMAP_CONFIG_FILE`` env var, or
|
|
12
|
+
``~/.config/calendaring-jmap/calendar.yaml``)
|
|
13
|
+
|
|
14
|
+
Returns ``None`` if none of the three sources yields a URL, rather than
|
|
15
|
+
raising. Callers decide whether "no configuration" is an error.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_CONN_KEYS = {"url", "username", "password", "auth_type", "timeout"}
|
|
25
|
+
_DEFAULT_CONFIG_PATH = Path.home() / ".config" / "calendaring-jmap" / "calendar.yaml"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_ENV_VARS = {
|
|
29
|
+
"url": "JMAP_URL",
|
|
30
|
+
"username": "JMAP_USERNAME",
|
|
31
|
+
"password": "JMAP_PASSWORD",
|
|
32
|
+
"auth_type": "JMAP_AUTH_TYPE",
|
|
33
|
+
"timeout": "JMAP_TIMEOUT",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _from_environment() -> dict[str, Any]:
|
|
38
|
+
env: dict[str, Any] = {}
|
|
39
|
+
for key, var in _ENV_VARS.items():
|
|
40
|
+
value = os.environ.get(var)
|
|
41
|
+
if value:
|
|
42
|
+
env[key] = int(value) if key == "timeout" else value
|
|
43
|
+
return env
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _from_config_file(config_file: str | Path | None) -> dict[str, Any]:
|
|
47
|
+
path = Path(config_file) if config_file else _DEFAULT_CONFIG_PATH
|
|
48
|
+
if not path.is_file():
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
import yaml
|
|
52
|
+
|
|
53
|
+
with path.open("rb") as f:
|
|
54
|
+
data = yaml.safe_load(f) or {}
|
|
55
|
+
if not isinstance(data, dict):
|
|
56
|
+
return {}
|
|
57
|
+
return {k: v for k, v in data.items() if k in _CONN_KEYS}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_connection_params(
|
|
61
|
+
config_file: str | Path | None = None,
|
|
62
|
+
**explicit_params: Any,
|
|
63
|
+
) -> dict[str, Any] | None:
|
|
64
|
+
"""Resolve JMAP connection parameters from kwargs, env vars, or a config file.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
config_file: Explicit path to a YAML config file. Defaults to the
|
|
68
|
+
``JMAP_CONFIG_FILE`` environment variable, then
|
|
69
|
+
``~/.config/calendaring-jmap/calendar.yaml``.
|
|
70
|
+
**explicit_params: ``url``, ``username``, ``password``, ``auth_type``,
|
|
71
|
+
``timeout``. A value of ``None`` is treated as "not supplied",
|
|
72
|
+
not "unset": this lets a thin CLI wrapper pass every option
|
|
73
|
+
through unconditionally without wiping out env vars for the
|
|
74
|
+
ones the user left off.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
A dict of connection parameters, or ``None`` if no source yields a URL.
|
|
78
|
+
"""
|
|
79
|
+
explicit = {k: v for k, v in explicit_params.items() if k in _CONN_KEYS and v is not None}
|
|
80
|
+
if explicit.get("url"):
|
|
81
|
+
return explicit
|
|
82
|
+
|
|
83
|
+
env = _from_environment()
|
|
84
|
+
if env.get("url"):
|
|
85
|
+
return {**env, **explicit}
|
|
86
|
+
|
|
87
|
+
file_config = _from_config_file(config_file or os.environ.get("JMAP_CONFIG_FILE"))
|
|
88
|
+
if file_config.get("url"):
|
|
89
|
+
return {**file_config, **env, **explicit}
|
|
90
|
+
|
|
91
|
+
return None
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 calendaring-jmap contributors
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""HTTP library selection, imported once for the whole package.
|
|
5
|
+
|
|
6
|
+
Everything that needs niquests-or-requests takes it from here rather than
|
|
7
|
+
repeating the fallback. ``requests`` is niquests when niquests is installed:
|
|
8
|
+
the name is the one the API is compatible with, not necessarily the package
|
|
9
|
+
that provides it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
_DOCS_URL = "https://calendaring-jmap.readthedocs.io/"
|
|
15
|
+
|
|
16
|
+
USE_NIQUESTS = False
|
|
17
|
+
USE_REQUESTS = False
|
|
18
|
+
|
|
19
|
+
## niquests' AsyncSession has no requests equivalent, so it's None on the
|
|
20
|
+
## fallback. The async client is the only thing that needs it; it goes
|
|
21
|
+
## through require_async_session() for a clear error instead of a
|
|
22
|
+
## TypeError on None.
|
|
23
|
+
AsyncSession: Any = None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import niquests as requests
|
|
27
|
+
from niquests.auth import AuthBase, HTTPBasicAuth
|
|
28
|
+
|
|
29
|
+
USE_NIQUESTS = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
try:
|
|
32
|
+
import requests # type: ignore[no-redef]
|
|
33
|
+
from requests.auth import ( # type: ignore[assignment]
|
|
34
|
+
AuthBase,
|
|
35
|
+
HTTPBasicAuth,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
USE_REQUESTS = True
|
|
39
|
+
except ImportError as e:
|
|
40
|
+
raise ImportError(
|
|
41
|
+
"calendaring-jmap needs an HTTP library, and none of the supported "
|
|
42
|
+
"ones is installed (tried: niquests, requests). Install one; "
|
|
43
|
+
"`pip install niquests` is the recommended choice. "
|
|
44
|
+
f"See {_DOCS_URL} for details."
|
|
45
|
+
) from e
|
|
46
|
+
|
|
47
|
+
if USE_NIQUESTS:
|
|
48
|
+
## Deliberately its own try: an ImportError here must not fall through to
|
|
49
|
+
## the requests branch and flip USE_NIQUESTS off on an install that does
|
|
50
|
+
## have niquests. Only the async client needs this.
|
|
51
|
+
try:
|
|
52
|
+
from niquests import AsyncSession
|
|
53
|
+
except ImportError:
|
|
54
|
+
## Old niquests without AsyncSession: leave it None, and let
|
|
55
|
+
## require_async_session() explain it if anything asks for it.
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def require_async_session() -> Any:
|
|
60
|
+
"""Return niquests' ``AsyncSession``, or explain why there isn't one.
|
|
61
|
+
|
|
62
|
+
The async client is built on it and has no fallback.
|
|
63
|
+
"""
|
|
64
|
+
if AsyncSession is None:
|
|
65
|
+
reason = "an old niquests install without AsyncSession" if USE_NIQUESTS else "not installed"
|
|
66
|
+
raise ImportError(
|
|
67
|
+
f"The async JMAP client requires niquests with AsyncSession support ({reason}). "
|
|
68
|
+
"Unlike the sync client it has no fallback to another HTTP library. "
|
|
69
|
+
f"Install or upgrade it with `pip install -U niquests`. See {_DOCS_URL} for details."
|
|
70
|
+
)
|
|
71
|
+
return AsyncSession
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class HTTPBearerAuth(AuthBase):
|
|
75
|
+
"""Bearer token authentication for niquests/requests."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, password: str) -> None:
|
|
78
|
+
self.password = password
|
|
79
|
+
|
|
80
|
+
def __eq__(self, other: object) -> bool:
|
|
81
|
+
return self.password == getattr(other, "password", None)
|
|
82
|
+
|
|
83
|
+
def __ne__(self, other: object) -> bool:
|
|
84
|
+
return not self == other
|
|
85
|
+
|
|
86
|
+
def __call__(self, r):
|
|
87
|
+
r.headers["Authorization"] = f"Bearer {self.password}"
|
|
88
|
+
return r
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"AsyncSession",
|
|
93
|
+
"AuthBase",
|
|
94
|
+
"HTTPBasicAuth",
|
|
95
|
+
"HTTPBearerAuth",
|
|
96
|
+
"USE_NIQUESTS",
|
|
97
|
+
"USE_REQUESTS",
|
|
98
|
+
"requests",
|
|
99
|
+
"require_async_session",
|
|
100
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 calendaring-jmap contributors
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def parse_set_response(response_args: dict) -> tuple[dict, dict, list[str], dict, dict, dict]:
|
|
6
|
+
"""Parse the arguments dict from any JMAP ``*/set`` method response.
|
|
7
|
+
|
|
8
|
+
Returns a 6-tuple ``(created, updated, destroyed, not_created, not_updated, not_destroyed)``.
|
|
9
|
+
"""
|
|
10
|
+
created: dict = response_args.get("created") or {}
|
|
11
|
+
updated: dict = response_args.get("updated") or {}
|
|
12
|
+
destroyed: list[str] = response_args.get("destroyed") or []
|
|
13
|
+
not_created: dict = response_args.get("notCreated") or {}
|
|
14
|
+
not_updated: dict = response_args.get("notUpdated") or {}
|
|
15
|
+
not_destroyed: dict = response_args.get("notDestroyed") or {}
|
|
16
|
+
return created, updated, destroyed, not_created, not_updated, not_destroyed
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 calendaring-jmap contributors
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
JMAP Calendar method builders and response parsers.
|
|
6
|
+
|
|
7
|
+
These are pure functions — no HTTP, no state. They build the request
|
|
8
|
+
tuples that go into a ``methodCalls`` list, and parse the corresponding
|
|
9
|
+
``methodResponses`` entries.
|
|
10
|
+
|
|
11
|
+
Method shapes follow RFC 8620 §3.3 (get), §3.4 (changes), §3.5 (set); Calendar-specific
|
|
12
|
+
properties are defined in the JMAP Calendars specification.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from calendaring_jmap.objects.calendar import JMAPCalendar
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_calendar_get(
|
|
21
|
+
account_id: str,
|
|
22
|
+
ids: list[str] | None = None,
|
|
23
|
+
properties: list[str] | None = None,
|
|
24
|
+
) -> tuple:
|
|
25
|
+
"""Build a ``Calendar/get`` method call tuple.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
account_id: The JMAP accountId to query.
|
|
29
|
+
ids: List of calendar IDs to fetch, or ``None`` to fetch all.
|
|
30
|
+
properties: List of property names to return, or ``None`` for all.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A 3-tuple ``("Calendar/get", arguments_dict, call_id)`` suitable
|
|
34
|
+
for inclusion in a ``methodCalls`` list.
|
|
35
|
+
"""
|
|
36
|
+
args: dict = {"accountId": account_id, "ids": ids}
|
|
37
|
+
if properties is not None:
|
|
38
|
+
args["properties"] = properties
|
|
39
|
+
return ("Calendar/get", args, "cal-get-0")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_calendar_get(response_args: dict) -> list[JMAPCalendar]:
|
|
43
|
+
"""Parse the arguments dict from a ``Calendar/get`` method response.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
response_args: The second element of a ``methodResponses`` entry
|
|
47
|
+
whose method name is ``"Calendar/get"``.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of :class:`~calendaring_jmap.objects.calendar.JMAPCalendar` objects.
|
|
51
|
+
Returns an empty list if ``"list"`` is absent or empty.
|
|
52
|
+
"""
|
|
53
|
+
return [JMAPCalendar.from_jmap(item) for item in response_args.get("list", [])]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_calendar_changes(account_id: str, since_state: str) -> tuple:
|
|
57
|
+
"""Build a ``Calendar/changes`` method call tuple.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
account_id: The JMAP accountId to query.
|
|
61
|
+
since_state: The ``state`` string from a previous ``Calendar/get``
|
|
62
|
+
or ``Calendar/changes`` response.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A 3-tuple ``("Calendar/changes", arguments_dict, call_id)``.
|
|
66
|
+
"""
|
|
67
|
+
return (
|
|
68
|
+
"Calendar/changes",
|
|
69
|
+
{"accountId": account_id, "sinceState": since_state},
|
|
70
|
+
"cal-changes-0",
|
|
71
|
+
)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 calendaring-jmap contributors
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
JMAP CalendarEvent method builders and response parsers.
|
|
6
|
+
|
|
7
|
+
These are pure functions — no HTTP, no state. They build the request
|
|
8
|
+
tuples that go into a ``methodCalls`` list, and parse the corresponding
|
|
9
|
+
``methodResponses`` entries.
|
|
10
|
+
|
|
11
|
+
Method shapes follow RFC 8620 §3.3 (get), §3.4 (changes), §3.5 (set),
|
|
12
|
+
§3.6 (query), §3.7 (queryChanges); CalendarEvent-specific properties are
|
|
13
|
+
defined in the JMAP Calendars specification.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from calendaring_jmap._methods import parse_set_response
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_event_get(
|
|
22
|
+
account_id: str,
|
|
23
|
+
ids: list[str] | None = None,
|
|
24
|
+
properties: list[str] | None = None,
|
|
25
|
+
) -> tuple:
|
|
26
|
+
"""Build a ``CalendarEvent/get`` method call tuple.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
account_id: The JMAP accountId to query.
|
|
30
|
+
ids: List of event IDs to fetch, or ``None`` to fetch all.
|
|
31
|
+
properties: List of property names to return, or ``None`` for all.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A 3-tuple ``("CalendarEvent/get", arguments_dict, call_id)`` suitable
|
|
35
|
+
for inclusion in a ``methodCalls`` list.
|
|
36
|
+
"""
|
|
37
|
+
args: dict = {"accountId": account_id, "ids": ids}
|
|
38
|
+
if properties is not None:
|
|
39
|
+
args["properties"] = properties
|
|
40
|
+
return ("CalendarEvent/get", args, "ev-get-0")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_event_get(response_args: dict) -> list[dict]:
|
|
44
|
+
"""Parse the arguments dict from a ``CalendarEvent/get`` method response.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
response_args: The second element of a ``methodResponses`` entry
|
|
48
|
+
whose method name is ``"CalendarEvent/get"``.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of raw JSCalendar dicts as returned by the server.
|
|
52
|
+
Returns an empty list if ``"list"`` is absent or empty.
|
|
53
|
+
"""
|
|
54
|
+
return list(response_args.get("list", []))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_event_changes(
|
|
58
|
+
account_id: str,
|
|
59
|
+
since_state: str,
|
|
60
|
+
max_changes: int | None = None,
|
|
61
|
+
) -> tuple:
|
|
62
|
+
"""Build a ``CalendarEvent/changes`` method call tuple.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
account_id: The JMAP accountId to query.
|
|
66
|
+
since_state: The ``state`` string from a previous ``CalendarEvent/get``
|
|
67
|
+
or ``CalendarEvent/changes`` response.
|
|
68
|
+
max_changes: Optional upper bound on the number of changes returned.
|
|
69
|
+
The server may return fewer.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A 3-tuple ``("CalendarEvent/changes", arguments_dict, call_id)``.
|
|
73
|
+
"""
|
|
74
|
+
args: dict = {"accountId": account_id, "sinceState": since_state}
|
|
75
|
+
if max_changes is not None:
|
|
76
|
+
args["maxChanges"] = max_changes
|
|
77
|
+
return ("CalendarEvent/changes", args, "ev-changes-0")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parse_event_changes(
|
|
81
|
+
response_args: dict,
|
|
82
|
+
) -> tuple[str, str, bool, list[str], list[str], list[str]]:
|
|
83
|
+
"""Parse the arguments dict from a ``CalendarEvent/changes`` response.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
response_args: The second element of a ``methodResponses`` entry
|
|
87
|
+
whose method name is ``"CalendarEvent/changes"``.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A 6-tuple ``(old_state, new_state, has_more_changes, created, updated, destroyed)``:
|
|
91
|
+
|
|
92
|
+
- ``old_state``: Echo of the ``sinceState`` argument.
|
|
93
|
+
- ``new_state``: State string to store as the next sync token.
|
|
94
|
+
- ``has_more_changes``: True if the server capped the response.
|
|
95
|
+
- ``created``: IDs of newly created events.
|
|
96
|
+
- ``updated``: IDs of modified events.
|
|
97
|
+
- ``destroyed``: IDs of deleted events.
|
|
98
|
+
"""
|
|
99
|
+
return (
|
|
100
|
+
response_args.get("oldState", ""),
|
|
101
|
+
response_args.get("newState", ""),
|
|
102
|
+
response_args.get("hasMoreChanges", False),
|
|
103
|
+
response_args.get("created") or [],
|
|
104
|
+
response_args.get("updated") or [],
|
|
105
|
+
response_args.get("destroyed") or [],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_event_query(
|
|
110
|
+
account_id: str,
|
|
111
|
+
filter_condition: dict | None = None,
|
|
112
|
+
sort: list[dict] | None = None,
|
|
113
|
+
position: int = 0,
|
|
114
|
+
limit: int | None = None,
|
|
115
|
+
) -> tuple:
|
|
116
|
+
"""Build a ``CalendarEvent/query`` method call tuple.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
account_id: The JMAP accountId to query.
|
|
120
|
+
filter_condition: A ``FilterCondition`` or ``FilterOperator`` dict, e.g.
|
|
121
|
+
``{"after": "2024-01-01T00:00:00Z", "before": "2024-12-31T23:59:59Z"}``.
|
|
122
|
+
``None`` means no filter (return all events).
|
|
123
|
+
sort: List of ``Comparator`` dicts, e.g.
|
|
124
|
+
``[{"property": "start", "isAscending": True}]``.
|
|
125
|
+
``None`` means server default ordering.
|
|
126
|
+
position: Zero-based index of the first result to return.
|
|
127
|
+
limit: Maximum number of IDs to return. ``None`` means no limit.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
A 3-tuple ``("CalendarEvent/query", arguments_dict, call_id)``.
|
|
131
|
+
"""
|
|
132
|
+
args: dict = {"accountId": account_id, "position": position}
|
|
133
|
+
if filter_condition is not None:
|
|
134
|
+
args["filter"] = filter_condition
|
|
135
|
+
if sort is not None:
|
|
136
|
+
args["sort"] = sort
|
|
137
|
+
if limit is not None:
|
|
138
|
+
args["limit"] = limit
|
|
139
|
+
return ("CalendarEvent/query", args, "ev-query-0")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_event_query(response_args: dict) -> tuple[list[str], str, int]:
|
|
143
|
+
"""Parse the arguments dict from a ``CalendarEvent/query`` response.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
response_args: The second element of a ``methodResponses`` entry
|
|
147
|
+
whose method name is ``"CalendarEvent/query"``.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A 3-tuple ``(ids, query_state, total)``:
|
|
151
|
+
|
|
152
|
+
- ``ids``: Ordered list of matching event IDs.
|
|
153
|
+
- ``query_state``: Opaque state string for use with
|
|
154
|
+
``CalendarEvent/queryChanges``.
|
|
155
|
+
- ``total``: Total number of matching events (may exceed ``len(ids)``
|
|
156
|
+
when a limit was applied).
|
|
157
|
+
"""
|
|
158
|
+
ids: list[str] = response_args.get("ids", [])
|
|
159
|
+
query_state: str = response_args.get("queryState", "")
|
|
160
|
+
total: int = response_args.get("total", len(ids))
|
|
161
|
+
return ids, query_state, total
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def build_event_query_changes(
|
|
165
|
+
account_id: str,
|
|
166
|
+
since_query_state: str,
|
|
167
|
+
filter_condition: dict | None = None,
|
|
168
|
+
sort: list[dict] | None = None,
|
|
169
|
+
max_changes: int | None = None,
|
|
170
|
+
) -> tuple:
|
|
171
|
+
"""Build a ``CalendarEvent/queryChanges`` method call tuple.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
account_id: The JMAP accountId to query.
|
|
175
|
+
since_query_state: The ``queryState`` string from a previous
|
|
176
|
+
``CalendarEvent/query`` or ``CalendarEvent/queryChanges`` response.
|
|
177
|
+
filter_condition: Same filter as the original ``CalendarEvent/query`` call.
|
|
178
|
+
sort: Same sort as the original ``CalendarEvent/query`` call.
|
|
179
|
+
max_changes: Optional upper bound on the number of changes returned.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A 3-tuple ``("CalendarEvent/queryChanges", arguments_dict, call_id)``.
|
|
183
|
+
"""
|
|
184
|
+
args: dict = {"accountId": account_id, "sinceQueryState": since_query_state}
|
|
185
|
+
if filter_condition is not None:
|
|
186
|
+
args["filter"] = filter_condition
|
|
187
|
+
if sort is not None:
|
|
188
|
+
args["sort"] = sort
|
|
189
|
+
if max_changes is not None:
|
|
190
|
+
args["maxChanges"] = max_changes
|
|
191
|
+
return ("CalendarEvent/queryChanges", args, "ev-qchanges-0")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def build_event_set_create(
|
|
195
|
+
account_id: str,
|
|
196
|
+
events: dict[str, dict],
|
|
197
|
+
) -> tuple:
|
|
198
|
+
"""Build a ``CalendarEvent/set`` method call for creating events.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
account_id: The JMAP accountId.
|
|
202
|
+
events: Map of client-assigned creation ID → JSCalendar dict.
|
|
203
|
+
The creation IDs are ephemeral — they are used to correlate
|
|
204
|
+
server responses with individual creation requests within the
|
|
205
|
+
same batch call.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
A 3-tuple ``("CalendarEvent/set", arguments_dict, call_id)``.
|
|
209
|
+
"""
|
|
210
|
+
return (
|
|
211
|
+
"CalendarEvent/set",
|
|
212
|
+
{
|
|
213
|
+
"accountId": account_id,
|
|
214
|
+
"create": dict(events),
|
|
215
|
+
},
|
|
216
|
+
"ev-set-create-0",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def build_event_set_update(
|
|
221
|
+
account_id: str,
|
|
222
|
+
updates: dict[str, dict],
|
|
223
|
+
) -> tuple:
|
|
224
|
+
"""Build a ``CalendarEvent/set`` method call for updating events.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
account_id: The JMAP accountId.
|
|
228
|
+
updates: Map of event ID → partial patch dict. Keys are property
|
|
229
|
+
names (or JSON Pointer paths for nested properties); values are
|
|
230
|
+
the new values. Use ``None`` as a value to reset a property to
|
|
231
|
+
its server default.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
A 3-tuple ``("CalendarEvent/set", arguments_dict, call_id)``.
|
|
235
|
+
"""
|
|
236
|
+
return (
|
|
237
|
+
"CalendarEvent/set",
|
|
238
|
+
{"accountId": account_id, "update": updates},
|
|
239
|
+
"ev-set-update-0",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def build_event_set_destroy(
|
|
244
|
+
account_id: str,
|
|
245
|
+
ids: list[str],
|
|
246
|
+
) -> tuple:
|
|
247
|
+
"""Build a ``CalendarEvent/set`` method call for destroying events.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
account_id: The JMAP accountId.
|
|
251
|
+
ids: List of event IDs to destroy.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
A 3-tuple ``("CalendarEvent/set", arguments_dict, call_id)``.
|
|
255
|
+
"""
|
|
256
|
+
return (
|
|
257
|
+
"CalendarEvent/set",
|
|
258
|
+
{"accountId": account_id, "destroy": ids},
|
|
259
|
+
"ev-set-destroy-0",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def parse_event_set(
|
|
264
|
+
response_args: dict,
|
|
265
|
+
) -> tuple[dict, dict, list[str], dict, dict, dict]:
|
|
266
|
+
"""Parse the arguments dict from a ``CalendarEvent/set`` method response.
|
|
267
|
+
|
|
268
|
+
Returns a 6-tuple ``(created, updated, destroyed, not_created, not_updated, not_destroyed)``.
|
|
269
|
+
See :func:`calendaring_jmap._methods.parse_set_response` for field semantics.
|
|
270
|
+
"""
|
|
271
|
+
return parse_set_response(response_args)
|