hap-cli 0.5.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.
Files changed (58) hide show
  1. hap_cli/README.md +194 -0
  2. hap_cli/README_CN.md +601 -0
  3. hap_cli/__init__.py +3 -0
  4. hap_cli/commands/__init__.py +1 -0
  5. hap_cli/commands/ai_cmd.py +224 -0
  6. hap_cli/commands/app_cmd.py +308 -0
  7. hap_cli/commands/calendar_cmd.py +138 -0
  8. hap_cli/commands/chat_cmd.py +101 -0
  9. hap_cli/commands/config_cmd.py +169 -0
  10. hap_cli/commands/contact_cmd.py +125 -0
  11. hap_cli/commands/department_cmd.py +168 -0
  12. hap_cli/commands/group_cmd.py +128 -0
  13. hap_cli/commands/instance_cmd.py +310 -0
  14. hap_cli/commands/node_cmd.py +538 -0
  15. hap_cli/commands/optionset_cmd.py +99 -0
  16. hap_cli/commands/page_cmd.py +102 -0
  17. hap_cli/commands/plugin_cmd.py +133 -0
  18. hap_cli/commands/post_cmd.py +155 -0
  19. hap_cli/commands/record_cmd.py +228 -0
  20. hap_cli/commands/role_cmd.py +221 -0
  21. hap_cli/commands/workflow_cmd.py +284 -0
  22. hap_cli/commands/worksheet_cmd.py +342 -0
  23. hap_cli/context.py +43 -0
  24. hap_cli/core/__init__.py +1 -0
  25. hap_cli/core/ai.py +133 -0
  26. hap_cli/core/app.py +307 -0
  27. hap_cli/core/auth.py +219 -0
  28. hap_cli/core/calendar_mod.py +114 -0
  29. hap_cli/core/chat.py +73 -0
  30. hap_cli/core/contact.py +85 -0
  31. hap_cli/core/department.py +131 -0
  32. hap_cli/core/flow_node.py +1001 -0
  33. hap_cli/core/group.py +99 -0
  34. hap_cli/core/instance.py +572 -0
  35. hap_cli/core/optionset.py +112 -0
  36. hap_cli/core/page.py +138 -0
  37. hap_cli/core/plugin.py +87 -0
  38. hap_cli/core/post.py +118 -0
  39. hap_cli/core/record.py +268 -0
  40. hap_cli/core/role.py +227 -0
  41. hap_cli/core/session.py +348 -0
  42. hap_cli/core/workflow.py +556 -0
  43. hap_cli/core/worksheet.py +403 -0
  44. hap_cli/hap_cli.py +105 -0
  45. hap_cli/skills/SKILL.md +383 -0
  46. hap_cli/skills/__init__.py +0 -0
  47. hap_cli/tests/__init__.py +1 -0
  48. hap_cli/tests/test_core.py +1824 -0
  49. hap_cli/tests/test_full_e2e.py +136 -0
  50. hap_cli/tests/test_integration.py +805 -0
  51. hap_cli/utils/__init__.py +1 -0
  52. hap_cli/utils/formatting.py +111 -0
  53. hap_cli/utils/options.py +10 -0
  54. hap_cli-0.5.0.dist-info/METADATA +223 -0
  55. hap_cli-0.5.0.dist-info/RECORD +58 -0
  56. hap_cli-0.5.0.dist-info/WHEEL +5 -0
  57. hap_cli-0.5.0.dist-info/entry_points.txt +2 -0
  58. hap_cli-0.5.0.dist-info/top_level.txt +1 -0
