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,331 @@
|
|
|
1
|
+
"""Output formatters for query results.
|
|
2
|
+
|
|
3
|
+
Formats query results as JSON, JSONL, Markdown, TOON, CSV, or table.
|
|
4
|
+
This module is CLI-only and NOT part of the public SDK API.
|
|
5
|
+
|
|
6
|
+
Supported output formats:
|
|
7
|
+
- JSON: Full structure with data, included, pagination, meta
|
|
8
|
+
- JSONL: One JSON object per line (data rows only)
|
|
9
|
+
- Markdown: GitHub-flavored markdown table (data rows only)
|
|
10
|
+
- TOON: Token-Optimized Object Notation (data rows only)
|
|
11
|
+
- CSV: Comma-separated values (data rows only)
|
|
12
|
+
- Table: Rich terminal tables
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
|
|
23
|
+
from ..formatters import OutputFormat, format_data, format_jsonl
|
|
24
|
+
from .models import ExecutionPlan, QueryResult
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# JSON Output
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_json(
|
|
34
|
+
result: QueryResult,
|
|
35
|
+
*,
|
|
36
|
+
pretty: bool = False,
|
|
37
|
+
include_meta: bool = False,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Format query result as JSON.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
result: Query result
|
|
43
|
+
pretty: If True, pretty-print with indentation
|
|
44
|
+
include_meta: If True, include metadata in output
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
JSON string
|
|
48
|
+
"""
|
|
49
|
+
output: dict[str, Any] = {"data": result.data}
|
|
50
|
+
|
|
51
|
+
if result.included:
|
|
52
|
+
output["included"] = result.included
|
|
53
|
+
|
|
54
|
+
if include_meta:
|
|
55
|
+
meta: dict[str, Any] = {}
|
|
56
|
+
# Add standardized summary (using camelCase aliases for consistency)
|
|
57
|
+
if result.summary:
|
|
58
|
+
meta["summary"] = result.summary.model_dump(by_alias=True, exclude_none=True)
|
|
59
|
+
# Add additional execution metadata
|
|
60
|
+
if result.meta:
|
|
61
|
+
meta.update(result.meta)
|
|
62
|
+
output["meta"] = meta
|
|
63
|
+
|
|
64
|
+
if result.pagination:
|
|
65
|
+
output["pagination"] = result.pagination
|
|
66
|
+
|
|
67
|
+
indent = 2 if pretty else None
|
|
68
|
+
return json.dumps(output, indent=indent, default=str)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# =============================================================================
|
|
72
|
+
# Table Output (Rich Table)
|
|
73
|
+
# =============================================================================
|
|
74
|
+
|
|
75
|
+
# Columns to exclude from table output by default (following CLI conventions)
|
|
76
|
+
# These are complex nested structures that don't display well in tables
|
|
77
|
+
# Use --json to see full data including these columns
|
|
78
|
+
_EXCLUDED_TABLE_COLUMNS = frozenset(
|
|
79
|
+
{
|
|
80
|
+
"fields", # Custom field values - use --json or entity get --all-fields
|
|
81
|
+
"interaction_dates", # Complex nested dates
|
|
82
|
+
"list_entries", # List entry associations
|
|
83
|
+
"interactions", # Null unless explicitly loaded
|
|
84
|
+
"company_ids", # Empty array unless relationships loaded
|
|
85
|
+
"opportunity_ids", # Empty array unless relationships loaded
|
|
86
|
+
"current_company_ids", # Empty array unless relationships loaded
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def format_table(result: QueryResult) -> str: # pragma: no cover
|
|
92
|
+
"""Format query result as a Rich table (matching CLI conventions).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
result: Query result
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Rendered table string
|
|
99
|
+
"""
|
|
100
|
+
# Use the CLI's standard table rendering
|
|
101
|
+
from ..render import _render_summary_footer, _table_from_rows
|
|
102
|
+
|
|
103
|
+
if not result.data:
|
|
104
|
+
return "No results."
|
|
105
|
+
|
|
106
|
+
# Filter out excluded columns (following CLI convention - ls commands don't show fields)
|
|
107
|
+
filtered_data = [
|
|
108
|
+
{k: v for k, v in row.items() if k not in _EXCLUDED_TABLE_COLUMNS} for row in result.data
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# Build Rich table using CLI's standard function
|
|
112
|
+
table, omitted = _table_from_rows(filtered_data)
|
|
113
|
+
|
|
114
|
+
# Render to string
|
|
115
|
+
console = Console(force_terminal=False, width=None)
|
|
116
|
+
with console.capture() as capture:
|
|
117
|
+
console.print(table)
|
|
118
|
+
|
|
119
|
+
output = capture.get()
|
|
120
|
+
|
|
121
|
+
# Add standardized summary footer
|
|
122
|
+
footer_parts: list[str] = []
|
|
123
|
+
if result.summary:
|
|
124
|
+
footer = _render_summary_footer(result.summary)
|
|
125
|
+
if footer:
|
|
126
|
+
footer_parts.append(footer.plain)
|
|
127
|
+
|
|
128
|
+
# Column omission notice
|
|
129
|
+
if omitted > 0:
|
|
130
|
+
footer_parts.append(f"({omitted} columns hidden — use --json for full data)")
|
|
131
|
+
|
|
132
|
+
return output + "\n".join(footer_parts)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# =============================================================================
|
|
136
|
+
# Dry-Run Output
|
|
137
|
+
# =============================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_dry_run(plan: ExecutionPlan, *, verbose: bool = False) -> str: # pragma: no cover
|
|
141
|
+
"""Format execution plan for dry-run output.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
plan: Execution plan
|
|
145
|
+
verbose: If True, show detailed API call breakdown
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Formatted plan string
|
|
149
|
+
"""
|
|
150
|
+
lines: list[str] = []
|
|
151
|
+
|
|
152
|
+
lines.append("Query Execution Plan")
|
|
153
|
+
lines.append("=" * 40)
|
|
154
|
+
lines.append("")
|
|
155
|
+
|
|
156
|
+
# Query summary
|
|
157
|
+
lines.append("Query:")
|
|
158
|
+
lines.append(f" $version: {plan.version}")
|
|
159
|
+
lines.append(f" from: {plan.query.from_}")
|
|
160
|
+
|
|
161
|
+
if plan.query.where is not None:
|
|
162
|
+
lines.append(" where: <filter condition>")
|
|
163
|
+
|
|
164
|
+
if plan.query.include is not None:
|
|
165
|
+
lines.append(f" include: {', '.join(plan.query.include)}")
|
|
166
|
+
|
|
167
|
+
if plan.query.order_by is not None:
|
|
168
|
+
order_fields = [ob.field or "expr" for ob in plan.query.order_by]
|
|
169
|
+
lines.append(f" orderBy: {', '.join(order_fields)}")
|
|
170
|
+
|
|
171
|
+
if plan.query.limit is not None:
|
|
172
|
+
lines.append(f" limit: {plan.query.limit}")
|
|
173
|
+
|
|
174
|
+
lines.append("")
|
|
175
|
+
|
|
176
|
+
# Execution summary
|
|
177
|
+
lines.append("Execution Summary:")
|
|
178
|
+
lines.append(f" Total steps: {len(plan.steps)}")
|
|
179
|
+
lines.append(f" Estimated API calls: {plan.total_api_calls}")
|
|
180
|
+
|
|
181
|
+
if plan.estimated_records_fetched is not None:
|
|
182
|
+
lines.append(f" Estimated records: {plan.estimated_records_fetched}")
|
|
183
|
+
|
|
184
|
+
if plan.estimated_memory_mb is not None:
|
|
185
|
+
lines.append(f" Estimated memory: {plan.estimated_memory_mb:.1f} MB")
|
|
186
|
+
|
|
187
|
+
lines.append("")
|
|
188
|
+
|
|
189
|
+
# Steps
|
|
190
|
+
lines.append("Execution Steps:")
|
|
191
|
+
for step in plan.steps:
|
|
192
|
+
status = "[client]" if step.is_client_side else f"[~{step.estimated_api_calls} calls]"
|
|
193
|
+
lines.append(f" {step.step_id}. {step.description} {status}")
|
|
194
|
+
|
|
195
|
+
if verbose:
|
|
196
|
+
if step.depends_on:
|
|
197
|
+
lines.append(f" depends on: step {', '.join(map(str, step.depends_on))}")
|
|
198
|
+
if step.filter_pushdown:
|
|
199
|
+
lines.append(f" pushdown: {step.pushdown_filter}")
|
|
200
|
+
for warning in step.warnings:
|
|
201
|
+
lines.append(f" [!] {warning}")
|
|
202
|
+
|
|
203
|
+
lines.append("")
|
|
204
|
+
|
|
205
|
+
# Warnings
|
|
206
|
+
if plan.warnings:
|
|
207
|
+
lines.append("Warnings:")
|
|
208
|
+
for warning in plan.warnings:
|
|
209
|
+
lines.append(f" [!] {warning}")
|
|
210
|
+
lines.append("")
|
|
211
|
+
|
|
212
|
+
# Recommendations
|
|
213
|
+
if plan.recommendations:
|
|
214
|
+
lines.append("Recommendations:")
|
|
215
|
+
for rec in plan.recommendations:
|
|
216
|
+
lines.append(f" - {rec}")
|
|
217
|
+
lines.append("")
|
|
218
|
+
|
|
219
|
+
# Assumptions (always show in verbose, or when includes present)
|
|
220
|
+
has_includes = plan.query.include is not None and len(plan.query.include) > 0
|
|
221
|
+
if verbose or has_includes:
|
|
222
|
+
lines.append("Assumptions:")
|
|
223
|
+
if plan.query.limit is not None:
|
|
224
|
+
lines.append(f" - Record count: {plan.query.limit} (from limit)")
|
|
225
|
+
else:
|
|
226
|
+
lines.append(f" - Record count: {plan.estimated_records_fetched} (heuristic estimate)")
|
|
227
|
+
if plan.query.where is not None:
|
|
228
|
+
lines.append(" - Filter selectivity: 50% (heuristic)")
|
|
229
|
+
if has_includes:
|
|
230
|
+
lines.append(" - Include calls: 1 API call per parent record (N+1)")
|
|
231
|
+
lines.append(" - Actual counts may vary; use --dry-run to preview before execution")
|
|
232
|
+
lines.append("")
|
|
233
|
+
|
|
234
|
+
return "\n".join(lines)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def format_dry_run_json(plan: ExecutionPlan) -> str:
|
|
238
|
+
"""Format execution plan as JSON for MCP.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
plan: Execution plan
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
JSON string
|
|
245
|
+
"""
|
|
246
|
+
output = {
|
|
247
|
+
"version": plan.version,
|
|
248
|
+
"query": {
|
|
249
|
+
"from": plan.query.from_,
|
|
250
|
+
"where": plan.query.where.model_dump() if plan.query.where else None,
|
|
251
|
+
"include": plan.query.include,
|
|
252
|
+
"orderBy": [ob.model_dump() for ob in plan.query.order_by]
|
|
253
|
+
if plan.query.order_by
|
|
254
|
+
else None,
|
|
255
|
+
"limit": plan.query.limit,
|
|
256
|
+
},
|
|
257
|
+
"execution": {
|
|
258
|
+
"totalSteps": len(plan.steps),
|
|
259
|
+
"estimatedApiCalls": plan.total_api_calls,
|
|
260
|
+
"estimatedRecords": plan.estimated_records_fetched,
|
|
261
|
+
"estimatedMemoryMb": plan.estimated_memory_mb,
|
|
262
|
+
},
|
|
263
|
+
"steps": [
|
|
264
|
+
{
|
|
265
|
+
"stepId": step.step_id,
|
|
266
|
+
"operation": step.operation,
|
|
267
|
+
"description": step.description,
|
|
268
|
+
"estimatedApiCalls": step.estimated_api_calls,
|
|
269
|
+
"isClientSide": step.is_client_side,
|
|
270
|
+
"dependsOn": step.depends_on,
|
|
271
|
+
"warnings": step.warnings,
|
|
272
|
+
}
|
|
273
|
+
for step in plan.steps
|
|
274
|
+
],
|
|
275
|
+
"warnings": plan.warnings,
|
|
276
|
+
"recommendations": plan.recommendations,
|
|
277
|
+
"hasExpensiveOperations": plan.has_expensive_operations,
|
|
278
|
+
"requiresFullScan": plan.requires_full_scan,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return json.dumps(output, indent=2, default=str)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# =============================================================================
|
|
285
|
+
# Unified Format Output
|
|
286
|
+
# =============================================================================
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def format_query_result(
|
|
290
|
+
result: QueryResult,
|
|
291
|
+
format: OutputFormat,
|
|
292
|
+
*,
|
|
293
|
+
pretty: bool = False,
|
|
294
|
+
include_meta: bool = False,
|
|
295
|
+
) -> str:
|
|
296
|
+
"""Format query result with full structure support.
|
|
297
|
+
|
|
298
|
+
For formats that only support flat data (markdown, toon, csv),
|
|
299
|
+
the included/pagination/meta are omitted with a warning.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
result: Query result to format
|
|
303
|
+
format: Output format (json, jsonl, markdown, toon, csv)
|
|
304
|
+
pretty: Pretty-print JSON output
|
|
305
|
+
include_meta: Include metadata in JSON output
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Formatted string
|
|
309
|
+
"""
|
|
310
|
+
if format == "json":
|
|
311
|
+
# Full structure
|
|
312
|
+
return format_json(result, pretty=pretty, include_meta=include_meta)
|
|
313
|
+
|
|
314
|
+
if format == "jsonl":
|
|
315
|
+
# Data rows only, one per line
|
|
316
|
+
return format_jsonl(result.data or [])
|
|
317
|
+
|
|
318
|
+
if format in ("markdown", "toon", "csv"):
|
|
319
|
+
# Data only - warn if losing information
|
|
320
|
+
if result.included:
|
|
321
|
+
logger.warning(
|
|
322
|
+
"Included data omitted in %s output (use --output json to see included entities)",
|
|
323
|
+
format,
|
|
324
|
+
)
|
|
325
|
+
fieldnames = list(result.data[0].keys()) if result.data else []
|
|
326
|
+
return format_data(result.data or [], format, fieldnames=fieldnames)
|
|
327
|
+
|
|
328
|
+
if format == "table":
|
|
329
|
+
return format_table(result)
|
|
330
|
+
|
|
331
|
+
raise ValueError(f"Unknown format: {format}")
|