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,433 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from contextlib import ExitStack
5
+
6
+ from rich.console import Console
7
+ from rich.progress import BarColumn, Progress, TaskID, TextColumn, TimeElapsedColumn
8
+
9
+ from affinity.models.secondary import Note, NoteCreate, NoteUpdate
10
+ from affinity.models.types import InteractionType, NoteType
11
+ from affinity.types import CompanyId, NoteId, OpportunityId, PersonId, UserId
12
+
13
+ from ..click_compat import RichCommand, RichGroup, click
14
+ from ..context import CLIContext
15
+ from ..decorators import category, destructive
16
+ from ..errors import CLIError
17
+ from ..options import output_options
18
+ from ..results import CommandContext
19
+ from ..runner import CommandOutput, run_command
20
+ from ._v1_parsing import parse_choice, parse_iso_datetime
21
+
22
+
23
+ @click.group(name="note", cls=RichGroup)
24
+ def note_group() -> None:
25
+ """Note commands."""
26
+
27
+
28
+ _NOTE_TYPE_MAP = {
29
+ "plain-text": NoteType.PLAIN_TEXT,
30
+ "plain": NoteType.PLAIN_TEXT,
31
+ "html": NoteType.HTML,
32
+ "ai-notetaker": NoteType.AI_NOTETAKER,
33
+ "email-derived": NoteType.EMAIL_DERIVED,
34
+ }
35
+
36
+
37
+ def _note_payload(note: Note) -> dict[str, object]:
38
+ # Convert enum values back to names for CLI display
39
+ type_name = NoteType(note.type).name.lower().replace("_", "-")
40
+ interaction_type_name = (
41
+ InteractionType(note.interaction_type).name.lower().replace("_", "-")
42
+ if note.interaction_type is not None
43
+ else None
44
+ )
45
+ return {
46
+ "id": int(note.id),
47
+ "type": type_name,
48
+ "creatorId": int(note.creator_id),
49
+ "content": note.content,
50
+ "personIds": [int(p) for p in note.person_ids],
51
+ "associatedPersonIds": [int(p) for p in note.associated_person_ids],
52
+ "interactionPersonIds": [int(p) for p in note.interaction_person_ids],
53
+ "mentionedPersonIds": [int(p) for p in note.mentioned_person_ids],
54
+ "companyIds": [int(o) for o in note.company_ids],
55
+ "opportunityIds": [int(o) for o in note.opportunity_ids],
56
+ "interactionId": note.interaction_id,
57
+ "interactionType": interaction_type_name,
58
+ "isMeeting": note.is_meeting,
59
+ "parentId": int(note.parent_id) if note.parent_id else None,
60
+ "createdAt": note.created_at,
61
+ "updatedAt": note.updated_at,
62
+ }
63
+
64
+
65
+ @category("read")
66
+ @note_group.command(name="ls", cls=RichCommand)
67
+ @click.option("--person-id", type=int, default=None, help="Filter by person id.")
68
+ @click.option("--company-id", type=int, default=None, help="Filter by company id.")
69
+ @click.option("--opportunity-id", type=int, default=None, help="Filter by opportunity id.")
70
+ @click.option("--creator-id", type=int, default=None, help="Filter by creator id.")
71
+ @click.option("--page-size", "-s", type=int, default=None, help="Page size (max 500).")
72
+ @click.option(
73
+ "--cursor", type=str, default=None, help="Resume from cursor (incompatible with --page-size)."
74
+ )
75
+ @click.option(
76
+ "--max-results", "--limit", "-n", type=int, default=None, help="Stop after N results total."
77
+ )
78
+ @click.option("--all", "-A", "all_pages", is_flag=True, help="Fetch all pages.")
79
+ @output_options
80
+ @click.pass_obj
81
+ def note_ls(
82
+ ctx: CLIContext,
83
+ *,
84
+ person_id: int | None,
85
+ company_id: int | None,
86
+ opportunity_id: int | None,
87
+ creator_id: int | None,
88
+ page_size: int | None,
89
+ cursor: str | None,
90
+ max_results: int | None,
91
+ all_pages: bool,
92
+ ) -> None:
93
+ """
94
+ List notes.
95
+
96
+ Examples:
97
+
98
+ - `xaffinity note ls --person-id 12345`
99
+ - `xaffinity note ls --company-id 67890 --all`
100
+ - `xaffinity note ls --creator-id 111 --max-results 50`
101
+ """
102
+
103
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
104
+ client = ctx.get_client(warnings=warnings)
105
+ results: list[dict[str, object]] = []
106
+ first_page = True
107
+ page_token = cursor
108
+ person_id_value = PersonId(person_id) if person_id is not None else None
109
+ company_id_value = CompanyId(company_id) if company_id is not None else None
110
+ opportunity_id_value = OpportunityId(opportunity_id) if opportunity_id is not None else None
111
+ creator_id_value = UserId(creator_id) if creator_id is not None else None
112
+
113
+ # Build CommandContext upfront for use in all return paths
114
+ ctx_modifiers: dict[str, object] = {}
115
+ if person_id is not None:
116
+ ctx_modifiers["personId"] = person_id
117
+ if company_id is not None:
118
+ ctx_modifiers["companyId"] = company_id
119
+ if opportunity_id is not None:
120
+ ctx_modifiers["opportunityId"] = opportunity_id
121
+ if creator_id is not None:
122
+ ctx_modifiers["creatorId"] = creator_id
123
+ if page_size is not None:
124
+ ctx_modifiers["pageSize"] = page_size
125
+ if cursor is not None:
126
+ ctx_modifiers["cursor"] = cursor
127
+ if max_results is not None:
128
+ ctx_modifiers["maxResults"] = max_results
129
+ if all_pages:
130
+ ctx_modifiers["allPages"] = True
131
+
132
+ cmd_context = CommandContext(
133
+ name="note ls",
134
+ inputs={},
135
+ modifiers=ctx_modifiers,
136
+ )
137
+
138
+ show_progress = (
139
+ ctx.progress != "never"
140
+ and not ctx.quiet
141
+ and (ctx.progress == "always" or sys.stderr.isatty())
142
+ )
143
+
144
+ with ExitStack() as stack:
145
+ progress: Progress | None = None
146
+ task_id: TaskID | None = None
147
+ if show_progress:
148
+ progress = stack.enter_context(
149
+ Progress(
150
+ TextColumn("{task.description}"),
151
+ BarColumn(),
152
+ TextColumn("{task.completed} rows"),
153
+ TimeElapsedColumn(),
154
+ console=Console(file=sys.stderr),
155
+ transient=True,
156
+ )
157
+ )
158
+ task_id = progress.add_task("Fetching", total=max_results)
159
+
160
+ while True:
161
+ page = client.notes.list(
162
+ person_id=person_id_value,
163
+ company_id=company_id_value,
164
+ opportunity_id=opportunity_id_value,
165
+ creator_id=creator_id_value,
166
+ page_size=page_size,
167
+ page_token=page_token,
168
+ )
169
+
170
+ for idx, note in enumerate(page.data):
171
+ results.append(_note_payload(note))
172
+ if progress and task_id is not None:
173
+ progress.update(task_id, completed=len(results))
174
+ if max_results is not None and len(results) >= max_results:
175
+ stopped_mid_page = idx < (len(page.data) - 1)
176
+ if stopped_mid_page:
177
+ warnings.append(
178
+ "Results limited by --max-results. Use --all to fetch all results."
179
+ )
180
+ pagination = None
181
+ if page.next_page_token and not stopped_mid_page:
182
+ pagination = {"nextCursor": page.next_page_token, "prevCursor": None}
183
+ return CommandOutput(
184
+ data=results[:max_results], # Direct array, not wrapped
185
+ context=cmd_context,
186
+ pagination=pagination,
187
+ api_called=True,
188
+ )
189
+
190
+ if first_page and not all_pages and max_results is None:
191
+ pagination = (
192
+ {"nextCursor": page.next_page_token, "prevCursor": None}
193
+ if page.next_page_token
194
+ else None
195
+ )
196
+ return CommandOutput(
197
+ data=results, # Direct array, not wrapped
198
+ context=cmd_context,
199
+ pagination=pagination,
200
+ api_called=True,
201
+ )
202
+ first_page = False
203
+
204
+ page_token = page.next_page_token
205
+ if not page_token:
206
+ break
207
+
208
+ return CommandOutput(
209
+ data=results, # Direct array, not wrapped
210
+ context=cmd_context,
211
+ pagination=None,
212
+ api_called=True,
213
+ )
214
+
215
+ run_command(ctx, command="note ls", fn=fn)
216
+
217
+
218
+ @category("read")
219
+ @note_group.command(name="get", cls=RichCommand)
220
+ @click.argument("note_id", type=int)
221
+ @output_options
222
+ @click.pass_obj
223
+ def note_get(ctx: CLIContext, note_id: int) -> None:
224
+ """
225
+ Get a note by id.
226
+
227
+ Example: `xaffinity note get 12345`
228
+ """
229
+
230
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
231
+ client = ctx.get_client(warnings=warnings)
232
+ note = client.notes.get(NoteId(note_id))
233
+
234
+ cmd_context = CommandContext(
235
+ name="note get",
236
+ inputs={"noteId": note_id},
237
+ modifiers={},
238
+ )
239
+
240
+ payload = _note_payload(note)
241
+
242
+ # For table display, separate content from metadata for better readability
243
+ if ctx.output != "json":
244
+ content = payload.pop("content", None)
245
+ data: dict[str, object] = {"note": payload}
246
+ if content:
247
+ # Use _text marker for clean text rendering without "Value:" wrapper
248
+ data["Content"] = {"_text": content}
249
+ else:
250
+ data = {"note": payload}
251
+
252
+ return CommandOutput(
253
+ data=data,
254
+ context=cmd_context,
255
+ api_called=True,
256
+ )
257
+
258
+ run_command(ctx, command="note get", fn=fn)
259
+
260
+
261
+ @category("write")
262
+ @note_group.command(name="create", cls=RichCommand)
263
+ @click.option("--content", type=str, required=True, help="Note content.")
264
+ @click.option(
265
+ "--type",
266
+ "note_type",
267
+ type=click.Choice(sorted(_NOTE_TYPE_MAP.keys())),
268
+ default=None,
269
+ help="Note type (plain-text, html, ai-notetaker, email-derived).",
270
+ )
271
+ @click.option("--person-id", "person_ids", multiple=True, type=int, help="Associate person id.")
272
+ @click.option(
273
+ "--company-id",
274
+ "company_ids",
275
+ multiple=True,
276
+ type=int,
277
+ help="Associate company id.",
278
+ )
279
+ @click.option(
280
+ "--opportunity-id",
281
+ "opportunity_ids",
282
+ multiple=True,
283
+ type=int,
284
+ help="Associate opportunity id.",
285
+ )
286
+ @click.option("--parent-id", type=int, default=None, help="Parent note id (reply).")
287
+ @click.option("--creator-id", type=int, default=None, help="Creator id override.")
288
+ @click.option(
289
+ "--created-at",
290
+ type=str,
291
+ default=None,
292
+ help="Creation timestamp (ISO-8601).",
293
+ )
294
+ @output_options
295
+ @click.pass_obj
296
+ def note_create(
297
+ ctx: CLIContext,
298
+ *,
299
+ content: str,
300
+ note_type: str | None,
301
+ person_ids: tuple[int, ...],
302
+ company_ids: tuple[int, ...],
303
+ opportunity_ids: tuple[int, ...],
304
+ parent_id: int | None,
305
+ creator_id: int | None,
306
+ created_at: str | None,
307
+ ) -> None:
308
+ """
309
+ Create a note attached to an entity.
310
+
311
+ Examples:
312
+
313
+ - `xaffinity note create --content "Meeting notes" --person-id 12345`
314
+ - `xaffinity note create --content "<b>Summary</b>" --type html --company-id 67890`
315
+ """
316
+
317
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
318
+ _ = warnings
319
+ if not (person_ids or company_ids or opportunity_ids or parent_id):
320
+ raise CLIError(
321
+ "Notes must be attached to at least one entity or parent note.",
322
+ exit_code=2,
323
+ error_type="usage_error",
324
+ hint="Provide --person-id/--company-id/--opportunity-id or --parent-id.",
325
+ )
326
+
327
+ parsed_type = parse_choice(note_type, _NOTE_TYPE_MAP, label="note type")
328
+ created_at_value = (
329
+ parse_iso_datetime(created_at, label="created-at") if created_at else None
330
+ )
331
+
332
+ client = ctx.get_client(warnings=warnings)
333
+ note = client.notes.create(
334
+ NoteCreate(
335
+ content=content,
336
+ type=parsed_type or NoteType.PLAIN_TEXT,
337
+ person_ids=[PersonId(pid) for pid in person_ids],
338
+ company_ids=[CompanyId(cid) for cid in company_ids],
339
+ opportunity_ids=[OpportunityId(oid) for oid in opportunity_ids],
340
+ parent_id=NoteId(parent_id) if parent_id else None,
341
+ creator_id=UserId(creator_id) if creator_id is not None else None,
342
+ created_at=created_at_value,
343
+ )
344
+ )
345
+
346
+ ctx_modifiers: dict[str, object] = {}
347
+ if note_type:
348
+ ctx_modifiers["type"] = note_type
349
+ if person_ids:
350
+ ctx_modifiers["personIds"] = list(person_ids)
351
+ if company_ids:
352
+ ctx_modifiers["companyIds"] = list(company_ids)
353
+ if opportunity_ids:
354
+ ctx_modifiers["opportunityIds"] = list(opportunity_ids)
355
+ if parent_id:
356
+ ctx_modifiers["parentId"] = parent_id
357
+ if creator_id is not None:
358
+ ctx_modifiers["creatorId"] = creator_id
359
+ if created_at:
360
+ ctx_modifiers["createdAt"] = created_at
361
+
362
+ cmd_context = CommandContext(
363
+ name="note create",
364
+ inputs={},
365
+ modifiers=ctx_modifiers,
366
+ )
367
+
368
+ return CommandOutput(
369
+ data={"note": _note_payload(note)},
370
+ context=cmd_context,
371
+ api_called=True,
372
+ )
373
+
374
+ run_command(ctx, command="note create", fn=fn)
375
+
376
+
377
+ @category("write")
378
+ @note_group.command(name="update", cls=RichCommand)
379
+ @click.argument("note_id", type=int)
380
+ @click.option("--content", type=str, required=True, help="Updated note content.")
381
+ @output_options
382
+ @click.pass_obj
383
+ def note_update(ctx: CLIContext, note_id: int, *, content: str) -> None:
384
+ """Update a note."""
385
+
386
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
387
+ client = ctx.get_client(warnings=warnings)
388
+ note = client.notes.update(NoteId(note_id), NoteUpdate(content=content))
389
+
390
+ cmd_context = CommandContext(
391
+ name="note update",
392
+ inputs={"noteId": note_id},
393
+ modifiers={"content": content},
394
+ )
395
+
396
+ return CommandOutput(
397
+ data={"note": _note_payload(note)},
398
+ context=cmd_context,
399
+ api_called=True,
400
+ )
401
+
402
+ run_command(ctx, command="note update", fn=fn)
403
+
404
+
405
+ @category("write")
406
+ @destructive
407
+ @note_group.command(name="delete", cls=RichCommand)
408
+ @click.argument("note_id", type=int)
409
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
410
+ @output_options
411
+ @click.pass_obj
412
+ def note_delete(ctx: CLIContext, note_id: int, yes: bool) -> None:
413
+ """Delete a note."""
414
+ if not yes:
415
+ click.confirm(f"Delete note {note_id}?", abort=True)
416
+
417
+ def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
418
+ client = ctx.get_client(warnings=warnings)
419
+ success = client.notes.delete(NoteId(note_id))
420
+
421
+ cmd_context = CommandContext(
422
+ name="note delete",
423
+ inputs={"noteId": note_id},
424
+ modifiers={},
425
+ )
426
+
427
+ return CommandOutput(
428
+ data={"success": success},
429
+ context=cmd_context,
430
+ api_called=True,
431
+ )
432
+
433
+ run_command(ctx, command="note delete", fn=fn)