hap_cli/core/app.py ADDED
@@ -0,0 +1,307 @@
1
+ """Application management module for MingDAO HAP."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ from hap_cli.core.session import Session
6
+
7
+
8
+ def get_app_info(session: Session, app_id: str) -> dict[str, Any]:
9
+ """Get application information (basic, no sections).
10
+
11
+ Args:
12
+ session: Active session
13
+ app_id: Application ID
14
+
15
+ Returns:
16
+ Application info dict
17
+ """
18
+ return session.api_call("HomeApp", "GetApp", {"appId": app_id})
19
+
20
+
21
+ def get_app_detail(session: Session, app_id: str) -> dict[str, Any]:
22
+ """Get detailed application information including sections and worksheets.
23
+
24
+ Args:
25
+ session: Active session
26
+ app_id: Application ID
27
+
28
+ Returns:
29
+ Application detail dict with sections and worksheet lists
30
+ """
31
+ return session.api_call(
32
+ "HomeApp", "GetApp",
33
+ {"appId": app_id, "getSection": True, "getManager": True, "getLang": True},
34
+ )
35
+
36
+
37
+ def get_app_worksheets(
38
+ session: Session, app_id: str, app_section_id: str = ""
39
+ ) -> list[dict[str, Any]]:
40
+ """List worksheets in an application.
41
+
42
+ Calls GetApp with getSection=true, then flattens all workSheetInfo arrays
43
+ from all sections (optionally filtered by appSectionId).
44
+
45
+ Args:
46
+ session: Active session
47
+ app_id: Application ID
48
+ app_section_id: Optional section ID to filter to a single section
49
+
50
+ Returns:
51
+ List of worksheet info dicts
52
+ """
53
+ result = session.api_call(
54
+ "HomeApp", "GetApp",
55
+ {"appId": app_id, "getSection": True, "getManager": True, "getLang": True},
56
+ )
57
+ sections = result.get("sections", [])
58
+ worksheets: list[dict[str, Any]] = []
59
+ for sec in sections:
60
+ if app_section_id and sec.get("appSectionId") != app_section_id:
61
+ continue
62
+ worksheets.extend(sec.get("workSheetInfo", []))
63
+ return worksheets
64
+
65
+
66
+ def get_managed_apps(session: Session, project_id: str) -> list[dict[str, Any]]:
67
+ """List applications where the user is a manager.
68
+
69
+ Args:
70
+ session: Active session
71
+ project_id: Project/organization ID
72
+
73
+ Returns:
74
+ List of application info dicts with appId, name, worksheetCount
75
+ """
76
+ result = session.api_call(
77
+ "AppManagement", "GetAppForManager",
78
+ {"projectId": project_id, "type": 0},
79
+ )
80
+ apps = result if isinstance(result, list) else []
81
+ return [
82
+ {
83
+ "appId": app.get("appId", ""),
84
+ "name": app.get("appName", ""),
85
+ "worksheetCount": len(app.get("workSheetInfo", [])),
86
+ }
87
+ for app in apps
88
+ ]
89
+
90
+
91
+ def get_all_home_apps(session: Session, project_id: str) -> list[dict[str, Any]]:
92
+ """List applications for a specific project.
93
+
94
+ Calls GetAllHomeApp and filters validProject entries by projectId.
95
+
96
+ Args:
97
+ session: Active session
98
+ project_id: Project/organization ID to filter by
99
+
100
+ Returns:
101
+ List of application info dicts with appId, name, orgName, projectId
102
+ """
103
+ result = session.api_call("HomeApp", "GetAllHomeApp", {"containsLinks": False})
104
+ apps: list[dict[str, Any]] = []
105
+ valid_projects = result if isinstance(result, list) else result.get("validProject", [])
106
+ for project in valid_projects:
107
+ if project.get("projectId") != project_id:
108
+ continue
109
+ org_name = project.get("projectName", "")
110
+ for app in project.get("projectApps", []):
111
+ apps.append({
112
+ "appId": app.get("id", ""),
113
+ "name": app.get("name", ""),
114
+ "orgName": org_name,
115
+ "projectId": project_id,
116
+ })
117
+ return apps
118
+
119
+
120
+ # ── App Lifecycle ────────────────────────────────────────────────────────
121
+
122
+
123
+ def create_app(
124
+ session: Session,
125
+ project_id: str,
126
+ name: str,
127
+ icon: str = "",
128
+ icon_color: str = "",
129
+ nav_color: str = "",
130
+ ) -> dict[str, Any]:
131
+ """Create a new application."""
132
+ data: dict[str, Any] = {"projectId": project_id, "name": name}
133
+ if icon:
134
+ data["icon"] = icon
135
+ if icon_color:
136
+ data["iconColor"] = icon_color
137
+ if nav_color:
138
+ data["navColor"] = nav_color
139
+ return session.api_call("HomeApp", "CreateApp", data)
140
+
141
+
142
+ def update_app(
143
+ session: Session,
144
+ app_id: str,
145
+ name: str = "",
146
+ description: str = "",
147
+ icon_color: str = "",
148
+ nav_color: str = "",
149
+ project_id: str = "",
150
+ ) -> dict[str, Any]:
151
+ """Update application information."""
152
+ data: dict[str, Any] = {"appId": app_id}
153
+ if name:
154
+ data["name"] = name
155
+ if description:
156
+ data["description"] = description
157
+ if icon_color:
158
+ data["iconColor"] = icon_color
159
+ if nav_color:
160
+ data["navColor"] = nav_color
161
+ if project_id:
162
+ data["projectId"] = project_id
163
+ return session.api_call("HomeApp", "EditAppInfo", data)
164
+
165
+
166
+ def delete_app(session: Session, app_id: str, project_id: str) -> dict[str, Any]:
167
+ """Delete an application."""
168
+ return session.api_call(
169
+ "HomeApp", "DeleteApp",
170
+ {"appId": app_id, "projectId": project_id},
171
+ )
172
+
173
+
174
+ # ── Section (app group) management ──────────────────────────────────────
175
+
176
+
177
+ def add_section(
178
+ session: Session, app_id: str, name: str = "",
179
+ ) -> dict[str, Any]:
180
+ """Add a section/group to an application."""
181
+ data: dict[str, Any] = {"appId": app_id}
182
+ if name:
183
+ data["name"] = name
184
+ return session.api_call("HomeApp", "AddAppSection", data)
185
+
186
+
187
+ def edit_section(
188
+ session: Session, app_id: str, section_id: str, name: str,
189
+ ) -> dict[str, Any]:
190
+ """Edit a section name."""
191
+ return session.api_call(
192
+ "HomeApp", "UpdateAppSection",
193
+ {"appId": app_id, "sectionId": section_id, "name": name},
194
+ )
195
+
196
+
197
+ def delete_section(
198
+ session: Session, app_id: str, section_id: str,
199
+ ) -> dict[str, Any]:
200
+ """Delete a section from an application."""
201
+ return session.api_call(
202
+ "HomeApp", "DeleteAppSection",
203
+ {"appId": app_id, "sectionId": section_id},
204
+ )
205
+
206
+
207
+ # ── Backup / Export ──────────────────────────────────────────────────────
208
+
209
+
210
+ def backup_app(
211
+ session: Session, app_id: str, project_id: str, contain_data: bool = True,
212
+ ) -> dict[str, Any]:
213
+ """Backup an application."""
214
+ return session.api_call(
215
+ "AppManagement", "Backup",
216
+ {"appId": app_id, "projectId": project_id, "containData": contain_data},
217
+ )
218
+
219
+
220
+ def restore_app(
221
+ session: Session,
222
+ app_id: str,
223
+ project_id: str,
224
+ backup_id: str,
225
+ file_url: str,
226
+ file_name: str,
227
+ contain_data: bool = True,
228
+ is_restore_new: bool = False,
229
+ ) -> dict[str, Any]:
230
+ """Restore an application from backup."""
231
+ return session.api_call(
232
+ "AppManagement", "Restore",
233
+ {
234
+ "appId": app_id,
235
+ "projectId": project_id,
236
+ "id": backup_id,
237
+ "fileUrl": file_url,
238
+ "fileName": file_name,
239
+ "containData": contain_data,
240
+ "isRestoreNew": is_restore_new,
241
+ },
242
+ )
243
+
244
+
245
+ def export_apps(session: Session, app_ids: list[str]) -> dict[str, Any]:
246
+ """Batch export applications."""
247
+ return session.api_call("AppManagement", "BatchExportApp", {"appIds": app_ids})
248
+
249
+
250
+ def get_exports(session: Session, app_id: str) -> dict[str, Any]:
251
+ """Get export list for an application."""
252
+ return session.api_call("AppManagement", "GetExportsByApp", {"appId": app_id})
253
+
254
+
255
+ def get_backup_logs(
256
+ session: Session,
257
+ app_id: str,
258
+ project_id: str,
259
+ page_index: int = 1,
260
+ page_size: int = 20,
261
+ ) -> dict[str, Any]:
262
+ """Get backup/restore operation logs."""
263
+ return session.api_call(
264
+ "AppManagement", "PageGetBackupRestoreOperationLog",
265
+ {
266
+ "appId": app_id,
267
+ "projectId": project_id,
268
+ "pageIndex": page_index,
269
+ "pageSize": page_size,
270
+ },
271
+ )
272
+
273
+
274
+ # ── Usage & Logs ─────────────────────────────────────────────────────────
275
+
276
+
277
+ def get_usage_stats(
278
+ session: Session,
279
+ project_id: str,
280
+ app_id: str = "",
281
+ day_range: int = 7,
282
+ ) -> dict[str, Any]:
283
+ """Get application usage statistics."""
284
+ data: dict[str, Any] = {"projectId": project_id, "dayRange": day_range}
285
+ if app_id:
286
+ data["appId"] = app_id
287
+ return session.api_call(
288
+ "AppManagement", "AllUsageOverviewStatistics", data,
289
+ )
290
+
291
+
292
+ def get_app_logs(
293
+ session: Session,
294
+ app_id: str,
295
+ page_index: int = 1,
296
+ page_size: int = 50,
297
+ ) -> dict[str, Any]:
298
+ """Get application operation logs."""
299
+ return session.api_call(
300
+ "AppManagement", "GetLogs",
301
+ {"appId": app_id, "pageIndex": page_index, "pageSize": page_size},
302
+ )
303
+
304
+
305
+ def get_app_langs(session: Session, app_id: str) -> dict[str, Any]:
306
+ """Get application language list."""
307
+ return session.api_call("AppManagement", "GetAppLangs", {"appId": app_id})
hap_cli/core/auth.py ADDED
@@ -0,0 +1,219 @@
1
+ """Browser-based authentication flow for MingDAO HAP.
2
+
3
+ Implements a local callback server pattern:
4
+ 1. Start local HTTP server on a free port
5
+ 2. Open browser to HAP login page with callback URL
6
+ 3. Capture token from redirect, save to config
7
+ """
8
+
9
+ import base64
10
+ import json
11
+ import socket
12
+ import threading
13
+ import webbrowser
14
+ from http.server import HTTPServer, BaseHTTPRequestHandler
15
+ from typing import Optional
16
+ from urllib.parse import urlparse, parse_qs
17
+
18
+ import requests
19
+
20
+ from hap_cli.core.session import _main_api_base_for_url
21
+
22
+ # Known server presets: name -> (api_base, webui_url)
23
+ # api_base is kept for backward compatibility with resolve_server callers.
24
+ SERVER_PRESETS = {
25
+ "mingdao": ("https://www.mingdao.com/api", "https://www.mingdao.com"),
26
+ "nocoly": ("https://www.nocoly.com/wwwapi", "https://www.nocoly.com"),
27
+ }
28
+
29
+
30
+ def resolve_server(server: str) -> tuple[str, str]:
31
+ """Resolve server name or URL to (api_host, webui_url).
32
+
33
+ Args:
34
+ server: Preset name ('mingdao', 'nocoly') or self-hosted URL.
35
+
36
+ Returns:
37
+ Tuple of (api_host, webui_url).
38
+ """
39
+ key = server.lower().strip()
40
+ if key in SERVER_PRESETS:
41
+ return SERVER_PRESETS[key]
42
+
43
+ # Self-hosted: URL is both webui base and API base
44
+ base = server.rstrip("/")
45
+ return (f"{base}/wwwapi", base)
46
+
47
+
48
+ def get_available_port(start: int = 5100) -> int:
49
+ """Find an available TCP port starting from `start`."""
50
+ for port in range(start, start + 100):
51
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
52
+ try:
53
+ s.bind(("127.0.0.1", port))
54
+ return port
55
+ except OSError:
56
+ continue
57
+ raise RuntimeError("No available port found in range 5100-5199")
58
+
59
+
60
+ def build_auth_url(webui: str, port: int) -> str:
61
+ """Build the browser auth URL with base64-encoded callback info."""
62
+ callback_info = json.dumps({"url": f"http://localhost:{port}"})
63
+ encoded = base64.b64encode(callback_info.encode()).decode()
64
+ return f"{webui}/cliauth?p={encoded}"
65
+
66
+
67
+ def _decrypt_response(encrypted_value: str, decrypt_key: str) -> dict:
68
+ """Decrypt AES-CBC encrypted API response (private deployments).
69
+
70
+ Requires pycryptodome: pip install pycryptodome
71
+ """
72
+ try:
73
+ from Crypto.Cipher import AES
74
+ except ImportError:
75
+ raise ImportError(
76
+ "pycryptodome is required for encrypted responses. "
77
+ "Install it with: pip install pycryptodome"
78
+ )
79
+
80
+ IV = b"MIGfMA0GCSqGSIb3" # Fixed 16-byte IV
81
+ key_bytes = decrypt_key.encode("utf-8")
82
+ key_len = len(key_bytes)
83
+ required_len = 16 if key_len <= 16 else (24 if key_len <= 24 else 32)
84
+ padded_key = key_bytes[:required_len].ljust(required_len, b"\x00")
85
+ cipher = AES.new(padded_key, AES.MODE_CBC, IV)
86
+ ct = base64.b64decode(encrypted_value)
87
+ decrypted = cipher.decrypt(ct)
88
+ pad_len = decrypted[-1]
89
+ raw = decrypted[:-pad_len].decode("utf-8").replace("\t", "\\t")
90
+ return json.loads(raw)
91
+
92
+
93
+ def get_user_info(host: str, token: str) -> dict:
94
+ """Verify token and get user info via GetGlobalMeta API.
95
+
96
+ Args:
97
+ host: Login URL or API base URL (e.g. 'https://www.mingdao.com' or
98
+ 'https://www.mingdao.com/api'). Host type is auto-detected.
99
+ token: Auth token.
100
+
101
+ Returns:
102
+ Dict with keys: id, name, email, avatar, lang.
103
+
104
+ Raises:
105
+ requests.HTTPError: If API call fails.
106
+ ValueError: If response is invalid or token is rejected.
107
+ """
108
+ url = _main_api_base_for_url(host) + "/Global/GetGlobalMeta"
109
+ resp = requests.post(
110
+ url,
111
+ json={},
112
+ headers={
113
+ "Content-Type": "application/json",
114
+ "Authorization": f"md_pss_id {token}",
115
+ },
116
+ timeout=15,
117
+ )
118
+ resp.raise_for_status()
119
+ raw = resp.json()
120
+
121
+ if raw.get("state") == 0:
122
+ raise ValueError(raw.get("exception", "Token verification failed"))
123
+
124
+ # Handle encrypted responses (private deployments)
125
+ if raw.get("encrypted") and raw.get("key"):
126
+ data = _decrypt_response(raw["data"], raw["key"])
127
+ else:
128
+ data = raw
129
+
130
+ account = data.get("data", {}).get("md.global", {}).get("Account", {})
131
+ if not account:
132
+ raise ValueError("Unable to extract account info from response")
133
+
134
+ return {
135
+ "id": account.get("accountId", ""),
136
+ "name": account.get("fullname", ""),
137
+ "email": account.get("email", ""),
138
+ "avatar": account.get("avatar", ""),
139
+ "lang": account.get("lang", ""),
140
+ }
141
+
142
+
143
+ def login(server: str, timeout: int = 300) -> tuple[str, str, dict]:
144
+ """Run the full browser-based login flow.
145
+
146
+ Args:
147
+ server: Preset name ('mingdao', 'nocoly') or self-hosted URL.
148
+ timeout: Max seconds to wait for user to complete login.
149
+
150
+ Returns:
151
+ Tuple of (token, api_host, user_info).
152
+
153
+ Raises:
154
+ TimeoutError: If user does not complete login in time.
155
+ """
156
+ api_host, webui = resolve_server(server)
157
+ port = get_available_port()
158
+
159
+ received = threading.Event()
160
+ result: dict = {}
161
+ server_ref: dict = {}
162
+
163
+ class AuthCallbackHandler(BaseHTTPRequestHandler):
164
+ def do_OPTIONS(self):
165
+ self.send_response(204)
166
+ self.send_header("Access-Control-Allow-Origin", "*")
167
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
168
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
169
+ self.end_headers()
170
+
171
+ def do_GET(self):
172
+ self.send_header("Access-Control-Allow-Origin", "*")
173
+ parsed = urlparse(self.path)
174
+ params = parse_qs(parsed.query)
175
+ encoded_token = params.get("t", [None])[0]
176
+
177
+ if not encoded_token:
178
+ self.send_response(400)
179
+ self.end_headers()
180
+ self.wfile.write(b"No token provided")
181
+ return
182
+
183
+ token = base64.b64decode(encoded_token).decode("utf-8")
184
+ result["token"] = token
185
+
186
+ # Redirect browser to success page
187
+ self.send_response(302)
188
+ self.send_header("Location", f"{webui}/cliauth/success")
189
+ self.end_headers()
190
+
191
+ # Shut down server after responding
192
+ threading.Timer(
193
+ 0.1,
194
+ lambda: (server_ref["srv"].shutdown(), received.set()),
195
+ ).start()
196
+
197
+ def log_message(self, format, *args):
198
+ pass # Suppress HTTP server logs
199
+
200
+ srv = HTTPServer(("127.0.0.1", port), AuthCallbackHandler)
201
+ server_ref["srv"] = srv
202
+
203
+ thread = threading.Thread(target=srv.serve_forever, daemon=True)
204
+ thread.start()
205
+
206
+ auth_url = build_auth_url(webui, port)
207
+ webbrowser.open(auth_url)
208
+
209
+ received.wait(timeout=timeout)
210
+
211
+ token = result.get("token")
212
+ if not token:
213
+ raise TimeoutError("Login timed out — no token received")
214
+
215
+ # Verify token and get user info
216
+ user_info = get_user_info(webui, token)
217
+ # Return the login URL (webui), not the API base, so callers store
218
+ # the canonical login URL in session config.
219
+ return token, webui, user_info
@@ -0,0 +1,114 @@
1
+ """Calendar and schedule management for MingDAO HAP.
2
+
3
+ Named calendar_mod.py to avoid conflict with Python stdlib calendar module.
4
+ """
5
+
6
+ from typing import Any, Optional
7
+
8
+ from hap_cli.core.session import Session
9
+
10
+
11
+ def get_calendars(
12
+ session: Session,
13
+ start_date: str,
14
+ end_date: str,
15
+ project_id: str = "",
16
+ ) -> dict[str, Any]:
17
+ """Get calendar events list."""
18
+ data: dict[str, Any] = {"startDate": start_date, "endDate": end_date}
19
+ if project_id:
20
+ data["projectId"] = project_id
21
+ return session.api_call("Calendar", "GetCalendars", data)
22
+
23
+
24
+ def get_calendar_detail(
25
+ session: Session,
26
+ calendar_id: str,
27
+ ) -> dict[str, Any]:
28
+ """Get calendar event detail."""
29
+ return session.api_call("Calendar", "GetCalendarDetail", {"calendarId": calendar_id})
30
+
31
+
32
+ def insert_calendar(
33
+ session: Session,
34
+ name: str,
35
+ start_date: str,
36
+ end_date: str,
37
+ description: str = "",
38
+ is_private: bool = False,
39
+ cat_id: str = "",
40
+ ) -> dict[str, Any]:
41
+ """Create a new calendar event."""
42
+ data: dict[str, Any] = {
43
+ "name": name,
44
+ "startDate": start_date,
45
+ "endDate": end_date,
46
+ "isPrivate": is_private,
47
+ }
48
+ if description:
49
+ data["description"] = description
50
+ if cat_id:
51
+ data["catId"] = cat_id
52
+ return session.api_call("Calendar", "InsertCalendar", data)
53
+
54
+
55
+ def edit_calendar(
56
+ session: Session,
57
+ calendar_id: str,
58
+ name: str = "",
59
+ start_date: str = "",
60
+ end_date: str = "",
61
+ description: str = "",
62
+ ) -> dict[str, Any]:
63
+ """Edit a calendar event."""
64
+ data: dict[str, Any] = {"calendarId": calendar_id}
65
+ if name:
66
+ data["name"] = name
67
+ if start_date:
68
+ data["startDate"] = start_date
69
+ if end_date:
70
+ data["endDate"] = end_date
71
+ if description:
72
+ data["description"] = description
73
+ return session.api_call("Calendar", "EditCalendar", data)
74
+
75
+
76
+ def delete_calendar(
77
+ session: Session,
78
+ calendar_id: str,
79
+ recur_time: str = "",
80
+ ) -> dict[str, Any]:
81
+ """Delete a calendar event."""
82
+ data: dict[str, Any] = {"calendarId": calendar_id}
83
+ if recur_time:
84
+ data["recurTime"] = recur_time
85
+ return session.api_call("Calendar", "DeleteCalendar", data)
86
+
87
+
88
+ def add_members(
89
+ session: Session,
90
+ calendar_id: str,
91
+ member_ids: list[str],
92
+ ) -> dict[str, Any]:
93
+ """Add members to a calendar event."""
94
+ return session.api_call(
95
+ "Calendar", "AddMembers",
96
+ {"calendarId": calendar_id, "memberIds": member_ids},
97
+ )
98
+
99
+
100
+ def remove_member(
101
+ session: Session,
102
+ calendar_id: str,
103
+ account_id: str,
104
+ ) -> dict[str, Any]:
105
+ """Remove a member from a calendar event."""
106
+ return session.api_call(
107
+ "Calendar", "RemoveMember",
108
+ {"calendarId": calendar_id, "accountId": account_id},
109
+ )
110
+
111
+
112
+ def get_categories(session: Session) -> dict[str, Any]:
113
+ """Get all calendar categories."""
114
+ return session.api_call("Calendar", "GetUserAllCalCategories", {})