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.
- affinity/__init__.py +139 -0
- affinity/cli/__init__.py +7 -0
- affinity/cli/click_compat.py +27 -0
- affinity/cli/commands/__init__.py +1 -0
- affinity/cli/commands/_entity_files_dump.py +219 -0
- affinity/cli/commands/_list_entry_fields.py +41 -0
- affinity/cli/commands/_v1_parsing.py +77 -0
- affinity/cli/commands/company_cmds.py +2139 -0
- affinity/cli/commands/completion_cmd.py +33 -0
- affinity/cli/commands/config_cmds.py +540 -0
- affinity/cli/commands/entry_cmds.py +33 -0
- affinity/cli/commands/field_cmds.py +413 -0
- affinity/cli/commands/interaction_cmds.py +875 -0
- affinity/cli/commands/list_cmds.py +3152 -0
- affinity/cli/commands/note_cmds.py +433 -0
- affinity/cli/commands/opportunity_cmds.py +1174 -0
- affinity/cli/commands/person_cmds.py +1980 -0
- affinity/cli/commands/query_cmd.py +444 -0
- affinity/cli/commands/relationship_strength_cmds.py +62 -0
- affinity/cli/commands/reminder_cmds.py +595 -0
- affinity/cli/commands/resolve_url_cmd.py +127 -0
- affinity/cli/commands/session_cmds.py +84 -0
- affinity/cli/commands/task_cmds.py +110 -0
- affinity/cli/commands/version_cmd.py +29 -0
- affinity/cli/commands/whoami_cmd.py +36 -0
- affinity/cli/config.py +108 -0
- affinity/cli/context.py +749 -0
- affinity/cli/csv_utils.py +195 -0
- affinity/cli/date_utils.py +42 -0
- affinity/cli/decorators.py +77 -0
- affinity/cli/errors.py +28 -0
- affinity/cli/field_utils.py +355 -0
- affinity/cli/formatters.py +551 -0
- affinity/cli/help_json.py +283 -0
- affinity/cli/logging.py +100 -0
- affinity/cli/main.py +261 -0
- affinity/cli/options.py +53 -0
- affinity/cli/paths.py +32 -0
- affinity/cli/progress.py +183 -0
- affinity/cli/query/__init__.py +163 -0
- affinity/cli/query/aggregates.py +357 -0
- affinity/cli/query/dates.py +194 -0
- affinity/cli/query/exceptions.py +147 -0
- affinity/cli/query/executor.py +1236 -0
- affinity/cli/query/filters.py +248 -0
- affinity/cli/query/models.py +333 -0
- affinity/cli/query/output.py +331 -0
- affinity/cli/query/parser.py +619 -0
- affinity/cli/query/planner.py +430 -0
- affinity/cli/query/progress.py +270 -0
- affinity/cli/query/schema.py +439 -0
- affinity/cli/render.py +1589 -0
- affinity/cli/resolve.py +222 -0
- affinity/cli/resolvers.py +249 -0
- affinity/cli/results.py +308 -0
- affinity/cli/runner.py +218 -0
- affinity/cli/serialization.py +65 -0
- affinity/cli/session_cache.py +276 -0
- affinity/cli/types.py +70 -0
- affinity/client.py +771 -0
- affinity/clients/__init__.py +19 -0
- affinity/clients/http.py +3664 -0
- affinity/clients/pipeline.py +165 -0
- affinity/compare.py +501 -0
- affinity/downloads.py +114 -0
- affinity/exceptions.py +615 -0
- affinity/filters.py +1128 -0
- affinity/hooks.py +198 -0
- affinity/inbound_webhooks.py +302 -0
- affinity/models/__init__.py +163 -0
- affinity/models/entities.py +798 -0
- affinity/models/pagination.py +513 -0
- affinity/models/rate_limit_snapshot.py +48 -0
- affinity/models/secondary.py +413 -0
- affinity/models/types.py +663 -0
- affinity/policies.py +40 -0
- affinity/progress.py +22 -0
- affinity/py.typed +0 -0
- affinity/services/__init__.py +42 -0
- affinity/services/companies.py +1286 -0
- affinity/services/lists.py +1892 -0
- affinity/services/opportunities.py +1330 -0
- affinity/services/persons.py +1348 -0
- affinity/services/rate_limits.py +173 -0
- affinity/services/tasks.py +193 -0
- affinity/services/v1_only.py +2445 -0
- affinity/types.py +83 -0
- affinity_sdk-0.9.5.dist-info/METADATA +622 -0
- affinity_sdk-0.9.5.dist-info/RECORD +92 -0
- affinity_sdk-0.9.5.dist-info/WHEEL +4 -0
- affinity_sdk-0.9.5.dist-info/entry_points.txt +2 -0
- affinity_sdk-0.9.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from contextlib import ExitStack
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.progress import BarColumn, Progress, TaskID, TextColumn, TimeElapsedColumn
|
|
12
|
+
|
|
13
|
+
from affinity.exceptions import AffinityError
|
|
14
|
+
from affinity.models.secondary import Interaction, InteractionCreate, InteractionUpdate
|
|
15
|
+
from affinity.models.types import InteractionDirection, InteractionType
|
|
16
|
+
from affinity.types import CompanyId, InteractionId, OpportunityId, PersonId
|
|
17
|
+
|
|
18
|
+
from ..click_compat import RichCommand, RichGroup, click
|
|
19
|
+
from ..context import CLIContext
|
|
20
|
+
from ..csv_utils import write_csv_to_stdout
|
|
21
|
+
from ..date_utils import ChunkedFetchResult, chunk_date_range
|
|
22
|
+
from ..decorators import category, destructive
|
|
23
|
+
from ..errors import CLIError
|
|
24
|
+
from ..options import output_options
|
|
25
|
+
from ..results import CommandContext, DateRange, ResultSummary
|
|
26
|
+
from ..runner import CommandOutput, run_command
|
|
27
|
+
from ._v1_parsing import parse_choice, parse_iso_datetime
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group(name="interaction", cls=RichGroup)
|
|
31
|
+
def interaction_group() -> None:
|
|
32
|
+
"""Interaction commands."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_INTERACTION_TYPE_MAP = {
|
|
36
|
+
"meeting": InteractionType.MEETING,
|
|
37
|
+
"call": InteractionType.CALL,
|
|
38
|
+
"chat-message": InteractionType.CHAT_MESSAGE,
|
|
39
|
+
"chat": InteractionType.CHAT_MESSAGE,
|
|
40
|
+
"email": InteractionType.EMAIL,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_INTERACTION_DIRECTION_MAP = {
|
|
44
|
+
"outgoing": InteractionDirection.OUTGOING,
|
|
45
|
+
"incoming": InteractionDirection.INCOMING,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Canonical types for --type all expansion and output
|
|
49
|
+
_CANONICAL_TYPES = ["call", "chat-message", "email", "meeting"]
|
|
50
|
+
|
|
51
|
+
# Accepted types includes aliases (e.g., "chat" for "chat-message")
|
|
52
|
+
_ACCEPTED_TYPES = sorted(_INTERACTION_TYPE_MAP.keys())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class TypeStats:
|
|
57
|
+
"""Per-type statistics for multi-type fetch."""
|
|
58
|
+
|
|
59
|
+
count: int
|
|
60
|
+
chunks_processed: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class MultiTypeFetchResult:
|
|
65
|
+
"""Result from multi-type interaction fetching."""
|
|
66
|
+
|
|
67
|
+
interactions: list[Interaction]
|
|
68
|
+
type_stats: dict[str, TypeStats]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class _NDJSONProgress:
|
|
73
|
+
"""NDJSON progress emitter for non-TTY environments (MCP consumption)."""
|
|
74
|
+
|
|
75
|
+
enabled: bool = False
|
|
76
|
+
# Rate limit at 0.65s (matches ProgressManager) to stay under mcp-bash 100/min limit
|
|
77
|
+
_min_interval: float = 0.65
|
|
78
|
+
_last_emit_time: float = field(default_factory=lambda: float("-inf"))
|
|
79
|
+
|
|
80
|
+
def emit(
|
|
81
|
+
self,
|
|
82
|
+
message: str,
|
|
83
|
+
*,
|
|
84
|
+
current: int | None = None,
|
|
85
|
+
total: int | None = None,
|
|
86
|
+
force: bool = False,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Emit NDJSON progress to stderr for MCP consumption.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
message: Human-readable progress message.
|
|
92
|
+
current: Current count (e.g., interactions fetched so far).
|
|
93
|
+
total: Total count if known (None for indeterminate).
|
|
94
|
+
force: Bypass rate limiting (for final summary).
|
|
95
|
+
"""
|
|
96
|
+
if not self.enabled:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Rate limit (unless forced)
|
|
100
|
+
now = time.monotonic()
|
|
101
|
+
if not force and now - self._last_emit_time < self._min_interval:
|
|
102
|
+
return
|
|
103
|
+
self._last_emit_time = now
|
|
104
|
+
|
|
105
|
+
# Compute percent if both current and total are known
|
|
106
|
+
percent = (current * 100 // total) if (current is not None and total) else None
|
|
107
|
+
|
|
108
|
+
obj: dict[str, int | str | None] = {
|
|
109
|
+
"type": "progress",
|
|
110
|
+
"progress": percent,
|
|
111
|
+
"message": message,
|
|
112
|
+
}
|
|
113
|
+
if current is not None:
|
|
114
|
+
obj["current"] = current
|
|
115
|
+
if total is not None:
|
|
116
|
+
obj["total"] = total
|
|
117
|
+
|
|
118
|
+
# flush=True is CRITICAL: Python buffers stderr when not a TTY
|
|
119
|
+
print(json.dumps(obj), file=sys.stderr, flush=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _resolve_types(interaction_types: tuple[str, ...]) -> list[str]:
|
|
123
|
+
"""Expand 'all' and deduplicate by canonical type.
|
|
124
|
+
|
|
125
|
+
Handles aliases: 'chat' → 'chat-message'
|
|
126
|
+
Returns canonical type names only.
|
|
127
|
+
"""
|
|
128
|
+
if "all" in interaction_types:
|
|
129
|
+
return _CANONICAL_TYPES.copy()
|
|
130
|
+
|
|
131
|
+
# Deduplicate by resolved enum (handles chat vs chat-message)
|
|
132
|
+
seen_enums: set[InteractionType] = set()
|
|
133
|
+
result: list[str] = []
|
|
134
|
+
for t in interaction_types:
|
|
135
|
+
enum_val = _INTERACTION_TYPE_MAP[t]
|
|
136
|
+
if enum_val not in seen_enums:
|
|
137
|
+
seen_enums.add(enum_val)
|
|
138
|
+
# Use canonical name (e.g., chat → chat-message)
|
|
139
|
+
canonical = InteractionType(enum_val).name.lower().replace("_", "-")
|
|
140
|
+
result.append(canonical)
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _interaction_payload(interaction: Interaction) -> dict[str, object]:
|
|
145
|
+
# Convert enum values back to names for CLI display
|
|
146
|
+
type_name = InteractionType(interaction.type).name.lower().replace("_", "-")
|
|
147
|
+
direction_name = (
|
|
148
|
+
InteractionDirection(interaction.direction).name.lower()
|
|
149
|
+
if interaction.direction is not None
|
|
150
|
+
else None
|
|
151
|
+
)
|
|
152
|
+
return {
|
|
153
|
+
"id": int(interaction.id),
|
|
154
|
+
"type": type_name,
|
|
155
|
+
"date": interaction.date,
|
|
156
|
+
"direction": direction_name,
|
|
157
|
+
"title": interaction.title,
|
|
158
|
+
"subject": interaction.subject,
|
|
159
|
+
"startTime": interaction.start_time,
|
|
160
|
+
"endTime": interaction.end_time,
|
|
161
|
+
"personIds": [int(p.id) for p in interaction.persons],
|
|
162
|
+
"attendees": interaction.attendees,
|
|
163
|
+
"notes": [int(n) for n in interaction.notes],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _resolve_date_range(
|
|
168
|
+
after: str | None,
|
|
169
|
+
before: str | None,
|
|
170
|
+
days: int | None,
|
|
171
|
+
) -> tuple[datetime, datetime]:
|
|
172
|
+
"""Resolve date flags to start/end datetimes.
|
|
173
|
+
|
|
174
|
+
If neither --days nor --after is specified, defaults to "all time"
|
|
175
|
+
(starting from 2010-01-01, which predates all possible Affinity data).
|
|
176
|
+
"""
|
|
177
|
+
now = datetime.now(timezone.utc)
|
|
178
|
+
|
|
179
|
+
# Mutual exclusion
|
|
180
|
+
if days is not None and after is not None:
|
|
181
|
+
raise CLIError(
|
|
182
|
+
"--days and --after are mutually exclusive.",
|
|
183
|
+
error_type="usage_error",
|
|
184
|
+
exit_code=2,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Resolve start
|
|
188
|
+
if days is not None:
|
|
189
|
+
start = now - timedelta(days=days)
|
|
190
|
+
elif after is not None:
|
|
191
|
+
# parse_iso_datetime returns UTC-aware datetime
|
|
192
|
+
# (naive strings interpreted as local time, then converted to UTC)
|
|
193
|
+
start = parse_iso_datetime(after, label="after")
|
|
194
|
+
else:
|
|
195
|
+
# Default: all time (Affinity founded 2014, so 2010 predates all possible data)
|
|
196
|
+
# Using a fixed date rather than datetime.min avoids timezone edge cases
|
|
197
|
+
start = datetime(2010, 1, 1, tzinfo=timezone.utc)
|
|
198
|
+
|
|
199
|
+
# Resolve end (parse_iso_datetime returns UTC-aware datetime)
|
|
200
|
+
end = parse_iso_datetime(before, label="before") if before is not None else now
|
|
201
|
+
|
|
202
|
+
# Validate
|
|
203
|
+
if start >= end:
|
|
204
|
+
raise CLIError(
|
|
205
|
+
f"Start date ({start.date()}) must be before end date ({end.date()}).",
|
|
206
|
+
error_type="usage_error",
|
|
207
|
+
exit_code=2,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return start, end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _fetch_interactions_chunked(
|
|
214
|
+
client: object, # Affinity client (typed as object to avoid import cycle)
|
|
215
|
+
*,
|
|
216
|
+
interaction_type: InteractionType,
|
|
217
|
+
start: datetime,
|
|
218
|
+
end: datetime,
|
|
219
|
+
person_id: PersonId | None,
|
|
220
|
+
company_id: CompanyId | None,
|
|
221
|
+
opportunity_id: OpportunityId | None,
|
|
222
|
+
page_size: int | None,
|
|
223
|
+
max_results: int | None,
|
|
224
|
+
progress: Progress | None,
|
|
225
|
+
task_id: TaskID | None,
|
|
226
|
+
suppress_chunk_description: bool = False,
|
|
227
|
+
progress_offset: int = 0,
|
|
228
|
+
) -> ChunkedFetchResult:
|
|
229
|
+
"""
|
|
230
|
+
Fetch interactions across date chunks.
|
|
231
|
+
|
|
232
|
+
Returns ChunkedFetchResult with interactions and chunk count for metadata.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
suppress_chunk_description: If True, don't update progress description with
|
|
236
|
+
chunk info (used in multi-type mode where outer loop controls description).
|
|
237
|
+
progress_offset: Offset to add to progress counts (for cumulative totals
|
|
238
|
+
in multi-type mode).
|
|
239
|
+
|
|
240
|
+
Note: Relies on API using exclusive end_time boundary.
|
|
241
|
+
If an interaction has timestamp exactly at chunk boundary,
|
|
242
|
+
it will appear in the later chunk (not both).
|
|
243
|
+
"""
|
|
244
|
+
chunks = list(chunk_date_range(start, end))
|
|
245
|
+
total_chunks = len(chunks)
|
|
246
|
+
results: list[Interaction] = []
|
|
247
|
+
chunks_processed = 0
|
|
248
|
+
|
|
249
|
+
for chunk_idx, (chunk_start, chunk_end) in enumerate(chunks, 1):
|
|
250
|
+
chunks_processed = chunk_idx
|
|
251
|
+
|
|
252
|
+
# Update progress description with chunk info (suppressed in multi-type mode)
|
|
253
|
+
if not suppress_chunk_description and progress and task_id is not None:
|
|
254
|
+
desc = f"{chunk_start.date()} - {chunk_end.date()} ({chunk_idx}/{total_chunks})"
|
|
255
|
+
progress.update(task_id, description=desc)
|
|
256
|
+
|
|
257
|
+
# Paginate within chunk
|
|
258
|
+
page_token: str | None = None
|
|
259
|
+
while True:
|
|
260
|
+
try:
|
|
261
|
+
page = client.interactions.list( # type: ignore[attr-defined]
|
|
262
|
+
type=interaction_type,
|
|
263
|
+
start_time=chunk_start,
|
|
264
|
+
end_time=chunk_end,
|
|
265
|
+
person_id=person_id,
|
|
266
|
+
company_id=company_id,
|
|
267
|
+
opportunity_id=opportunity_id,
|
|
268
|
+
page_size=page_size,
|
|
269
|
+
page_token=page_token,
|
|
270
|
+
)
|
|
271
|
+
except AffinityError as e:
|
|
272
|
+
raise CLIError(
|
|
273
|
+
f"Failed on chunk {chunk_idx}/{total_chunks} "
|
|
274
|
+
f"({chunk_start.date()} \u2192 {chunk_end.date()}): {e}",
|
|
275
|
+
error_type="api_error",
|
|
276
|
+
exit_code=1,
|
|
277
|
+
) from e
|
|
278
|
+
|
|
279
|
+
for interaction in page.data:
|
|
280
|
+
results.append(interaction)
|
|
281
|
+
if progress and task_id is not None:
|
|
282
|
+
progress.update(task_id, completed=progress_offset + len(results))
|
|
283
|
+
|
|
284
|
+
# Check max_results limit
|
|
285
|
+
if max_results is not None and len(results) >= max_results:
|
|
286
|
+
return ChunkedFetchResult(
|
|
287
|
+
interactions=results[:max_results],
|
|
288
|
+
chunks_processed=chunks_processed,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
page_token = page.next_page_token
|
|
292
|
+
if not page_token:
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
return ChunkedFetchResult(
|
|
296
|
+
interactions=results,
|
|
297
|
+
chunks_processed=chunks_processed,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _fetch_interactions_multi_type(
|
|
302
|
+
client: object, # Affinity client
|
|
303
|
+
*,
|
|
304
|
+
types: list[str],
|
|
305
|
+
start: datetime,
|
|
306
|
+
end: datetime,
|
|
307
|
+
person_id: PersonId | None,
|
|
308
|
+
company_id: CompanyId | None,
|
|
309
|
+
opportunity_id: OpportunityId | None,
|
|
310
|
+
page_size: int | None,
|
|
311
|
+
progress: Progress | None,
|
|
312
|
+
task_id: TaskID | None,
|
|
313
|
+
ndjson_progress: _NDJSONProgress | None = None,
|
|
314
|
+
) -> MultiTypeFetchResult:
|
|
315
|
+
"""Fetch interactions across multiple types, merging results.
|
|
316
|
+
|
|
317
|
+
Note: max_results is NOT applied here - it's applied after sorting in the caller.
|
|
318
|
+
This ensures correct "most recent N" semantics across types.
|
|
319
|
+
"""
|
|
320
|
+
all_interactions: list[Interaction] = []
|
|
321
|
+
type_stats: dict[str, TypeStats] = {}
|
|
322
|
+
is_multi_type = len(types) > 1
|
|
323
|
+
cumulative_count = 0
|
|
324
|
+
|
|
325
|
+
for type_idx, itype in enumerate(types):
|
|
326
|
+
# Build progress message for this type
|
|
327
|
+
desc = f"{itype} ({type_idx + 1}/{len(types)})" if is_multi_type else f"Fetching {itype}"
|
|
328
|
+
|
|
329
|
+
# Update Rich progress (TTY mode)
|
|
330
|
+
if progress and task_id is not None:
|
|
331
|
+
progress.update(task_id, description=desc, completed=cumulative_count)
|
|
332
|
+
|
|
333
|
+
# Emit NDJSON progress (non-TTY mode for MCP)
|
|
334
|
+
if ndjson_progress:
|
|
335
|
+
ndjson_progress.emit(desc, current=cumulative_count)
|
|
336
|
+
|
|
337
|
+
# Fetch this type completely (no max_results - applied after sorting)
|
|
338
|
+
result = _fetch_interactions_chunked(
|
|
339
|
+
client,
|
|
340
|
+
interaction_type=_INTERACTION_TYPE_MAP[itype],
|
|
341
|
+
start=start,
|
|
342
|
+
end=end,
|
|
343
|
+
person_id=person_id,
|
|
344
|
+
company_id=company_id,
|
|
345
|
+
opportunity_id=opportunity_id,
|
|
346
|
+
page_size=page_size,
|
|
347
|
+
max_results=None, # Fetch all - truncate after sorting
|
|
348
|
+
progress=progress,
|
|
349
|
+
task_id=task_id,
|
|
350
|
+
suppress_chunk_description=is_multi_type,
|
|
351
|
+
progress_offset=cumulative_count,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
all_interactions.extend(result.interactions)
|
|
355
|
+
cumulative_count += len(result.interactions)
|
|
356
|
+
type_stats[itype] = TypeStats(
|
|
357
|
+
count=len(result.interactions),
|
|
358
|
+
chunks_processed=result.chunks_processed,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Ensure all requested types appear in stats (even with 0 count)
|
|
362
|
+
for itype in types:
|
|
363
|
+
if itype not in type_stats:
|
|
364
|
+
type_stats[itype] = TypeStats(count=0, chunks_processed=0)
|
|
365
|
+
|
|
366
|
+
return MultiTypeFetchResult(
|
|
367
|
+
interactions=all_interactions,
|
|
368
|
+
type_stats=type_stats,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@category("read")
|
|
373
|
+
@interaction_group.command(name="ls", cls=RichCommand)
|
|
374
|
+
@click.option(
|
|
375
|
+
"--type",
|
|
376
|
+
"-t",
|
|
377
|
+
"interaction_types",
|
|
378
|
+
type=click.Choice([*_ACCEPTED_TYPES, "all"]),
|
|
379
|
+
multiple=True,
|
|
380
|
+
required=True,
|
|
381
|
+
help="Interaction type(s): call, chat-message, email, meeting, or 'all'. Repeatable.",
|
|
382
|
+
)
|
|
383
|
+
@click.option(
|
|
384
|
+
"--after",
|
|
385
|
+
type=str,
|
|
386
|
+
default=None,
|
|
387
|
+
help="Start date (ISO-8601). Mutually exclusive with --days.",
|
|
388
|
+
)
|
|
389
|
+
@click.option(
|
|
390
|
+
"--before",
|
|
391
|
+
type=str,
|
|
392
|
+
default=None,
|
|
393
|
+
help="End date (ISO-8601). Default: now.",
|
|
394
|
+
)
|
|
395
|
+
@click.option(
|
|
396
|
+
"--days",
|
|
397
|
+
"-d",
|
|
398
|
+
type=int,
|
|
399
|
+
default=None,
|
|
400
|
+
help="Fetch last N days. Mutually exclusive with --after.",
|
|
401
|
+
)
|
|
402
|
+
@click.option("--person-id", type=int, default=None, help="Filter by person id.")
|
|
403
|
+
@click.option("--company-id", type=int, default=None, help="Filter by company id.")
|
|
404
|
+
@click.option("--opportunity-id", type=int, default=None, help="Filter by opportunity id.")
|
|
405
|
+
@click.option("--page-size", "-s", type=int, default=None, help="Page size (max 500).")
|
|
406
|
+
@click.option(
|
|
407
|
+
"--max-results", "--limit", "-n", type=int, default=None, help="Stop after N results total."
|
|
408
|
+
)
|
|
409
|
+
@click.option("--csv", "csv_flag", is_flag=True, help="Output as CSV to stdout.")
|
|
410
|
+
@click.option("--csv-bom", is_flag=True, help="Add UTF-8 BOM for Excel compatibility.")
|
|
411
|
+
@output_options
|
|
412
|
+
@click.pass_obj
|
|
413
|
+
def interaction_ls(
|
|
414
|
+
ctx: CLIContext,
|
|
415
|
+
*,
|
|
416
|
+
interaction_types: tuple[str, ...],
|
|
417
|
+
after: str | None,
|
|
418
|
+
before: str | None,
|
|
419
|
+
days: int | None,
|
|
420
|
+
person_id: int | None,
|
|
421
|
+
company_id: int | None,
|
|
422
|
+
opportunity_id: int | None,
|
|
423
|
+
page_size: int | None,
|
|
424
|
+
max_results: int | None,
|
|
425
|
+
csv_flag: bool,
|
|
426
|
+
csv_bom: bool,
|
|
427
|
+
) -> None:
|
|
428
|
+
"""List interactions with automatic date range handling.
|
|
429
|
+
|
|
430
|
+
Requires --type (repeatable) and one entity selector (--person-id, --company-id, or
|
|
431
|
+
--opportunity-id). Multiple types can be specified with -t/--type flags, or use
|
|
432
|
+
--type all to fetch all interaction types.
|
|
433
|
+
|
|
434
|
+
Date range defaults to all time if not specified. Use --days (relative) or
|
|
435
|
+
--after/--before (absolute) to limit the range. Ranges exceeding 1 year are
|
|
436
|
+
automatically split into chunks.
|
|
437
|
+
|
|
438
|
+
Examples:
|
|
439
|
+
|
|
440
|
+
# All interactions ever with a company
|
|
441
|
+
xaffinity interaction ls --type all --company-id 456
|
|
442
|
+
|
|
443
|
+
# Last 30 days of meetings with a person
|
|
444
|
+
xaffinity interaction ls --type meeting --person-id 123 --days 30
|
|
445
|
+
|
|
446
|
+
# Emails and meetings from a specific date range
|
|
447
|
+
xaffinity interaction ls -t email -t meeting --company-id 456 --after 2023-01-01
|
|
448
|
+
|
|
449
|
+
# Last 2 years of calls (auto-chunked)
|
|
450
|
+
xaffinity interaction ls -t call --person-id 789 --days 730
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
|
|
454
|
+
# Validate entity selector
|
|
455
|
+
entity_count = sum(1 for x in [person_id, company_id, opportunity_id] if x is not None)
|
|
456
|
+
if entity_count == 0:
|
|
457
|
+
raise CLIError(
|
|
458
|
+
"Specify --person-id, --company-id, or --opportunity-id.",
|
|
459
|
+
error_type="usage_error",
|
|
460
|
+
exit_code=2,
|
|
461
|
+
)
|
|
462
|
+
if entity_count > 1:
|
|
463
|
+
raise CLIError(
|
|
464
|
+
"Only one entity selector allowed.",
|
|
465
|
+
error_type="usage_error",
|
|
466
|
+
exit_code=2,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Resolve types (expand 'all', deduplicate aliases)
|
|
470
|
+
resolved_types = _resolve_types(interaction_types)
|
|
471
|
+
|
|
472
|
+
# Resolve dates (validates mutual exclusion, defaults to all-time)
|
|
473
|
+
start, end = _resolve_date_range(after, before, days)
|
|
474
|
+
|
|
475
|
+
# CSV mutual exclusion
|
|
476
|
+
if csv_flag and ctx.output == "json":
|
|
477
|
+
raise CLIError(
|
|
478
|
+
"--csv and --json are mutually exclusive.",
|
|
479
|
+
exit_code=2,
|
|
480
|
+
error_type="usage_error",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
client = ctx.get_client(warnings=warnings)
|
|
484
|
+
|
|
485
|
+
# Determine if Rich progress (TTY) should be shown
|
|
486
|
+
show_rich_progress = (
|
|
487
|
+
ctx.progress != "never"
|
|
488
|
+
and not ctx.quiet
|
|
489
|
+
and (ctx.progress == "always" or sys.stderr.isatty())
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# NDJSON progress for non-TTY environments (MCP consumption)
|
|
493
|
+
ndjson_progress = _NDJSONProgress(
|
|
494
|
+
enabled=(ctx.progress != "never" and not ctx.quiet and not sys.stderr.isatty())
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
with ExitStack() as stack:
|
|
498
|
+
progress: Progress | None = None
|
|
499
|
+
task_id: TaskID | None = None
|
|
500
|
+
|
|
501
|
+
if show_rich_progress:
|
|
502
|
+
progress = stack.enter_context(
|
|
503
|
+
Progress(
|
|
504
|
+
TextColumn("{task.description}"),
|
|
505
|
+
BarColumn(),
|
|
506
|
+
TextColumn("{task.completed} rows"),
|
|
507
|
+
TimeElapsedColumn(),
|
|
508
|
+
console=Console(file=sys.stderr),
|
|
509
|
+
transient=True,
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
task_id = progress.add_task("Fetching interactions", total=None)
|
|
513
|
+
|
|
514
|
+
fetch_result = _fetch_interactions_multi_type(
|
|
515
|
+
client,
|
|
516
|
+
types=resolved_types,
|
|
517
|
+
start=start,
|
|
518
|
+
end=end,
|
|
519
|
+
person_id=PersonId(person_id) if person_id else None,
|
|
520
|
+
company_id=CompanyId(company_id) if company_id else None,
|
|
521
|
+
opportunity_id=OpportunityId(opportunity_id) if opportunity_id else None,
|
|
522
|
+
page_size=page_size,
|
|
523
|
+
progress=progress,
|
|
524
|
+
task_id=task_id,
|
|
525
|
+
ndjson_progress=ndjson_progress,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# Emit final summary (NDJSON mode, forced to bypass rate limit)
|
|
529
|
+
total_count = sum(s.count for s in fetch_result.type_stats.values())
|
|
530
|
+
types_with_data = sum(1 for s in fetch_result.type_stats.values() if s.count > 0)
|
|
531
|
+
if types_with_data > 1:
|
|
532
|
+
summary_msg = f"{total_count} interactions across {types_with_data} types"
|
|
533
|
+
else:
|
|
534
|
+
summary_msg = f"{total_count} interactions"
|
|
535
|
+
ndjson_progress.emit(summary_msg, current=total_count, force=True)
|
|
536
|
+
|
|
537
|
+
# Sort by date descending, then by type name, then by id for stability
|
|
538
|
+
sorted_interactions = sorted(
|
|
539
|
+
fetch_result.interactions,
|
|
540
|
+
key=lambda i: (
|
|
541
|
+
i.date or datetime.min.replace(tzinfo=timezone.utc),
|
|
542
|
+
InteractionType(i.type).name, # Alphabetical: CALL < CHAT_MESSAGE < EMAIL < MEETING
|
|
543
|
+
i.id,
|
|
544
|
+
),
|
|
545
|
+
reverse=True,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Apply max_results AFTER sorting (correct "most recent N" semantics)
|
|
549
|
+
if max_results and len(sorted_interactions) > max_results:
|
|
550
|
+
sorted_interactions = sorted_interactions[:max_results]
|
|
551
|
+
warnings.append(f"Results limited to {max_results}. Remove --max-results for all.")
|
|
552
|
+
|
|
553
|
+
# Convert to output format
|
|
554
|
+
results = [_interaction_payload(i) for i in sorted_interactions]
|
|
555
|
+
|
|
556
|
+
# CSV output
|
|
557
|
+
if csv_flag:
|
|
558
|
+
fieldnames = list(results[0].keys()) if results else []
|
|
559
|
+
write_csv_to_stdout(rows=results, fieldnames=fieldnames, bom=csv_bom)
|
|
560
|
+
sys.exit(0)
|
|
561
|
+
|
|
562
|
+
# Build type breakdown for summary (only types with results)
|
|
563
|
+
type_breakdown = {
|
|
564
|
+
itype: stats.count
|
|
565
|
+
for itype, stats in fetch_result.type_stats.items()
|
|
566
|
+
if stats.count > 0
|
|
567
|
+
}
|
|
568
|
+
total_chunks = sum(stats.chunks_processed for stats in fetch_result.type_stats.values())
|
|
569
|
+
|
|
570
|
+
# Build context - types is always an array
|
|
571
|
+
cmd_context = CommandContext(
|
|
572
|
+
name="interaction ls",
|
|
573
|
+
inputs={},
|
|
574
|
+
modifiers={
|
|
575
|
+
"types": resolved_types, # Always array, even for single type
|
|
576
|
+
"start": start.isoformat(),
|
|
577
|
+
"end": end.isoformat(),
|
|
578
|
+
**({"personId": person_id} if person_id else {}),
|
|
579
|
+
**({"companyId": company_id} if company_id else {}),
|
|
580
|
+
**({"opportunityId": opportunity_id} if opportunity_id else {}),
|
|
581
|
+
},
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Build summary for footer display
|
|
585
|
+
summary = ResultSummary(
|
|
586
|
+
total_rows=len(results),
|
|
587
|
+
date_range=DateRange(start=start, end=end),
|
|
588
|
+
type_breakdown=type_breakdown if type_breakdown else None,
|
|
589
|
+
chunks_processed=total_chunks if total_chunks > 0 else None,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
return CommandOutput(
|
|
593
|
+
data=results, # Direct array, not wrapped
|
|
594
|
+
context=cmd_context,
|
|
595
|
+
summary=summary,
|
|
596
|
+
api_called=True,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
run_command(ctx, command="interaction ls", fn=fn)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
@category("read")
|
|
603
|
+
@interaction_group.command(name="get", cls=RichCommand)
|
|
604
|
+
@click.argument("interaction_id", type=int)
|
|
605
|
+
@click.option(
|
|
606
|
+
"--type",
|
|
607
|
+
"-t",
|
|
608
|
+
"interaction_type",
|
|
609
|
+
type=click.Choice(sorted(_INTERACTION_TYPE_MAP.keys())),
|
|
610
|
+
required=True,
|
|
611
|
+
help="Interaction type (required by API).",
|
|
612
|
+
)
|
|
613
|
+
@output_options
|
|
614
|
+
@click.pass_obj
|
|
615
|
+
def interaction_get(ctx: CLIContext, interaction_id: int, *, interaction_type: str) -> None:
|
|
616
|
+
"""Get an interaction by id.
|
|
617
|
+
|
|
618
|
+
The --type flag is required because the Affinity API stores interactions
|
|
619
|
+
in type-specific tables.
|
|
620
|
+
|
|
621
|
+
Examples:
|
|
622
|
+
|
|
623
|
+
- `xaffinity interaction get 123 --type meeting`
|
|
624
|
+
- `xaffinity interaction get 456 -t email`
|
|
625
|
+
"""
|
|
626
|
+
|
|
627
|
+
def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
|
|
628
|
+
parsed_type = parse_choice(
|
|
629
|
+
interaction_type,
|
|
630
|
+
_INTERACTION_TYPE_MAP,
|
|
631
|
+
label="interaction type",
|
|
632
|
+
)
|
|
633
|
+
if parsed_type is None:
|
|
634
|
+
raise CLIError("Missing interaction type.", error_type="usage_error", exit_code=2)
|
|
635
|
+
client = ctx.get_client(warnings=warnings)
|
|
636
|
+
interaction = client.interactions.get(InteractionId(interaction_id), parsed_type)
|
|
637
|
+
|
|
638
|
+
cmd_context = CommandContext(
|
|
639
|
+
name="interaction get",
|
|
640
|
+
inputs={"interactionId": interaction_id},
|
|
641
|
+
modifiers={"type": interaction_type},
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return CommandOutput(
|
|
645
|
+
data={"interaction": _interaction_payload(interaction)},
|
|
646
|
+
context=cmd_context,
|
|
647
|
+
api_called=True,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
run_command(ctx, command="interaction get", fn=fn)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@category("write")
|
|
654
|
+
@interaction_group.command(name="create", cls=RichCommand)
|
|
655
|
+
@click.option(
|
|
656
|
+
"--type",
|
|
657
|
+
"-t",
|
|
658
|
+
"interaction_type",
|
|
659
|
+
type=click.Choice(sorted(_INTERACTION_TYPE_MAP.keys())),
|
|
660
|
+
required=True,
|
|
661
|
+
help="Interaction type (required).",
|
|
662
|
+
)
|
|
663
|
+
@click.option("--person-id", "person_ids", multiple=True, type=int, help="Person id.")
|
|
664
|
+
@click.option("--content", type=str, required=True, help="Interaction content.")
|
|
665
|
+
@click.option("--date", type=str, required=True, help="Interaction date (ISO-8601).")
|
|
666
|
+
@click.option(
|
|
667
|
+
"--direction",
|
|
668
|
+
type=click.Choice(sorted(_INTERACTION_DIRECTION_MAP.keys())),
|
|
669
|
+
default=None,
|
|
670
|
+
help="Direction (incoming, outgoing).",
|
|
671
|
+
)
|
|
672
|
+
@output_options
|
|
673
|
+
@click.pass_obj
|
|
674
|
+
def interaction_create(
|
|
675
|
+
ctx: CLIContext,
|
|
676
|
+
*,
|
|
677
|
+
interaction_type: str,
|
|
678
|
+
person_ids: tuple[int, ...],
|
|
679
|
+
content: str,
|
|
680
|
+
date: str,
|
|
681
|
+
direction: str | None,
|
|
682
|
+
) -> None:
|
|
683
|
+
"""Create an interaction."""
|
|
684
|
+
|
|
685
|
+
def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
|
|
686
|
+
if not person_ids:
|
|
687
|
+
raise CLIError(
|
|
688
|
+
"At least one --person-id is required.",
|
|
689
|
+
error_type="usage_error",
|
|
690
|
+
exit_code=2,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
parsed_type = parse_choice(
|
|
694
|
+
interaction_type,
|
|
695
|
+
_INTERACTION_TYPE_MAP,
|
|
696
|
+
label="interaction type",
|
|
697
|
+
)
|
|
698
|
+
if parsed_type is None:
|
|
699
|
+
raise CLIError("Missing interaction type.", error_type="usage_error", exit_code=2)
|
|
700
|
+
parsed_direction = parse_choice(direction, _INTERACTION_DIRECTION_MAP, label="direction")
|
|
701
|
+
date_value = parse_iso_datetime(date, label="date")
|
|
702
|
+
|
|
703
|
+
client = ctx.get_client(warnings=warnings)
|
|
704
|
+
interaction = client.interactions.create(
|
|
705
|
+
InteractionCreate(
|
|
706
|
+
type=parsed_type,
|
|
707
|
+
person_ids=[PersonId(pid) for pid in person_ids],
|
|
708
|
+
content=content,
|
|
709
|
+
date=date_value,
|
|
710
|
+
direction=parsed_direction,
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
# Build CommandContext for interaction create
|
|
715
|
+
ctx_modifiers: dict[str, object] = {
|
|
716
|
+
"type": interaction_type,
|
|
717
|
+
"personIds": list(person_ids),
|
|
718
|
+
"date": date,
|
|
719
|
+
}
|
|
720
|
+
if direction:
|
|
721
|
+
ctx_modifiers["direction"] = direction
|
|
722
|
+
|
|
723
|
+
cmd_context = CommandContext(
|
|
724
|
+
name="interaction create",
|
|
725
|
+
inputs={"type": interaction_type},
|
|
726
|
+
modifiers=ctx_modifiers,
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
return CommandOutput(
|
|
730
|
+
data={"interaction": _interaction_payload(interaction)},
|
|
731
|
+
context=cmd_context,
|
|
732
|
+
api_called=True,
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
run_command(ctx, command="interaction create", fn=fn)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@category("write")
|
|
739
|
+
@interaction_group.command(name="update", cls=RichCommand)
|
|
740
|
+
@click.argument("interaction_id", type=int)
|
|
741
|
+
@click.option(
|
|
742
|
+
"--type",
|
|
743
|
+
"-t",
|
|
744
|
+
"interaction_type",
|
|
745
|
+
type=click.Choice(sorted(_INTERACTION_TYPE_MAP.keys())),
|
|
746
|
+
required=True,
|
|
747
|
+
help="Interaction type (required by API).",
|
|
748
|
+
)
|
|
749
|
+
@click.option("--person-id", "person_ids", multiple=True, type=int, help="Person id.")
|
|
750
|
+
@click.option("--content", type=str, default=None, help="Interaction content.")
|
|
751
|
+
@click.option("--date", type=str, default=None, help="Interaction date (ISO-8601).")
|
|
752
|
+
@click.option(
|
|
753
|
+
"--direction",
|
|
754
|
+
type=click.Choice(sorted(_INTERACTION_DIRECTION_MAP.keys())),
|
|
755
|
+
default=None,
|
|
756
|
+
help="Direction (incoming, outgoing).",
|
|
757
|
+
)
|
|
758
|
+
@output_options
|
|
759
|
+
@click.pass_obj
|
|
760
|
+
def interaction_update(
|
|
761
|
+
ctx: CLIContext,
|
|
762
|
+
interaction_id: int,
|
|
763
|
+
*,
|
|
764
|
+
interaction_type: str,
|
|
765
|
+
person_ids: tuple[int, ...],
|
|
766
|
+
content: str | None,
|
|
767
|
+
date: str | None,
|
|
768
|
+
direction: str | None,
|
|
769
|
+
) -> None:
|
|
770
|
+
"""Update an interaction."""
|
|
771
|
+
|
|
772
|
+
def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
|
|
773
|
+
parsed_type = parse_choice(
|
|
774
|
+
interaction_type,
|
|
775
|
+
_INTERACTION_TYPE_MAP,
|
|
776
|
+
label="interaction type",
|
|
777
|
+
)
|
|
778
|
+
if parsed_type is None:
|
|
779
|
+
raise CLIError("Missing interaction type.", error_type="usage_error", exit_code=2)
|
|
780
|
+
|
|
781
|
+
parsed_direction = parse_choice(direction, _INTERACTION_DIRECTION_MAP, label="direction")
|
|
782
|
+
date_value = parse_iso_datetime(date, label="date") if date else None
|
|
783
|
+
|
|
784
|
+
if not (person_ids or content or date_value or parsed_direction is not None):
|
|
785
|
+
raise CLIError(
|
|
786
|
+
"Provide at least one field to update.",
|
|
787
|
+
error_type="usage_error",
|
|
788
|
+
exit_code=2,
|
|
789
|
+
hint="Use --person-id, --content, --date, or --direction.",
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
client = ctx.get_client(warnings=warnings)
|
|
793
|
+
interaction = client.interactions.update(
|
|
794
|
+
InteractionId(interaction_id),
|
|
795
|
+
parsed_type,
|
|
796
|
+
InteractionUpdate(
|
|
797
|
+
person_ids=[PersonId(pid) for pid in person_ids] if person_ids else None,
|
|
798
|
+
content=content,
|
|
799
|
+
date=date_value,
|
|
800
|
+
direction=parsed_direction,
|
|
801
|
+
),
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
# Build CommandContext for interaction update
|
|
805
|
+
ctx_modifiers: dict[str, object] = {"type": interaction_type}
|
|
806
|
+
if person_ids:
|
|
807
|
+
ctx_modifiers["personIds"] = list(person_ids)
|
|
808
|
+
if content:
|
|
809
|
+
ctx_modifiers["content"] = content
|
|
810
|
+
if date:
|
|
811
|
+
ctx_modifiers["date"] = date
|
|
812
|
+
if direction:
|
|
813
|
+
ctx_modifiers["direction"] = direction
|
|
814
|
+
|
|
815
|
+
cmd_context = CommandContext(
|
|
816
|
+
name="interaction update",
|
|
817
|
+
inputs={"interactionId": interaction_id},
|
|
818
|
+
modifiers=ctx_modifiers,
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
return CommandOutput(
|
|
822
|
+
data={"interaction": _interaction_payload(interaction)},
|
|
823
|
+
context=cmd_context,
|
|
824
|
+
api_called=True,
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
run_command(ctx, command="interaction update", fn=fn)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@category("write")
|
|
831
|
+
@destructive
|
|
832
|
+
@interaction_group.command(name="delete", cls=RichCommand)
|
|
833
|
+
@click.argument("interaction_id", type=int)
|
|
834
|
+
@click.option(
|
|
835
|
+
"--type",
|
|
836
|
+
"-t",
|
|
837
|
+
"interaction_type",
|
|
838
|
+
type=click.Choice(sorted(_INTERACTION_TYPE_MAP.keys())),
|
|
839
|
+
required=True,
|
|
840
|
+
help="Interaction type (required by API).",
|
|
841
|
+
)
|
|
842
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
|
|
843
|
+
@output_options
|
|
844
|
+
@click.pass_obj
|
|
845
|
+
def interaction_delete(
|
|
846
|
+
ctx: CLIContext, interaction_id: int, *, interaction_type: str, yes: bool
|
|
847
|
+
) -> None:
|
|
848
|
+
"""Delete an interaction."""
|
|
849
|
+
if not yes:
|
|
850
|
+
click.confirm(f"Delete interaction {interaction_id}?", abort=True)
|
|
851
|
+
|
|
852
|
+
def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
|
|
853
|
+
parsed_type = parse_choice(
|
|
854
|
+
interaction_type,
|
|
855
|
+
_INTERACTION_TYPE_MAP,
|
|
856
|
+
label="interaction type",
|
|
857
|
+
)
|
|
858
|
+
if parsed_type is None:
|
|
859
|
+
raise CLIError("Missing interaction type.", error_type="usage_error", exit_code=2)
|
|
860
|
+
client = ctx.get_client(warnings=warnings)
|
|
861
|
+
success = client.interactions.delete(InteractionId(interaction_id), parsed_type)
|
|
862
|
+
|
|
863
|
+
cmd_context = CommandContext(
|
|
864
|
+
name="interaction delete",
|
|
865
|
+
inputs={"interactionId": interaction_id},
|
|
866
|
+
modifiers={"type": interaction_type},
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
return CommandOutput(
|
|
870
|
+
data={"success": success},
|
|
871
|
+
context=cmd_context,
|
|
872
|
+
api_called=True,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
run_command(ctx, command="interaction delete", fn=fn)
|