affinity-sdk 0.9.5__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 (92) hide show
  1. affinity/__init__.py +139 -0
  2. affinity/cli/__init__.py +7 -0
  3. affinity/cli/click_compat.py +27 -0
  4. affinity/cli/commands/__init__.py +1 -0
  5. affinity/cli/commands/_entity_files_dump.py +219 -0
  6. affinity/cli/commands/_list_entry_fields.py +41 -0
  7. affinity/cli/commands/_v1_parsing.py +77 -0
  8. affinity/cli/commands/company_cmds.py +2139 -0
  9. affinity/cli/commands/completion_cmd.py +33 -0
  10. affinity/cli/commands/config_cmds.py +540 -0
  11. affinity/cli/commands/entry_cmds.py +33 -0
  12. affinity/cli/commands/field_cmds.py +413 -0
  13. affinity/cli/commands/interaction_cmds.py +875 -0
  14. affinity/cli/commands/list_cmds.py +3152 -0
  15. affinity/cli/commands/note_cmds.py +433 -0
  16. affinity/cli/commands/opportunity_cmds.py +1174 -0
  17. affinity/cli/commands/person_cmds.py +1980 -0
  18. affinity/cli/commands/query_cmd.py +444 -0
  19. affinity/cli/commands/relationship_strength_cmds.py +62 -0
  20. affinity/cli/commands/reminder_cmds.py +595 -0
  21. affinity/cli/commands/resolve_url_cmd.py +127 -0
  22. affinity/cli/commands/session_cmds.py +84 -0
  23. affinity/cli/commands/task_cmds.py +110 -0
  24. affinity/cli/commands/version_cmd.py +29 -0
  25. affinity/cli/commands/whoami_cmd.py +36 -0
  26. affinity/cli/config.py +108 -0
  27. affinity/cli/context.py +749 -0
  28. affinity/cli/csv_utils.py +195 -0
  29. affinity/cli/date_utils.py +42 -0
  30. affinity/cli/decorators.py +77 -0
  31. affinity/cli/errors.py +28 -0
  32. affinity/cli/field_utils.py +355 -0
  33. affinity/cli/formatters.py +551 -0
  34. affinity/cli/help_json.py +283 -0
  35. affinity/cli/logging.py +100 -0
  36. affinity/cli/main.py +261 -0
  37. affinity/cli/options.py +53 -0
  38. affinity/cli/paths.py +32 -0
  39. affinity/cli/progress.py +183 -0
  40. affinity/cli/query/__init__.py +163 -0
  41. affinity/cli/query/aggregates.py +357 -0
  42. affinity/cli/query/dates.py +194 -0
  43. affinity/cli/query/exceptions.py +147 -0
  44. affinity/cli/query/executor.py +1236 -0
  45. affinity/cli/query/filters.py +248 -0
  46. affinity/cli/query/models.py +333 -0
  47. affinity/cli/query/output.py +331 -0
  48. affinity/cli/query/parser.py +619 -0
  49. affinity/cli/query/planner.py +430 -0
  50. affinity/cli/query/progress.py +270 -0
  51. affinity/cli/query/schema.py +439 -0
  52. affinity/cli/render.py +1589 -0
  53. affinity/cli/resolve.py +222 -0
  54. affinity/cli/resolvers.py +249 -0
  55. affinity/cli/results.py +308 -0
  56. affinity/cli/runner.py +218 -0
  57. affinity/cli/serialization.py +65 -0
  58. affinity/cli/session_cache.py +276 -0
  59. affinity/cli/types.py +70 -0
  60. affinity/client.py +771 -0
  61. affinity/clients/__init__.py +19 -0
  62. affinity/clients/http.py +3664 -0
  63. affinity/clients/pipeline.py +165 -0
  64. affinity/compare.py +501 -0
  65. affinity/downloads.py +114 -0
  66. affinity/exceptions.py +615 -0
  67. affinity/filters.py +1128 -0
  68. affinity/hooks.py +198 -0
  69. affinity/inbound_webhooks.py +302 -0
  70. affinity/models/__init__.py +163 -0
  71. affinity/models/entities.py +798 -0
  72. affinity/models/pagination.py +513 -0
  73. affinity/models/rate_limit_snapshot.py +48 -0
  74. affinity/models/secondary.py +413 -0
  75. affinity/models/types.py +663 -0
  76. affinity/policies.py +40 -0
  77. affinity/progress.py +22 -0
  78. affinity/py.typed +0 -0
  79. affinity/services/__init__.py +42 -0
  80. affinity/services/companies.py +1286 -0
  81. affinity/services/lists.py +1892 -0
  82. affinity/services/opportunities.py +1330 -0
  83. affinity/services/persons.py +1348 -0
  84. affinity/services/rate_limits.py +173 -0
  85. affinity/services/tasks.py +193 -0
  86. affinity/services/v1_only.py +2445 -0
  87. affinity/types.py +83 -0
  88. affinity_sdk-0.9.5.dist-info/METADATA +622 -0
  89. affinity_sdk-0.9.5.dist-info/RECORD +92 -0
  90. affinity_sdk-0.9.5.dist-info/WHEEL +4 -0
  91. affinity_sdk-0.9.5.dist-info/entry_points.txt +2 -0
  92. affinity_sdk-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,595 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from contextlib import ExitStack
