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,444 @@
|
|
|
1
|
+
"""CLI query command.
|
|
2
|
+
|
|
3
|
+
Executes structured queries against Affinity data.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ..click_compat import RichCommand, click
|
|
15
|
+
from ..context import CLIContext
|
|
16
|
+
from ..decorators import category, progress_capable
|
|
17
|
+
from ..errors import CLIError
|
|
18
|
+
from ..options import output_options
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# CLI Command
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@category("read")
|
|
26
|
+
@progress_capable
|
|
27
|
+
@click.command(name="query", cls=RichCommand)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--file",
|
|
30
|
+
"-f",
|
|
31
|
+
"file_path",
|
|
32
|
+
type=click.Path(exists=True, path_type=Path), # type: ignore[type-var]
|
|
33
|
+
help="Read query from JSON file.",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--query",
|
|
37
|
+
"query_str",
|
|
38
|
+
type=str,
|
|
39
|
+
help="Inline JSON query string.",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--query-version",
|
|
43
|
+
type=str,
|
|
44
|
+
help="Override $version in query (e.g., '1.0').",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--dry-run",
|
|
48
|
+
is_flag=True,
|
|
49
|
+
help="Show execution plan without running.",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--dry-run-verbose",
|
|
53
|
+
is_flag=True,
|
|
54
|
+
help="Show detailed plan with API call breakdown.",
|
|
55
|
+
)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--confirm",
|
|
58
|
+
is_flag=True,
|
|
59
|
+
help="Require confirmation before expensive operations.",
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--max-records",
|
|
63
|
+
type=int,
|
|
64
|
+
default=10000,
|
|
65
|
+
show_default=True,
|
|
66
|
+
help="Safety limit on total records fetched.",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--timeout",
|
|
70
|
+
type=float,
|
|
71
|
+
default=300.0,
|
|
72
|
+
show_default=True,
|
|
73
|
+
help="Overall timeout in seconds.",
|
|
74
|
+
)
|
|
75
|
+
@click.option(
|
|
76
|
+
"--csv",
|
|
77
|
+
"csv_flag",
|
|
78
|
+
is_flag=True,
|
|
79
|
+
help="Output as CSV.",
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--csv-bom",
|
|
83
|
+
is_flag=True,
|
|
84
|
+
help="Add UTF-8 BOM for Excel (use with redirection: --csv --csv-bom > file.csv).",
|
|
85
|
+
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--pretty",
|
|
88
|
+
is_flag=True,
|
|
89
|
+
help="Pretty-print JSON output.",
|
|
90
|
+
)
|
|
91
|
+
@click.option(
|
|
92
|
+
"--include-meta",
|
|
93
|
+
is_flag=True,
|
|
94
|
+
help="Include execution metadata in output.",
|
|
95
|
+
)
|
|
96
|
+
@click.option(
|
|
97
|
+
"--quiet",
|
|
98
|
+
"-q",
|
|
99
|
+
is_flag=True,
|
|
100
|
+
help="Suppress progress output.",
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--verbose",
|
|
104
|
+
"-v",
|
|
105
|
+
is_flag=True,
|
|
106
|
+
help="Show detailed progress.",
|
|
107
|
+
)
|
|
108
|
+
@output_options
|
|
109
|
+
@click.pass_obj
|
|
110
|
+
def query_cmd(
|
|
111
|
+
ctx: CLIContext,
|
|
112
|
+
file_path: Path | None,
|
|
113
|
+
query_str: str | None,
|
|
114
|
+
query_version: str | None,
|
|
115
|
+
dry_run: bool,
|
|
116
|
+
dry_run_verbose: bool,
|
|
117
|
+
confirm: bool,
|
|
118
|
+
max_records: int,
|
|
119
|
+
timeout: float,
|
|
120
|
+
csv_flag: bool,
|
|
121
|
+
csv_bom: bool,
|
|
122
|
+
pretty: bool,
|
|
123
|
+
include_meta: bool,
|
|
124
|
+
quiet: bool,
|
|
125
|
+
verbose: bool,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Execute a structured query against Affinity data.
|
|
128
|
+
|
|
129
|
+
The query can be provided via --file, --query, or piped from stdin.
|
|
130
|
+
|
|
131
|
+
\b
|
|
132
|
+
Examples:
|
|
133
|
+
# From file
|
|
134
|
+
xaffinity query --file query.json
|
|
135
|
+
|
|
136
|
+
# Inline JSON
|
|
137
|
+
xaffinity query --query '{"from": "persons", "limit": 10}'
|
|
138
|
+
|
|
139
|
+
# Dry-run to preview execution plan
|
|
140
|
+
xaffinity query --file query.json --dry-run
|
|
141
|
+
|
|
142
|
+
# CSV output
|
|
143
|
+
xaffinity query --file query.json --csv
|
|
144
|
+
|
|
145
|
+
# JSON output
|
|
146
|
+
xaffinity query --file query.json --json
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
_query_cmd_impl(
|
|
150
|
+
ctx=ctx,
|
|
151
|
+
file_path=file_path,
|
|
152
|
+
query_str=query_str,
|
|
153
|
+
query_version=query_version,
|
|
154
|
+
dry_run=dry_run,
|
|
155
|
+
dry_run_verbose=dry_run_verbose,
|
|
156
|
+
confirm=confirm,
|
|
157
|
+
max_records=max_records,
|
|
158
|
+
timeout=timeout,
|
|
159
|
+
csv_flag=csv_flag,
|
|
160
|
+
csv_bom=csv_bom,
|
|
161
|
+
pretty=pretty,
|
|
162
|
+
include_meta=include_meta,
|
|
163
|
+
quiet=quiet,
|
|
164
|
+
verbose=verbose,
|
|
165
|
+
)
|
|
166
|
+
except CLIError as e:
|
|
167
|
+
# Display error cleanly without traceback
|
|
168
|
+
click.echo(f"Error: {e.message}", err=True)
|
|
169
|
+
if e.hint:
|
|
170
|
+
click.echo(f"Hint: {e.hint}", err=True)
|
|
171
|
+
raise click.exceptions.Exit(e.exit_code) from None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _query_cmd_impl(
|
|
175
|
+
*,
|
|
176
|
+
ctx: CLIContext,
|
|
177
|
+
file_path: Path | None,
|
|
178
|
+
query_str: str | None,
|
|
179
|
+
query_version: str | None,
|
|
180
|
+
dry_run: bool,
|
|
181
|
+
dry_run_verbose: bool,
|
|
182
|
+
confirm: bool,
|
|
183
|
+
max_records: int,
|
|
184
|
+
timeout: float,
|
|
185
|
+
csv_flag: bool,
|
|
186
|
+
csv_bom: bool,
|
|
187
|
+
pretty: bool,
|
|
188
|
+
include_meta: bool,
|
|
189
|
+
quiet: bool,
|
|
190
|
+
verbose: bool,
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Internal implementation of query command."""
|
|
193
|
+
from affinity.cli.query import (
|
|
194
|
+
QueryExecutionError,
|
|
195
|
+
QueryInterruptedError,
|
|
196
|
+
QueryParseError,
|
|
197
|
+
QuerySafetyLimitError,
|
|
198
|
+
QueryTimeoutError,
|
|
199
|
+
QueryValidationError,
|
|
200
|
+
create_planner,
|
|
201
|
+
parse_query,
|
|
202
|
+
)
|
|
203
|
+
from affinity.cli.query.executor import QueryExecutor
|
|
204
|
+
from affinity.cli.query.output import (
|
|
205
|
+
format_dry_run,
|
|
206
|
+
format_dry_run_json,
|
|
207
|
+
format_json,
|
|
208
|
+
format_table,
|
|
209
|
+
)
|
|
210
|
+
from affinity.cli.query.progress import RichQueryProgress, create_progress_callback
|
|
211
|
+
|
|
212
|
+
# Check mutual exclusivity: --csv and --json
|
|
213
|
+
if csv_flag and ctx.output == "json":
|
|
214
|
+
raise CLIError(
|
|
215
|
+
"--csv and --json are mutually exclusive.",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Get query input
|
|
219
|
+
query_dict = _get_query_input(file_path, query_str)
|
|
220
|
+
|
|
221
|
+
# Parse and validate query
|
|
222
|
+
try:
|
|
223
|
+
parse_result = parse_query(query_dict, version_override=query_version)
|
|
224
|
+
except (QueryParseError, QueryValidationError) as e:
|
|
225
|
+
raise CLIError(f"Query validation failed: {e}") from None
|
|
226
|
+
|
|
227
|
+
query = parse_result.query
|
|
228
|
+
|
|
229
|
+
# Show parsing warnings
|
|
230
|
+
if parse_result.warnings and not quiet:
|
|
231
|
+
for warning in parse_result.warnings:
|
|
232
|
+
click.echo(f"[warning] {warning}", err=True)
|
|
233
|
+
|
|
234
|
+
# Create execution plan
|
|
235
|
+
planner = create_planner(max_records=max_records)
|
|
236
|
+
try:
|
|
237
|
+
plan = planner.plan(query)
|
|
238
|
+
except QueryValidationError as e:
|
|
239
|
+
raise CLIError(f"Query planning failed: {e}") from None
|
|
240
|
+
|
|
241
|
+
# Dry-run mode
|
|
242
|
+
if dry_run or dry_run_verbose:
|
|
243
|
+
if ctx.output == "json":
|
|
244
|
+
click.echo(format_dry_run_json(plan))
|
|
245
|
+
else:
|
|
246
|
+
click.echo(format_dry_run(plan, verbose=dry_run_verbose))
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Check for expensive operations
|
|
250
|
+
if (
|
|
251
|
+
plan.has_expensive_operations
|
|
252
|
+
and confirm
|
|
253
|
+
and not click.confirm(
|
|
254
|
+
f"This query will make approximately {plan.total_api_calls} API calls. Continue?"
|
|
255
|
+
)
|
|
256
|
+
):
|
|
257
|
+
raise CLIError("Query cancelled by user.")
|
|
258
|
+
|
|
259
|
+
# Show warnings
|
|
260
|
+
if plan.warnings and not quiet:
|
|
261
|
+
for warning in plan.warnings:
|
|
262
|
+
click.echo(f"[warning] {warning}", err=True)
|
|
263
|
+
|
|
264
|
+
# Resolve client settings before async execution
|
|
265
|
+
warnings_list: list[str] = []
|
|
266
|
+
settings = ctx.resolve_client_settings(warnings=warnings_list)
|
|
267
|
+
for warning in warnings_list:
|
|
268
|
+
click.echo(f"[warning] {warning}", err=True)
|
|
269
|
+
|
|
270
|
+
# Execute query
|
|
271
|
+
async def run_query() -> Any:
|
|
272
|
+
from affinity import AsyncAffinity
|
|
273
|
+
|
|
274
|
+
async with AsyncAffinity(
|
|
275
|
+
api_key=settings.api_key,
|
|
276
|
+
v1_base_url=settings.v1_base_url,
|
|
277
|
+
v2_base_url=settings.v2_base_url,
|
|
278
|
+
timeout=settings.timeout,
|
|
279
|
+
log_requests=settings.log_requests,
|
|
280
|
+
max_retries=settings.max_retries,
|
|
281
|
+
on_request=settings.on_request,
|
|
282
|
+
on_response=settings.on_response,
|
|
283
|
+
on_error=settings.on_error,
|
|
284
|
+
policies=settings.policies,
|
|
285
|
+
) as client:
|
|
286
|
+
# Create progress callback
|
|
287
|
+
if quiet:
|
|
288
|
+
progress = None
|
|
289
|
+
else:
|
|
290
|
+
progress = create_progress_callback(
|
|
291
|
+
total_steps=len(plan.steps),
|
|
292
|
+
quiet=quiet,
|
|
293
|
+
force_ndjson=ctx.output == "json",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Use context manager for Rich progress
|
|
297
|
+
if isinstance(progress, RichQueryProgress):
|
|
298
|
+
with progress:
|
|
299
|
+
executor = QueryExecutor(
|
|
300
|
+
client,
|
|
301
|
+
progress=progress,
|
|
302
|
+
concurrency=10,
|
|
303
|
+
max_records=max_records,
|
|
304
|
+
timeout=timeout,
|
|
305
|
+
allow_partial=True,
|
|
306
|
+
)
|
|
307
|
+
result = await executor.execute(plan)
|
|
308
|
+
else:
|
|
309
|
+
executor = QueryExecutor(
|
|
310
|
+
client,
|
|
311
|
+
progress=progress,
|
|
312
|
+
concurrency=10,
|
|
313
|
+
max_records=max_records,
|
|
314
|
+
timeout=timeout,
|
|
315
|
+
allow_partial=True,
|
|
316
|
+
)
|
|
317
|
+
result = await executor.execute(plan)
|
|
318
|
+
|
|
319
|
+
# Capture rate limit before client closes
|
|
320
|
+
result.rate_limit = client.rate_limits.snapshot()
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
result = asyncio.run(run_query())
|
|
325
|
+
except QueryTimeoutError as e:
|
|
326
|
+
raise CLIError(f"Query timed out after {e.elapsed_seconds:.1f}s: {e}") from None
|
|
327
|
+
except QuerySafetyLimitError as e:
|
|
328
|
+
raise CLIError(f"Query exceeded safety limit: {e}") from None
|
|
329
|
+
except QueryInterruptedError as e:
|
|
330
|
+
if e.partial_results:
|
|
331
|
+
click.echo(
|
|
332
|
+
f"[interrupted] Returning {len(e.partial_results)} partial results",
|
|
333
|
+
err=True,
|
|
334
|
+
)
|
|
335
|
+
from affinity.cli.query.models import QueryResult
|
|
336
|
+
|
|
337
|
+
result = QueryResult(data=e.partial_results, meta={"interrupted": True})
|
|
338
|
+
else:
|
|
339
|
+
raise CLIError(f"Query interrupted: {e}") from None
|
|
340
|
+
except QueryExecutionError as e:
|
|
341
|
+
raise CLIError(f"Query execution failed: {e}") from None
|
|
342
|
+
|
|
343
|
+
# Format and output results
|
|
344
|
+
if csv_flag:
|
|
345
|
+
from ..csv_utils import write_csv_to_stdout
|
|
346
|
+
|
|
347
|
+
if not result.data:
|
|
348
|
+
click.echo("No results.", err=True)
|
|
349
|
+
sys.exit(0)
|
|
350
|
+
|
|
351
|
+
# Collect all unique field names from data
|
|
352
|
+
all_keys: set[str] = set()
|
|
353
|
+
for record in result.data:
|
|
354
|
+
all_keys.update(record.keys())
|
|
355
|
+
fieldnames = sorted(all_keys)
|
|
356
|
+
|
|
357
|
+
write_csv_to_stdout(rows=result.data, fieldnames=fieldnames, bom=csv_bom)
|
|
358
|
+
sys.exit(0)
|
|
359
|
+
elif ctx.output == "json":
|
|
360
|
+
output = format_json(result, pretty=pretty, include_meta=include_meta)
|
|
361
|
+
elif ctx.output in ("jsonl", "markdown", "toon", "csv"):
|
|
362
|
+
# Use unified formatters for new output formats
|
|
363
|
+
from ..formatters import format_data
|
|
364
|
+
|
|
365
|
+
if not result.data:
|
|
366
|
+
click.echo("No results.", err=True)
|
|
367
|
+
sys.exit(0)
|
|
368
|
+
|
|
369
|
+
# Collect all unique field names from data
|
|
370
|
+
keys: set[str] = set()
|
|
371
|
+
for record in result.data:
|
|
372
|
+
keys.update(record.keys())
|
|
373
|
+
fieldnames = sorted(keys)
|
|
374
|
+
|
|
375
|
+
output = format_data(result.data, ctx.output, fieldnames=fieldnames)
|
|
376
|
+
else:
|
|
377
|
+
# Default to table for interactive use
|
|
378
|
+
output = format_table(result)
|
|
379
|
+
|
|
380
|
+
click.echo(output)
|
|
381
|
+
|
|
382
|
+
# Show summary if not quiet
|
|
383
|
+
if not quiet and include_meta and result.meta:
|
|
384
|
+
exec_time = result.meta.get("executionTime", 0)
|
|
385
|
+
click.echo(f"\n[info] {len(result.data)} records in {exec_time:.2f}s", err=True)
|
|
386
|
+
|
|
387
|
+
# Show rate limit info (at verbose level, matching other commands)
|
|
388
|
+
if verbose and not quiet and result.rate_limit is not None:
|
|
389
|
+
rl = result.rate_limit
|
|
390
|
+
parts: list[str] = []
|
|
391
|
+
if rl.api_key_per_minute.remaining is not None and rl.api_key_per_minute.limit is not None:
|
|
392
|
+
parts.append(f"user {rl.api_key_per_minute.remaining}/{rl.api_key_per_minute.limit}")
|
|
393
|
+
if rl.org_monthly.remaining is not None and rl.org_monthly.limit is not None:
|
|
394
|
+
parts.append(f"org {rl.org_monthly.remaining}/{rl.org_monthly.limit}")
|
|
395
|
+
if parts:
|
|
396
|
+
click.echo(f"rate-limit[{rl.source}]: " + " | ".join(parts), err=True)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _get_query_input(file_path: Path | None, query_str: str | None) -> dict[str, Any]:
|
|
400
|
+
"""Get query input from file, string, or stdin.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
file_path: Path to query file
|
|
404
|
+
query_str: Inline JSON string
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Parsed query dict
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
CLIError: If no input provided or parsing fails
|
|
411
|
+
"""
|
|
412
|
+
if file_path:
|
|
413
|
+
try:
|
|
414
|
+
content = file_path.read_text()
|
|
415
|
+
result: dict[str, Any] = json.loads(content)
|
|
416
|
+
return result
|
|
417
|
+
except json.JSONDecodeError as e:
|
|
418
|
+
raise CLIError(f"Invalid JSON in file: {e}") from None
|
|
419
|
+
except OSError as e:
|
|
420
|
+
raise CLIError(f"Failed to read file: {e}") from None
|
|
421
|
+
|
|
422
|
+
if query_str:
|
|
423
|
+
try:
|
|
424
|
+
result = json.loads(query_str)
|
|
425
|
+
return result
|
|
426
|
+
except json.JSONDecodeError as e:
|
|
427
|
+
raise CLIError(f"Invalid JSON: {e}") from None
|
|
428
|
+
|
|
429
|
+
# Try stdin
|
|
430
|
+
if not sys.stdin.isatty():
|
|
431
|
+
try:
|
|
432
|
+
content = sys.stdin.read()
|
|
433
|
+
result = json.loads(content)
|
|
434
|
+
return result
|
|
435
|
+
except json.JSONDecodeError as e:
|
|
436
|
+
raise CLIError(f"Invalid JSON from stdin: {e}") from None
|
|
437
|
+
|
|
438
|
+
raise CLIError(
|
|
439
|
+
"No query provided. Use --file, --query, or pipe JSON to stdin.\n\n"
|
|
440
|
+
"Examples:\n"
|
|
441
|
+
" xaffinity query --file query.json\n"
|
|
442
|
+
' xaffinity query --query \'{"from": "persons", "limit": 10}\'\n'
|
|
443
|
+
' echo \'{"from": "persons"}\' | xaffinity query'
|
|
444
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from affinity.models.secondary import RelationshipStrength
|
|
4
|
+
from affinity.types import PersonId, UserId
|
|
5
|
+
|
|
6
|
+
from ..click_compat import RichCommand, RichGroup, click
|
|
7
|
+
from ..context import CLIContext
|
|
8
|
+
from ..decorators import category
|
|
9
|
+
from ..options import output_options
|
|
10
|
+
from ..results import CommandContext
|
|
11
|
+
from ..runner import CommandOutput, run_command
|
|
12
|
+
from ..serialization import serialize_model_for_cli
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group(name="relationship-strength", cls=RichGroup)
|
|
16
|
+
def relationship_strength_group() -> None:
|
|
17
|
+
"""Relationship strength commands."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _strength_payload(item: RelationshipStrength) -> dict[str, object]:
|
|
21
|
+
return serialize_model_for_cli(item)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@category("read")
|
|
25
|
+
@relationship_strength_group.command(name="ls", cls=RichCommand)
|
|
26
|
+
@click.option("--external-id", type=int, required=True, help="External person id.")
|
|
27
|
+
@click.option("--internal-id", type=int, default=None, help="Internal user id.")
|
|
28
|
+
@output_options
|
|
29
|
+
@click.pass_obj
|
|
30
|
+
def relationship_strength_ls(
|
|
31
|
+
ctx: CLIContext,
|
|
32
|
+
*,
|
|
33
|
+
external_id: int,
|
|
34
|
+
internal_id: int | None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""List relationship strengths."""
|
|
37
|
+
|
|
38
|
+
def fn(ctx: CLIContext, warnings: list[str]) -> CommandOutput:
|
|
39
|
+
client = ctx.get_client(warnings=warnings)
|
|
40
|
+
strengths = client.relationships.get(
|
|
41
|
+
external_id=PersonId(external_id),
|
|
42
|
+
internal_id=UserId(internal_id) if internal_id is not None else None,
|
|
43
|
+
)
|
|
44
|
+
payload = [_strength_payload(item) for item in strengths]
|
|
45
|
+
|
|
46
|
+
# Build CommandContext
|
|
47
|
+
# Both externalId and internalId are inputs (composite key per spec)
|
|
48
|
+
ctx_inputs: dict[str, object] = {"externalId": external_id}
|
|
49
|
+
if internal_id is not None:
|
|
50
|
+
ctx_inputs["internalId"] = internal_id
|
|
51
|
+
|
|
52
|
+
cmd_context = CommandContext(
|
|
53
|
+
name="relationship-strength ls",
|
|
54
|
+
inputs=ctx_inputs,
|
|
55
|
+
modifiers={},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return CommandOutput(
|
|
59
|
+
data={"relationshipStrengths": payload}, context=cmd_context, api_called=True
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
run_command(ctx, command="relationship-strength ls", fn=fn)
|