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,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}")