loomal-sdk 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Loomal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: loomal-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Loomal API — identity infrastructure for AI agents
5
+ Author: Loomal
6
+ License: MIT
7
+ Project-URL: Homepage, https://loomal.ai
8
+ Project-URL: Documentation, https://docs.loomal.ai
9
+ Project-URL: Repository, https://github.com/loomal-ai/loomal-python
10
+ Project-URL: Issues, https://github.com/loomal-ai/loomal-python/issues
11
+ Keywords: loomal,ai,agent,email,mcp,did,vault,sdk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: httpx>=0.27
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
32
+ Requires-Dist: respx>=0.21; extra == "dev"
33
+ Requires-Dist: build; extra == "dev"
34
+ Requires-Dist: twine; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # Loomal Python SDK
38
+
39
+ The official Python SDK for the [Loomal API](https://loomal.ai) -- identity infrastructure for AI agents.
40
+
41
+ [![PyPI version](https://img.shields.io/pypi/v/loomal-sdk.svg)](https://pypi.org/project/loomal-sdk/)
42
+ [![Python 3.9+](https://img.shields.io/pypi/pyversions/loomal-sdk.svg)](https://pypi.org/project/loomal-sdk/)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install loomal-sdk
49
+ ```
50
+
51
+ > The distribution is `loomal-sdk` on PyPI, but the import name is `loomal`.
52
+
53
+ ## Quick start
54
+
55
+ ```python
56
+ from loomal import Loomal
57
+
58
+ client = Loomal(api_key="loid-...")
59
+
60
+ me = client.identity.whoami()
61
+ print(me.email)
62
+
63
+ client.mail.send(
64
+ to=["colleague@example.com"],
65
+ subject="Hello from my agent",
66
+ text="Sent via the Loomal Python SDK.",
67
+ )
68
+ ```
69
+
70
+ ## Async usage
71
+
72
+ ```python
73
+ from loomal import AsyncLoomal
74
+
75
+ async with AsyncLoomal(api_key="loid-...") as client:
76
+ me = await client.identity.whoami()
77
+ await client.mail.send(
78
+ to=["colleague@example.com"],
79
+ subject="Hello",
80
+ text="Sent asynchronously.",
81
+ )
82
+ ```
83
+
84
+ ## Authentication
85
+
86
+ Pass your API key directly, or set the `LOOMAL_API_KEY` environment variable:
87
+
88
+ ```python
89
+ # Explicit
90
+ client = Loomal(api_key="loid-...")
91
+
92
+ # From environment
93
+ import os
94
+ os.environ["LOOMAL_API_KEY"] = "loid-..."
95
+ client = Loomal()
96
+ ```
97
+
98
+ Both `Loomal` and `AsyncLoomal` support context managers for automatic resource cleanup:
99
+
100
+ ```python
101
+ with Loomal() as client:
102
+ me = client.identity.whoami()
103
+ ```
104
+
105
+ ## Usage
106
+
107
+ ### Identity
108
+
109
+ ```python
110
+ me = client.identity.whoami()
111
+ print(me.email, me.display_name)
112
+ ```
113
+
114
+ ### More resources
115
+
116
+ The SDK also exposes `client.mail`, `client.calendar`, `client.vault`, `client.logs`, and `client.did`. See the full reference at **[docs.loomal.ai](https://docs.loomal.ai)** for request/response shapes, pagination, and end-to-end examples.
117
+
118
+ ## Error handling
119
+
120
+ All API errors raise `LoomalError` with structured fields:
121
+
122
+ ```python
123
+ from loomal import LoomalError
124
+
125
+ try:
126
+ client.mail.send(to=["a@b.com"], subject="Hi", text="Hello")
127
+ except LoomalError as e:
128
+ print(e.status) # HTTP status code
129
+ print(e.code) # Error code string
130
+ print(e.message) # Human-readable message
131
+ ```
132
+
133
+ ## Types
134
+
135
+ The SDK returns typed dataclasses, not raw dictionaries. API responses are automatically converted from camelCase to snake_case.
136
+
137
+ | Type | Description |
138
+ |------|-------------|
139
+ | `IdentityResponse` | Agent identity details |
140
+ | `MessageResponse` | Email message |
141
+ | `ThreadResponse` | Thread summary |
142
+ | `ThreadDetailResponse` | Thread with messages |
143
+ | `CredentialMetadata` | Vault credential metadata |
144
+ | `CredentialWithData` | Credential with decrypted data |
145
+ | `ActivityLog` | Single activity log entry |
146
+ | `LogsStats` | Aggregated log statistics |
147
+ | `TotpResponse` | Generated TOTP code |
148
+ | `DidDocument` | DID document |
149
+
150
+ > **Note:** The `from` field in message responses is exposed as `from_addrs` since `from` is a reserved keyword in Python.
151
+
152
+ ## Requirements
153
+
154
+ - Python 3.9+
155
+ - [`httpx`](https://www.python-httpx.org/) (installed automatically)
156
+
157
+ ## Links
158
+
159
+ - [Documentation](https://docs.loomal.ai)
160
+ - [Console](https://console.loomal.ai)
161
+ - [Website](https://loomal.ai)
162
+ - [PyPI](https://pypi.org/project/loomal-sdk/)
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,130 @@
1
+ # Loomal Python SDK
2
+
3
+ The official Python SDK for the [Loomal API](https://loomal.ai) -- identity infrastructure for AI agents.
4
+
5
+ [![PyPI version](https://img.shields.io/pypi/v/loomal-sdk.svg)](https://pypi.org/project/loomal-sdk/)
6
+ [![Python 3.9+](https://img.shields.io/pypi/pyversions/loomal-sdk.svg)](https://pypi.org/project/loomal-sdk/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install loomal-sdk
13
+ ```
14
+
15
+ > The distribution is `loomal-sdk` on PyPI, but the import name is `loomal`.
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ from loomal import Loomal
21
+
22
+ client = Loomal(api_key="loid-...")
23
+
24
+ me = client.identity.whoami()
25
+ print(me.email)
26
+
27
+ client.mail.send(
28
+ to=["colleague@example.com"],
29
+ subject="Hello from my agent",
30
+ text="Sent via the Loomal Python SDK.",
31
+ )
32
+ ```
33
+
34
+ ## Async usage
35
+
36
+ ```python
37
+ from loomal import AsyncLoomal
38
+
39
+ async with AsyncLoomal(api_key="loid-...") as client:
40
+ me = await client.identity.whoami()
41
+ await client.mail.send(
42
+ to=["colleague@example.com"],
43
+ subject="Hello",
44
+ text="Sent asynchronously.",
45
+ )
46
+ ```
47
+
48
+ ## Authentication
49
+
50
+ Pass your API key directly, or set the `LOOMAL_API_KEY` environment variable:
51
+
52
+ ```python
53
+ # Explicit
54
+ client = Loomal(api_key="loid-...")
55
+
56
+ # From environment
57
+ import os
58
+ os.environ["LOOMAL_API_KEY"] = "loid-..."
59
+ client = Loomal()
60
+ ```
61
+
62
+ Both `Loomal` and `AsyncLoomal` support context managers for automatic resource cleanup:
63
+
64
+ ```python
65
+ with Loomal() as client:
66
+ me = client.identity.whoami()
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ ### Identity
72
+
73
+ ```python
74
+ me = client.identity.whoami()
75
+ print(me.email, me.display_name)
76
+ ```
77
+
78
+ ### More resources
79
+
80
+ The SDK also exposes `client.mail`, `client.calendar`, `client.vault`, `client.logs`, and `client.did`. See the full reference at **[docs.loomal.ai](https://docs.loomal.ai)** for request/response shapes, pagination, and end-to-end examples.
81
+
82
+ ## Error handling
83
+
84
+ All API errors raise `LoomalError` with structured fields:
85
+
86
+ ```python
87
+ from loomal import LoomalError
88
+
89
+ try:
90
+ client.mail.send(to=["a@b.com"], subject="Hi", text="Hello")
91
+ except LoomalError as e:
92
+ print(e.status) # HTTP status code
93
+ print(e.code) # Error code string
94
+ print(e.message) # Human-readable message
95
+ ```
96
+
97
+ ## Types
98
+
99
+ The SDK returns typed dataclasses, not raw dictionaries. API responses are automatically converted from camelCase to snake_case.
100
+
101
+ | Type | Description |
102
+ |------|-------------|
103
+ | `IdentityResponse` | Agent identity details |
104
+ | `MessageResponse` | Email message |
105
+ | `ThreadResponse` | Thread summary |
106
+ | `ThreadDetailResponse` | Thread with messages |
107
+ | `CredentialMetadata` | Vault credential metadata |
108
+ | `CredentialWithData` | Credential with decrypted data |
109
+ | `ActivityLog` | Single activity log entry |
110
+ | `LogsStats` | Aggregated log statistics |
111
+ | `TotpResponse` | Generated TOTP code |
112
+ | `DidDocument` | DID document |
113
+
114
+ > **Note:** The `from` field in message responses is exposed as `from_addrs` since `from` is a reserved keyword in Python.
115
+
116
+ ## Requirements
117
+
118
+ - Python 3.9+
119
+ - [`httpx`](https://www.python-httpx.org/) (installed automatically)
120
+
121
+ ## Links
122
+
123
+ - [Documentation](https://docs.loomal.ai)
124
+ - [Console](https://console.loomal.ai)
125
+ - [Website](https://loomal.ai)
126
+ - [PyPI](https://pypi.org/project/loomal-sdk/)
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,23 @@
1
+ from loomal.client import Loomal, AsyncLoomal
2
+ from loomal.platform_client import LoomalPlatform, AsyncLoomalPlatform
3
+ from loomal.types import (
4
+ MessageResponse, ThreadResponse, ThreadDetailResponse,
5
+ CredentialMetadata, CredentialWithData, IdentityResponse,
6
+ IdentitySummary, IdentityDetail, CreateIdentityResponse, RotateKeyResponse,
7
+ CalendarEvent,
8
+ ActivityLog, LogsStats, TotpResponse, DidDocument,
9
+ )
10
+ from loomal._errors import LoomalError
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = [
15
+ "Loomal", "AsyncLoomal",
16
+ "LoomalPlatform", "AsyncLoomalPlatform",
17
+ "LoomalError",
18
+ "MessageResponse", "ThreadResponse", "ThreadDetailResponse",
19
+ "CredentialMetadata", "CredentialWithData", "IdentityResponse",
20
+ "IdentitySummary", "IdentityDetail", "CreateIdentityResponse", "RotateKeyResponse",
21
+ "CalendarEvent",
22
+ "ActivityLog", "LogsStats", "TotpResponse", "DidDocument",
23
+ ]
@@ -0,0 +1,10 @@
1
+ class LoomalError(Exception):
2
+ """Raised when the Loomal API returns a non-2xx response."""
3
+
4
+ def __init__(self, status: int, code: str, message: str) -> None:
5
+ super().__init__(message)
6
+ self.status = status
7
+ self.code = code
8
+
9
+ def __repr__(self) -> str:
10
+ return f"LoomalError(status={self.status}, code={self.code!r}, message={self.args[0]!r})"
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ import httpx
6
+
7
+ from loomal._errors import LoomalError
8
+
9
+ DEFAULT_BASE_URL = "https://api.loomal.ai"
10
+ DEFAULT_TIMEOUT = 30.0
11
+
12
+
13
+ def _build_headers(api_key: str) -> dict[str, str]:
14
+ return {
15
+ "Authorization": f"Bearer {api_key}",
16
+ "Content-Type": "application/json",
17
+ }
18
+
19
+
20
+ def _handle_response(response: httpx.Response) -> Any:
21
+ if response.status_code == 204:
22
+ return None
23
+
24
+ data = response.json() if response.content else {}
25
+
26
+ if not response.is_success:
27
+ raise LoomalError(
28
+ status=response.status_code,
29
+ code=data.get("error", "unknown_error"),
30
+ message=data.get("message", f"Request failed with status {response.status_code}"),
31
+ )
32
+
33
+ return data
34
+
35
+
36
+ class SyncHttpClient:
37
+ def __init__(self, base_url: str, api_key: str, timeout: float = DEFAULT_TIMEOUT) -> None:
38
+ self._client = httpx.Client(
39
+ base_url=base_url.rstrip("/"),
40
+ headers=_build_headers(api_key),
41
+ timeout=timeout,
42
+ )
43
+
44
+ def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
45
+ return _handle_response(self._client.get(path, params=params))
46
+
47
+ def post(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
48
+ return _handle_response(self._client.post(path, json=json))
49
+
50
+ def put(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
51
+ return _handle_response(self._client.put(path, json=json))
52
+
53
+ def patch(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
54
+ return _handle_response(self._client.patch(path, json=json))
55
+
56
+ def delete(self, path: str) -> Any:
57
+ return _handle_response(self._client.delete(path))
58
+
59
+ def close(self) -> None:
60
+ self._client.close()
61
+
62
+
63
+ class AsyncHttpClient:
64
+ def __init__(self, base_url: str, api_key: str, timeout: float = DEFAULT_TIMEOUT) -> None:
65
+ self._client = httpx.AsyncClient(
66
+ base_url=base_url.rstrip("/"),
67
+ headers=_build_headers(api_key),
68
+ timeout=timeout,
69
+ )
70
+
71
+ async def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
72
+ return _handle_response(await self._client.get(path, params=params))
73
+
74
+ async def post(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
75
+ return _handle_response(await self._client.post(path, json=json))
76
+
77
+ async def put(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
78
+ return _handle_response(await self._client.put(path, json=json))
79
+
80
+ async def patch(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
81
+ return _handle_response(await self._client.patch(path, json=json))
82
+
83
+ async def delete(self, path: str) -> Any:
84
+ return _handle_response(await self._client.delete(path))
85
+
86
+ async def close(self) -> None:
87
+ await self._client.aclose()
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Optional
5
+
6
+ from loomal._http import SyncHttpClient, AsyncHttpClient, DEFAULT_BASE_URL
7
+ from loomal.resources.identity import IdentityResource, AsyncIdentityResource
8
+ from loomal.resources.mail import MailResource, AsyncMailResource
9
+ from loomal.resources.vault import VaultResource, AsyncVaultResource
10
+ from loomal.resources.logs import LogsResource, AsyncLogsResource
11
+ from loomal.resources.did import DidResource, AsyncDidResource
12
+ from loomal.resources.calendar import CalendarResource, AsyncCalendarResource
13
+
14
+
15
+ class Loomal:
16
+ """Synchronous Loomal API client."""
17
+
18
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None,
19
+ timeout: float = 30.0) -> None:
20
+ resolved_key = api_key or os.environ.get("LOOMAL_API_KEY")
21
+ if not resolved_key:
22
+ raise ValueError("API key is required. Pass api_key= or set LOOMAL_API_KEY env var.")
23
+
24
+ http = SyncHttpClient(
25
+ base_url=base_url or os.environ.get("LOOMAL_API_URL", DEFAULT_BASE_URL),
26
+ api_key=resolved_key, timeout=timeout,
27
+ )
28
+ self.identity = IdentityResource(http)
29
+ self.mail = MailResource(http)
30
+ self.vault = VaultResource(http)
31
+ self.logs = LogsResource(http)
32
+ self.did = DidResource(http)
33
+ self.calendar = CalendarResource(http)
34
+ self._http = http
35
+
36
+ def close(self) -> None:
37
+ self._http.close()
38
+
39
+ def __enter__(self):
40
+ return self
41
+
42
+ def __exit__(self, *args):
43
+ self.close()
44
+
45
+
46
+ class AsyncLoomal:
47
+ """Asynchronous Loomal API client."""
48
+
49
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None,
50
+ timeout: float = 30.0) -> None:
51
+ resolved_key = api_key or os.environ.get("LOOMAL_API_KEY")
52
+ if not resolved_key:
53
+ raise ValueError("API key is required. Pass api_key= or set LOOMAL_API_KEY env var.")
54
+
55
+ http = AsyncHttpClient(
56
+ base_url=base_url or os.environ.get("LOOMAL_API_URL", DEFAULT_BASE_URL),
57
+ api_key=resolved_key, timeout=timeout,
58
+ )
59
+ self.identity = AsyncIdentityResource(http)
60
+ self.mail = AsyncMailResource(http)
61
+ self.vault = AsyncVaultResource(http)
62
+ self.logs = AsyncLogsResource(http)
63
+ self.did = AsyncDidResource(http)
64
+ self.calendar = AsyncCalendarResource(http)
65
+ self._http = http
66
+
67
+ async def close(self) -> None:
68
+ await self._http.close()
69
+
70
+ async def __aenter__(self):
71
+ return self
72
+
73
+ async def __aexit__(self, *args):
74
+ await self.close()
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Optional
5
+
6
+ from loomal._http import SyncHttpClient, AsyncHttpClient, DEFAULT_BASE_URL
7
+ from loomal.resources.platform_identities import PlatformIdentitiesResource, AsyncPlatformIdentitiesResource
8
+
9
+
10
+ class LoomalPlatform:
11
+ """Synchronous Loomal Platform client for identity management."""
12
+
13
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None,
14
+ timeout: float = 30.0) -> None:
15
+ resolved_key = api_key or os.environ.get("LOOMAL_PLATFORM_KEY")
16
+ if not resolved_key:
17
+ raise ValueError("Platform key is required. Pass api_key= or set LOOMAL_PLATFORM_KEY env var.")
18
+
19
+ http = SyncHttpClient(
20
+ base_url=base_url or os.environ.get("LOOMAL_API_URL", DEFAULT_BASE_URL),
21
+ api_key=resolved_key, timeout=timeout,
22
+ )
23
+ self.identities = PlatformIdentitiesResource(http)
24
+ self._http = http
25
+
26
+ def close(self) -> None:
27
+ self._http.close()
28
+
29
+ def __enter__(self):
30
+ return self
31
+
32
+ def __exit__(self, *args):
33
+ self.close()
34
+
35
+
36
+ class AsyncLoomalPlatform:
37
+ """Asynchronous Loomal Platform client for identity management."""
38
+
39
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None,
40
+ timeout: float = 30.0) -> None:
41
+ resolved_key = api_key or os.environ.get("LOOMAL_PLATFORM_KEY")
42
+ if not resolved_key:
43
+ raise ValueError("Platform key is required. Pass api_key= or set LOOMAL_PLATFORM_KEY env var.")
44
+
45
+ http = AsyncHttpClient(
46
+ base_url=base_url or os.environ.get("LOOMAL_API_URL", DEFAULT_BASE_URL),
47
+ api_key=resolved_key, timeout=timeout,
48
+ )
49
+ self.identities = AsyncPlatformIdentitiesResource(http)
50
+ self._http = http
51
+
52
+ async def close(self) -> None:
53
+ await self._http.close()
54
+
55
+ async def __aenter__(self):
56
+ return self
57
+
58
+ async def __aexit__(self, *args):
59
+ await self.close()
File without changes
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Optional
3
+ from loomal.types import CalendarEvent
4
+
5
+
6
+ class CalendarResource:
7
+ def __init__(self, http):
8
+ self._http = http
9
+
10
+ def create(self, title: str, start_at: str, end_at: Optional[str] = None,
11
+ is_all_day: bool = False, description: Optional[str] = None,
12
+ location: Optional[str] = None, metadata: Optional[dict[str, Any]] = None) -> CalendarEvent:
13
+ body: dict[str, Any] = {"title": title, "startAt": start_at, "isAllDay": is_all_day}
14
+ if end_at: body["endAt"] = end_at
15
+ if description: body["description"] = description
16
+ if location: body["location"] = location
17
+ if metadata: body["metadata"] = metadata
18
+ return CalendarEvent.from_dict(self._http.post("/v0/calendar", json=body))
19
+
20
+ def list(self, limit: Optional[int] = None, from_date: Optional[str] = None,
21
+ to_date: Optional[str] = None) -> dict[str, Any]:
22
+ params: dict[str, Any] = {}
23
+ if limit is not None: params["limit"] = limit
24
+ if from_date: params["from"] = from_date
25
+ if to_date: params["to"] = to_date
26
+ data = self._http.get("/v0/calendar", params=params or None)
27
+ return {"events": [CalendarEvent.from_dict(e) for e in data.get("events", [])], "count": data["count"]}
28
+
29
+ def get(self, event_id: str) -> CalendarEvent:
30
+ return CalendarEvent.from_dict(self._http.get(f"/v0/calendar/{event_id}"))
31
+
32
+ def update(self, event_id: str, title: Optional[str] = None, start_at: Optional[str] = None,
33
+ end_at: Optional[str] = None, is_all_day: Optional[bool] = None,
34
+ description: Optional[str] = None, location: Optional[str] = None) -> CalendarEvent:
35
+ body: dict[str, Any] = {}
36
+ if title is not None: body["title"] = title
37
+ if start_at is not None: body["startAt"] = start_at
38
+ if end_at is not None: body["endAt"] = end_at
39
+ if is_all_day is not None: body["isAllDay"] = is_all_day
40
+ if description is not None: body["description"] = description
41
+ if location is not None: body["location"] = location
42
+ return CalendarEvent.from_dict(self._http.patch(f"/v0/calendar/{event_id}", json=body))
43
+
44
+ def delete(self, event_id: str) -> None:
45
+ self._http.delete(f"/v0/calendar/{event_id}")
46
+
47
+ def set_public(self, enabled: bool) -> dict[str, bool]:
48
+ return self._http.post("/v0/calendar/public", json={"enabled": enabled})
49
+
50
+
51
+ class AsyncCalendarResource:
52
+ def __init__(self, http):
53
+ self._http = http
54
+
55
+ async def create(self, title: str, start_at: str, end_at: Optional[str] = None,
56
+ is_all_day: bool = False, description: Optional[str] = None,
57
+ location: Optional[str] = None, metadata: Optional[dict[str, Any]] = None) -> CalendarEvent:
58
+ body: dict[str, Any] = {"title": title, "startAt": start_at, "isAllDay": is_all_day}
59
+ if end_at: body["endAt"] = end_at
60
+ if description: body["description"] = description
61
+ if location: body["location"] = location
62
+ if metadata: body["metadata"] = metadata
63
+ return CalendarEvent.from_dict(await self._http.post("/v0/calendar", json=body))
64
+
65
+ async def list(self, limit: Optional[int] = None, from_date: Optional[str] = None,
66
+ to_date: Optional[str] = None) -> dict[str, Any]:
67
+ params: dict[str, Any] = {}
68
+ if limit is not None: params["limit"] = limit
69
+ if from_date: params["from"] = from_date
70
+ if to_date: params["to"] = to_date
71
+ data = await self._http.get("/v0/calendar", params=params or None)
72
+ return {"events": [CalendarEvent.from_dict(e) for e in data.get("events", [])], "count": data["count"]}
73
+
74
+ async def get(self, event_id: str) -> CalendarEvent:
75
+ return CalendarEvent.from_dict(await self._http.get(f"/v0/calendar/{event_id}"))
76
+
77
+ async def update(self, event_id: str, title: Optional[str] = None, start_at: Optional[str] = None,
78
+ end_at: Optional[str] = None, is_all_day: Optional[bool] = None,
79
+ description: Optional[str] = None, location: Optional[str] = None) -> CalendarEvent:
80
+ body: dict[str, Any] = {}
81
+ if title is not None: body["title"] = title
82
+ if start_at is not None: body["startAt"] = start_at
83
+ if end_at is not None: body["endAt"] = end_at
84
+ if is_all_day is not None: body["isAllDay"] = is_all_day
85
+ if description is not None: body["description"] = description
86
+ if location is not None: body["location"] = location
87
+ return CalendarEvent.from_dict(await self._http.patch(f"/v0/calendar/{event_id}", json=body))
88
+
89
+ async def delete(self, event_id: str) -> None:
90
+ await self._http.delete(f"/v0/calendar/{event_id}")
91
+
92
+ async def set_public(self, enabled: bool) -> dict[str, bool]:
93
+ return await self._http.post("/v0/calendar/public", json={"enabled": enabled})