5
+ from typing import Any
6
+
7
+ from rich.console import Console
8
+ from rich.progress import BarColumn, Progress, TaskID, TextColumn, TimeElapsedColumn
9
+
10
+ from affinity.models.secondary import Reminder, ReminderCreate, ReminderUpdate
11
+ from affinity.models.types import ReminderResetType, ReminderStatus, ReminderType
12
+ from affinity.types import CompanyId, OpportunityId, PersonId, ReminderIdType, UserId
13
+
14
+ from ..click_compat import RichCommand, RichGroup, click
15
+ from ..context import CLIContext
16
+ from ..decorators import category, destructive
17
+ from ..errors import CLIError
18
+ from ..options import output_options
19
+ from ..results import CommandContext
20
+ from ..runner import CommandOutput, run_command
21
+ from ._v1_parsing import parse_choice, parse_iso_datetime
22
+
23
+
24
+ @click.group(name="reminder", cls=RichGroup)
25
+ def reminder_group() -> None:
26
+ """Reminder commands."""
27
+
28
+
29
+ _REMINDER_TYPE_MAP = {
30
+ "one-time": ReminderType.ONE_TIME,
31
+ "recurring": ReminderType.RECURRING,
32
+ }
33
+
34
+ _REMINDER_RESET_MAP = {
35
+ "interaction": ReminderResetType.INTERACTION,
36
+ "email": ReminderResetType.EMAIL,
37
+ "meeting": ReminderResetType.MEETING,
38
+ }
39
+
40
+ _REMINDER_STATUS_MAP = {
41
+ "active": ReminderStatus.ACTIVE,
42
+ "completed": ReminderStatus.COMPLETED,
43
+ "overdue": ReminderStatus.OVERDUE,
44
+ }
45
+
46
+
47
+ def _extract_id(value: Any) -> int | None:
48
+ if value is None:
49
+ return None
50
+ if hasattr(value, "id"):
51
+ try:
52
+ return int(value.id)
53
+ except Exception:
54
+ return None
55
+ if isinstance(value, dict):
56
+ for key in (
57
+ "id",
58
+ "personId",
59
+ "organizationId",
60
+ "companyId",
61
+ "opportunityId",
62
+ "person_id",
63
+ "organization_id",
64
+ "company_id",
65
+ "opportunity_id",
66
+ ):
67
+ raw = value.get(key)
68
+ if raw is None:
69
+ continue
70
+ if isinstance(raw, bool):
71
+ continue
72
+ if isinstance(raw, (int, float)):
73
+ return int(raw)
74
+ if isinstance(raw, str) and raw.isdigit():
75
+ return int(raw)
76
+ return None
77
+
78
+
79
+ def _reminder_payload(reminder: Reminder) -> dict[str, object]:
80
+ # Convert enum values back to names for CLI display
81
+ type_name = ReminderType(reminder.type).name.lower().replace("_", "-")
82
+ status_name = ReminderStatus(reminder.status).name.lower()
83
+ reset_type_name = (
84
+ ReminderResetType(reminder.reset_type).name.lower()
85
+ if reminder.reset_type is not None
86
+ else None
87
+ )
88
+ return {
89
+ "id": int(reminder.id),
90
+ "type": type_name,
91
+ "status": status_name,
92
+ "content": reminder.content,
93
+ "dueDate": reminder.due_date,
94
+ "resetType": reset_type_name,
95
+ "reminderDays": reminder.reminder_days,
96
+ "ownerId": _extract_id(reminder.owner),
97
+ "creatorId": _extract_id(reminder.creator),
98
+ "completerId": _extract_id(reminder.completer),
99
+ "personId": _extract_id(reminder.person),
100
+ "companyId": _extract_id(reminder.company),
101
+ "opportunityId": _extract_id(reminder.opportunity),
102
+ "createdAt": reminder.created_at,
103
+ "completedAt": reminder.completed_at,
104
+ }
105
+
106
+
107
+ def _validate_single_entity(
108
+ person_id: int | None,
109
+ company_id: int | None,
110
+ opportunity_id: int | None,
111
+ ) -> None:
112
+ count = sum(1 for value in (person_id, company_id, opportunity_id) if value is not None)
113
+ if count > 1:
114
+ raise CLIError(
115
+ "Reminders can be associated with only one entity.",
116
+ error_type="usage_error",
117
+ exit_code=2,
118
+ hint="Provide only one of --person-id, --company-id, or --opportunity-id.",
119
+ )
120
+
121
+
122
+ @category("read")
123
+ @reminder_group.command(name="ls", cls=RichCommand)
124
+ @click.option("--person-id", type=int, default=None, help="Filter by person id.")
125
+ @click.option("--company-id", type=int, default=None, help="Filter by company id.")
126
+ @click.option("--opportunity-id", type=int, default=None, help="Filter by opportunity id.")
127
+ @click.option("--creator-id", type=int, default=None, help="Filter by creator id.")
128
+ @click.option("--owner-id", type=int, default=None, help="Filter by owner id.")
129
+ @click.option("--completer-id", type=int, default=None, help="Filter by completer id.")
130
+ @click.option(
131
+ "--type",
132
+ "reminder_type",
133
+ type=click.Choice(sorted(_REMINDER_TYPE_MAP.keys())),
134
+ default=None,
135
+ help="Reminder type (one-time, recurring).",
136
+ )
137
+ @click.option(
138
+ "--reset-type",
139
+ type=click.Choice(sorted(_REMINDER_RESET_MAP.keys())),
140
+ default=None,
141
+ help="Reset type for recurring reminders.",
142
+ )
143
+ @click.option(
144
+ "--status",
145
+ type=click.Choice(sorted(_REMINDER_STATUS_MAP.keys())),
146
+ default=None,
147
+ help="Reminder status (active, completed, overdue).",
148
+ )
149
+ @click.option(
150
+ "--due-after", type=str, default=None, help="Filter reminders due after this date (ISO-8601)."
151
+ )
152
+ @click.option(
153
+ "--due-before", type=str, default=None, help="Filter reminders due before this date (ISO-8601)."
154
+ )
155
+ @click.option("--page-size", "-s", type=int, default=None, help="Page size (max 500).")
156
+ @click.option(
157
+ "--cursor", type=str, default=None, help="Resume from cursor (incompatible with --page-size)."
158
+ )
159
+ @click.option(
160
+ "--max-results", "--limit", "-n", type=int, default=None, help="Stop after N results total."
161
+ )
162
+ @click.option("--all", "-A", "all_pages", is_flag=True, help="Fetch all pages.")
163
+ @output_options
164
+ @click.pass_obj
165
+ def reminder_ls(
166
+ ctx: CLIContext,
167
+ *,
168
+ person_id: int | None,
169
+ company_id: int | None,
170
+ opportunity_id: int | None,
171
+ creator_id: int | None,
172
+ owner_id: int | None,
173
+ completer_id: int | None,
174
+ reminder_type: str | None,
175
+ reset_type: str | None,
176
+ status: str | None,
177
+ due_after: str | None,
178
+ due_before: str | None,
179
+ page_size: int | None,
180
+ cursor: str | None,
181
+ max_results: int | None,
182
+ all_pages: bool,
183
+ ) -> None:
184
+ """List reminders.
185
+
186
+ Filter by entity (--person-id, --company-id, --opportunity-id), user (--owner-id,
187
+ --creator-id, --completer-id), type, status, or due date range (--due-after/--due-before).
188
+
189
+ Examples:
190
+
191
+ - `xaffinity reminder ls --person-id 123`
192
+
193
+ - `xaffinity reminder ls --status active --due-after 2024-01-01`
194
+
195
+ - `xaffinity reminder ls --owner-id 456 --type recurring`
196
+ """
197
+
198
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
199
+ client = ctx.get_client(warnings=warnings)
200
+ results: list[dict[str, object]] = []
201
+ first_page = True
202
+ page_token = cursor
203
+
204
+ # Build CommandContext upfront for all return paths
205
+ ctx_modifiers: dict[str, object] = {}
206
+ if person_id is not None:
207
+ ctx_modifiers["personId"] = person_id
208
+ if company_id is not None:
209
+ ctx_modifiers["companyId"] = company_id
210
+ if opportunity_id is not None:
211
+ ctx_modifiers["opportunityId"] = opportunity_id
212
+ if creator_id is not None:
213
+ ctx_modifiers["creatorId"] = creator_id
214
+ if owner_id is not None:
215
+ ctx_modifiers["ownerId"] = owner_id
216
+ if completer_id is not None:
217
+ ctx_modifiers["completerId"] = completer_id
218
+ if reminder_type:
219
+ ctx_modifiers["type"] = reminder_type
220
+ if reset_type:
221
+ ctx_modifiers["resetType"] = reset_type
222
+ if status:
223
+ ctx_modifiers["status"] = status
224
+ if due_after:
225
+ ctx_modifiers["dueAfter"] = due_after
226
+ if due_before:
227
+ ctx_modifiers["dueBefore"] = due_before
228
+ if page_size is not None:
229
+ ctx_modifiers["pageSize"] = page_size
230
+ if cursor is not None:
231
+ ctx_modifiers["cursor"] = cursor
232
+ if max_results is not None:
233
+ ctx_modifiers["maxResults"] = max_results
234
+ if all_pages:
235
+ ctx_modifiers["allPages"] = True
236
+
237
+ cmd_context = CommandContext(
238
+ name="reminder ls",
239
+ inputs={},
240
+ modifiers=ctx_modifiers,
241
+ )
242
+
243
+ parsed_type = parse_choice(reminder_type, _REMINDER_TYPE_MAP, label="reminder type")
244
+ parsed_reset = parse_choice(reset_type, _REMINDER_RESET_MAP, label="reset type")
245
+ parsed_status = parse_choice(status, _REMINDER_STATUS_MAP, label="status")
246
+ due_before_value = (
247
+ parse_iso_datetime(due_before, label="due-before") if due_before else None
248
+ )
249
+ due_after_value = parse_iso_datetime(due_after, label="due-after") if due_after else None
250
+ person_id_value = PersonId(person_id) if person_id is not None else None
251
+ company_id_value = CompanyId(company_id) if company_id is not None else None
252
+ opportunity_id_value = OpportunityId(opportunity_id) if opportunity_id is not None else None
253
+ creator_id_value = UserId(creator_id) if creator_id is not None else None
254
+ owner_id_value = UserId(owner_id) if owner_id is not None else None
255
+ completer_id_value = UserId(completer_id) if completer_id is not None else None
256
+
257
+ show_progress = (
258
+ ctx.progress != "never"
259
+ and not ctx.quiet
260
+ and (ctx.progress == "always" or sys.stderr.isatty())
261
+ )
262
+
263
+ with ExitStack() as stack:
264
+ progress: Progress | None = None
265
+ task_id: TaskID | None = None
266
+ if show_progress:
267
+ progress = stack.enter_context(
268
+ Progress(
269
+ TextColumn("{task.description}"),
270
+ BarColumn(),
271
+ TextColumn("{task.completed} rows"),
272
+ TimeElapsedColumn(),
273
+ console=Console(file=sys.stderr),
274
+ transient=True,
275
+ )
276
+ )
277
+ task_id = progress.add_task("Fetching", total=max_results)
278
+
279
+ while True:
280
+ page = client.reminders.list(
281
+ person_id=person_id_value,
282
+ company_id=company_id_value,
283
+ opportunity_id=opportunity_id_value,
284
+ creator_id=creator_id_value,
285
+ owner_id=owner_id_value,
286
+ completer_id=completer_id_value,
287
+ type=parsed_type,
288
+ reset_type=parsed_reset,
289
+ status=parsed_status,
290
+ due_before=due_before_value,
291
+ due_after=due_after_value,
292
+ page_size=page_size,
293
+ page_token=page_token,
294
+ )
295
+
296
+ for idx, reminder in enumerate(page.data):
297
+ results.append(_reminder_payload(reminder))
298
+ if progress and task_id is not None:
299
+ progress.update(task_id, completed=len(results))
300
+ if max_results is not None and len(results) >= max_results:
301
+ stopped_mid_page = idx < (len(page.data) - 1)
302
+ if stopped_mid_page:
303
+ warnings.append(
304
+ "Results limited by --max-results. Use --all to fetch all results."
305
+ )
306
+ pagination = None
307
+ if page.next_page_token and not stopped_mid_page:
308
+ pagination = {
309
+ "reminders": {
310
+ "nextCursor": page.next_page_token,
311
+ "prevCursor": None,
312
+ }
313
+ }
314
+ return CommandOutput(
315
+ data={"reminders": results[:max_results]},
316
+ context=cmd_context,
317
+ pagination=pagination,
318
+ api_called=True,
319
+ )
320
+
321
+ if first_page and not all_pages and max_results is None:
322
+ pagination = (
323
+ {"reminders": {"nextCursor": page.next_page_token, "prevCursor": None}}
324
+ if page.next_page_token
325
+ else None
326
+ )
327
+ return CommandOutput(
328
+ data={"reminders": results},
329
+ context=cmd_context,
330
+ pagination=pagination,
331
+ api_called=True,
332
+ )
333
+ first_page = False
334
+
335
+ page_token = page.next_page_token
336
+ if not page_token:
337
+ break
338
+
339
+ return CommandOutput(
340
+ data={"reminders": results},
341
+ context=cmd_context,
342
+ pagination=None,
343
+ api_called=True,
344
+ )
345
+
346
+ run_command(ctx, command="reminder ls", fn=fn)
347
+
348
+
349
+ @category("read")
350
+ @reminder_group.command(name="get", cls=RichCommand)
351
+ @click.argument("reminder_id", type=int)
352
+ @output_options
353
+ @click.pass_obj
354
+ def reminder_get(ctx: CLIContext, reminder_id: int) -> None:
355
+ """Get a reminder by id."""
356
+
357
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
358
+ client = ctx.get_client(warnings=warnings)
359
+ reminder = client.reminders.get(ReminderIdType(reminder_id))
360
+
361
+ cmd_context = CommandContext(
362
+ name="reminder get",
363
+ inputs={"reminderId": reminder_id},
364
+ modifiers={},
365
+ )
366
+
367
+ return CommandOutput(
368
+ data={"reminder": _reminder_payload(reminder)},
369
+ context=cmd_context,
370
+ api_called=True,
371
+ )
372
+
373
+ run_command(ctx, command="reminder get", fn=fn)
374
+
375
+
376
+ @category("write")
377
+ @reminder_group.command(name="create", cls=RichCommand)
378
+ @click.option("--owner-id", type=int, required=True, help="Owner id (required).")
379
+ @click.option(
380
+ "--type",
381
+ "reminder_type",
382
+ type=click.Choice(sorted(_REMINDER_TYPE_MAP.keys())),
383
+ required=True,
384
+ help="Reminder type (one-time, recurring).",
385
+ )
386
+ @click.option("--content", type=str, default=None, help="Reminder content.")
387
+ @click.option("--due-date", type=str, default=None, help="Due date (ISO-8601).")
388
+ @click.option(
389
+ "--reset-type",
390
+ type=click.Choice(sorted(_REMINDER_RESET_MAP.keys())),
391
+ default=None,
392
+ help="Reset type for recurring reminders.",
393
+ )
394
+ @click.option("--reminder-days", type=int, default=None, help="Days before due date to remind.")
395
+ @click.option("--person-id", type=int, default=None, help="Associate person id.")
396
+ @click.option("--company-id", type=int, default=None, help="Associate company id.")
397
+ @click.option("--opportunity-id", type=int, default=None, help="Associate opportunity id.")
398
+ @output_options
399
+ @click.pass_obj
400
+ def reminder_create(
401
+ ctx: CLIContext,
402
+ *,
403
+ owner_id: int,
404
+ reminder_type: str,
405
+ content: str | None,
406
+ due_date: str | None,
407
+ reset_type: str | None,
408
+ reminder_days: int | None,
409
+ person_id: int | None,
410
+ company_id: int | None,
411
+ opportunity_id: int | None,
412
+ ) -> None:
413
+ """Create a reminder."""
414
+
415
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
416
+ _ = warnings
417
+ _validate_single_entity(person_id, company_id, opportunity_id)
418
+
419
+ parsed_type = parse_choice(reminder_type, _REMINDER_TYPE_MAP, label="reminder type")
420
+ if parsed_type is None:
421
+ raise CLIError("Missing reminder type.", error_type="usage_error", exit_code=2)
422
+ parsed_reset = parse_choice(reset_type, _REMINDER_RESET_MAP, label="reset type")
423
+ due_date_value = parse_iso_datetime(due_date, label="due-date") if due_date else None
424
+
425
+ client = ctx.get_client(warnings=warnings)
426
+ reminder = client.reminders.create(
427
+ ReminderCreate(
428
+ owner_id=UserId(owner_id),
429
+ type=parsed_type,
430
+ content=content,
431
+ due_date=due_date_value,
432
+ reset_type=parsed_reset,
433
+ reminder_days=reminder_days,
434
+ person_id=PersonId(person_id) if person_id is not None else None,
435
+ company_id=CompanyId(company_id) if company_id is not None else None,
436
+ opportunity_id=OpportunityId(opportunity_id)
437
+ if opportunity_id is not None
438
+ else None,
439
+ )
440
+ )
441
+
442
+ # Build CommandContext for reminder create
443
+ ctx_modifiers: dict[str, object] = {
444
+ "ownerId": owner_id,
445
+ "type": reminder_type,
446
+ }
447
+ if content:
448
+ ctx_modifiers["content"] = content
449
+ if due_date:
450
+ ctx_modifiers["dueDate"] = due_date
451
+ if reset_type:
452
+ ctx_modifiers["resetType"] = reset_type
453
+ if reminder_days is not None:
454
+ ctx_modifiers["reminderDays"] = reminder_days
455
+ if person_id is not None:
456
+ ctx_modifiers["personId"] = person_id
457
+ if company_id is not None:
458
+ ctx_modifiers["companyId"] = company_id
459
+ if opportunity_id is not None:
460
+ ctx_modifiers["opportunityId"] = opportunity_id
461
+
462
+ cmd_context = CommandContext(
463
+ name="reminder create",
464
+ inputs={},
465
+ modifiers=ctx_modifiers,
466
+ )
467
+
468
+ return CommandOutput(
469
+ data={"reminder": _reminder_payload(reminder)},
470
+ context=cmd_context,
471
+ api_called=True,
472
+ )
473
+
474
+ run_command(ctx, command="reminder create", fn=fn)
475
+
476
+
477
+ @category("write")
478
+ @reminder_group.command(name="update", cls=RichCommand)
479
+ @click.argument("reminder_id", type=int)
480
+ @click.option("--owner-id", type=int, default=None, help="Owner id.")
481
+ @click.option(
482
+ "--type",
483
+ "reminder_type",
484
+ type=click.Choice(sorted(_REMINDER_TYPE_MAP.keys())),
485
+ default=None,
486
+ help="Reminder type (one-time, recurring).",
487
+ )
488
+ @click.option("--content", type=str, default=None, help="Reminder content.")
489
+ @click.option("--due-date", type=str, default=None, help="Due date (ISO-8601).")
490
+ @click.option(
491
+ "--reset-type",
492
+ type=click.Choice(sorted(_REMINDER_RESET_MAP.keys())),
493
+ default=None,
494
+ help="Reset type for recurring reminders.",
495
+ )
496
+ @click.option("--reminder-days", type=int, default=None, help="Days before due date to remind.")
497
+ @click.option(
498
+ "--completed/--not-completed", "is_completed", default=None, help="Set completion status."
499
+ )
500
+ @output_options
501
+ @click.pass_obj
502
+ def reminder_update(
503
+ ctx: CLIContext,
504
+ reminder_id: int,
505
+ *,
506
+ owner_id: int | None,
507
+ reminder_type: str | None,
508
+ content: str | None,
509
+ due_date: str | None,
510
+ reset_type: str | None,
511
+ reminder_days: int | None,
512
+ is_completed: bool | None,
513
+ ) -> None:
514
+ """Update a reminder."""
515
+
516
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
517
+ parsed_type = parse_choice(reminder_type, _REMINDER_TYPE_MAP, label="reminder type")
518
+ parsed_reset = parse_choice(reset_type, _REMINDER_RESET_MAP, label="reset type")
519
+ due_date_value = parse_iso_datetime(due_date, label="due-date") if due_date else None
520
+
521
+ client = ctx.get_client(warnings=warnings)
522
+ reminder = client.reminders.update(
523
+ ReminderIdType(reminder_id),
524
+ ReminderUpdate(
525
+ owner_id=UserId(owner_id) if owner_id is not None else None,
526
+ type=parsed_type,
527
+ content=content,
528
+ due_date=due_date_value,
529
+ reset_type=parsed_reset,
530
+ reminder_days=reminder_days,
531
+ is_completed=is_completed,
532
+ ),
533
+ )
534
+
535
+ # Build CommandContext for reminder update
536
+ ctx_modifiers: dict[str, object] = {}
537
+ if owner_id is not None:
538
+ ctx_modifiers["ownerId"] = owner_id
539
+ if reminder_type:
540
+ ctx_modifiers["type"] = reminder_type
541
+ if content:
542
+ ctx_modifiers["content"] = content
543
+ if due_date:
544
+ ctx_modifiers["dueDate"] = due_date
545
+ if reset_type:
546
+ ctx_modifiers["resetType"] = reset_type
547
+ if reminder_days is not None:
548
+ ctx_modifiers["reminderDays"] = reminder_days
549
+ if is_completed is not None:
550
+ ctx_modifiers["completed"] = is_completed
551
+
552
+ cmd_context = CommandContext(
553
+ name="reminder update",
554
+ inputs={"reminderId": reminder_id},
555
+ modifiers=ctx_modifiers,
556
+ )
557
+
558
+ return CommandOutput(
559
+ data={"reminder": _reminder_payload(reminder)},
560
+ context=cmd_context,
561
+ api_called=True,
562
+ )
563
+
564
+ run_command(ctx, command="reminder update", fn=fn)
565
+
566
+
567
+ @category("write")
568
+ @destructive
569
+ @reminder_group.command(name="delete", cls=RichCommand)
570
+ @click.argument("reminder_id", type=int)
571
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
572
+ @output_options
573
+ @click.pass_obj
574
+ def reminder_delete(ctx: CLIContext, reminder_id: int, yes: bool) -> None:
575
+ """Delete a reminder."""
576
+ if not yes:
577
+ click.confirm(f"Delete reminder {reminder_id}?", abort=True)
578
+
579
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
580
+ client = ctx.get_client(warnings=warnings)
581
+ success = client.reminders.delete(ReminderIdType(reminder_id))
582
+
583
+ cmd_context = CommandContext(
584
+ name="reminder delete",
585
+ inputs={"reminderId": reminder_id},
586
+ modifiers={},
587
+ )
588
+
589
+ return CommandOutput(
590
+ data={"success": success},
591
+ context=cmd_context,
592
+ api_called=True,
593
+ )
594
+
595
+ run_command(ctx, command="reminder delete", fn=fn)