autotouch-cli 0.2.58__tar.gz → 0.2.60__tar.gz
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.
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/PKG-INFO +1 -1
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/cli.py +10 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/sequences.py +123 -17
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/tasks.py +98 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/http.py +8 -2
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/validation.py +62 -6
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/data/CLI_REFERENCE.md +193 -11
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/data/cli-manifest.json +1124 -15
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/parser.py +5 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/parser_groups.py +56 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/sequence_support.py +36 -2
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/templates.py +45 -6
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/PKG-INFO +1 -1
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_shared/provider_registry.py +56 -12
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/pyproject.toml +1 -1
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/MANIFEST.in +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/README.md +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/__init__.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/cli_contracts.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/__init__.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/auth.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/cells.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/columns.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/jobs.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/leads.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/linkedin.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/prompts.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/rows.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/search.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/tables.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/webhooks.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/commands/workspace_secrets.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/__init__.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/auth.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/config.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/csv_import.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/io.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/output.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/polling.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/core/run.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/exceptions.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli/mongo_status.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/SOURCES.txt +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/dependency_links.txt +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/entry_points.txt +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/requires.txt +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_cli.egg-info/top_level.txt +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_shared/__init__.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_shared/linkedin_contract.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/autotouch_shared/search_contract.py +0 -0
- {autotouch_cli-0.2.58 → autotouch_cli-0.2.60}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: autotouch-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.60
|
|
4
4
|
Summary: Autotouch Smart Table CLI
|
|
5
5
|
Project-URL: Homepage, https://app.autotouch.ai
|
|
6
6
|
Project-URL: Documentation, https://github.com/nicolonic/autotouch_main/tree/main/docs/research-table/reference
|
|
@@ -158,9 +158,14 @@ from autotouch_cli.commands.tasks import (
|
|
|
158
158
|
cmd_tasks_delete as cmd_tasks_delete_impl,
|
|
159
159
|
cmd_tasks_draft as cmd_tasks_draft_impl,
|
|
160
160
|
cmd_tasks_get as cmd_tasks_get_impl,
|
|
161
|
+
cmd_tasks_prioritize_email as cmd_tasks_prioritize_email_impl,
|
|
162
|
+
cmd_tasks_prioritize_linkedin as cmd_tasks_prioritize_linkedin_impl,
|
|
161
163
|
cmd_tasks_query as cmd_tasks_query_impl,
|
|
162
164
|
cmd_tasks_recipe as cmd_tasks_recipe_impl,
|
|
165
|
+
cmd_tasks_reschedule_email as cmd_tasks_reschedule_email_impl,
|
|
166
|
+
cmd_tasks_reschedule_linkedin as cmd_tasks_reschedule_linkedin_impl,
|
|
163
167
|
cmd_tasks_schedule_email as cmd_tasks_schedule_email_impl,
|
|
168
|
+
cmd_tasks_schedule_linkedin as cmd_tasks_schedule_linkedin_impl,
|
|
164
169
|
cmd_tasks_stats as cmd_tasks_stats_impl,
|
|
165
170
|
cmd_tasks_update as cmd_tasks_update_impl,
|
|
166
171
|
)
|
|
@@ -1460,6 +1465,11 @@ _register("tasks_bulk_update", cmd_tasks_bulk_update_impl, _task_command_runtime
|
|
|
1460
1465
|
_register("tasks_bulk_delete", cmd_tasks_bulk_delete_impl, _task_command_runtime)
|
|
1461
1466
|
_register("tasks_draft", cmd_tasks_draft_impl, _task_command_runtime)
|
|
1462
1467
|
_register("tasks_schedule_email", cmd_tasks_schedule_email_impl, _task_command_runtime)
|
|
1468
|
+
_register("tasks_prioritize_email", cmd_tasks_prioritize_email_impl, _task_command_runtime)
|
|
1469
|
+
_register("tasks_reschedule_email", cmd_tasks_reschedule_email_impl, _task_command_runtime)
|
|
1470
|
+
_register("tasks_schedule_linkedin", cmd_tasks_schedule_linkedin_impl, _task_command_runtime)
|
|
1471
|
+
_register("tasks_prioritize_linkedin", cmd_tasks_prioritize_linkedin_impl, _task_command_runtime)
|
|
1472
|
+
_register("tasks_reschedule_linkedin", cmd_tasks_reschedule_linkedin_impl, _task_command_runtime)
|
|
1463
1473
|
_register("tasks_recipe", cmd_tasks_recipe_impl, _task_command_runtime)
|
|
1464
1474
|
|
|
1465
1475
|
# -- Webhook commands --
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
4
6
|
from dataclasses import dataclass
|
|
5
7
|
from typing import Any, Callable, Dict, Optional, Sequence
|
|
6
8
|
|
|
@@ -30,6 +32,51 @@ class SequenceCommandRuntime:
|
|
|
30
32
|
sequence_runtime_factory: Callable[[], SequenceRuntime]
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
def _sequence_enroll_research_context(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
36
|
+
research_context = payload.get("researchContext")
|
|
37
|
+
return research_context if isinstance(research_context, dict) else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _sequence_enroll_output_hints(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
41
|
+
research_context = _sequence_enroll_research_context(payload) or {}
|
|
42
|
+
source_table_id = str(
|
|
43
|
+
research_context.get("sourceTableId")
|
|
44
|
+
or research_context.get("source_table_id")
|
|
45
|
+
or ""
|
|
46
|
+
).strip() or None
|
|
47
|
+
attached = bool(research_context)
|
|
48
|
+
|
|
49
|
+
if attached:
|
|
50
|
+
context_hint = (
|
|
51
|
+
"Attached research context will follow later tasks, sidecar views, and AI drafts"
|
|
52
|
+
+ (f" using table {source_table_id}." if source_table_id else ".")
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
context_hint = (
|
|
56
|
+
"No research context is attached; later tasks and AI drafts will use lead/company context only."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"research_context_attached": attached,
|
|
61
|
+
"research_context_table_id": source_table_id,
|
|
62
|
+
"context_hint": context_hint,
|
|
63
|
+
"workflow_hint": (
|
|
64
|
+
"Direct enroll is best for one-off/manual adds. Use add_to_sequence when rows should "
|
|
65
|
+
"auto-enroll from a research table over time."
|
|
66
|
+
),
|
|
67
|
+
"personalization_hint": (
|
|
68
|
+
"Attached research context does not personalize copy by itself; static copy stays the same "
|
|
69
|
+
"unless the sequence uses variables or AI draft steps."
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _attach_sequence_enroll_hints(output: Dict[str, Any], payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
75
|
+
enriched = dict(output)
|
|
76
|
+
enriched.update(_sequence_enroll_output_hints(payload))
|
|
77
|
+
return enriched
|
|
78
|
+
|
|
79
|
+
|
|
33
80
|
def _sequence_mutation_params(args: argparse.Namespace) -> Optional[Dict[str, Any]]:
|
|
34
81
|
return sequence_mutation_params_impl(args)
|
|
35
82
|
|
|
@@ -148,17 +195,41 @@ def cmd_sequences_update(args: argparse.Namespace, *, runtime: SequenceCommandRu
|
|
|
148
195
|
if not isinstance(payload, dict):
|
|
149
196
|
raise AutotouchInputError("sequence update requires --data-json/--data-file with a JSON object")
|
|
150
197
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
198
|
+
try:
|
|
199
|
+
data = runtime.request_api(
|
|
200
|
+
"PUT",
|
|
201
|
+
f"/api/sequences/{args.sequence_id}",
|
|
202
|
+
base_url=args.base_url,
|
|
203
|
+
token=token,
|
|
204
|
+
use_x_api_key=args.use_x_api_key,
|
|
205
|
+
params=_sequence_mutation_params(args),
|
|
206
|
+
payload=payload,
|
|
207
|
+
timeout=args.timeout,
|
|
208
|
+
verbose=args.verbose,
|
|
209
|
+
)
|
|
210
|
+
except AutotouchAPIError as exc:
|
|
211
|
+
if exc.status_code == 409 and exc.response_body:
|
|
212
|
+
try:
|
|
213
|
+
body = json.loads(exc.response_body)
|
|
214
|
+
except Exception:
|
|
215
|
+
body = None
|
|
216
|
+
if isinstance(body, dict) and str(body.get("error") or "").strip() == "delivery_edit_locked":
|
|
217
|
+
message = str(body.get("message") or "Cannot change delivery backend because this sequence has in-flight email work.").strip()
|
|
218
|
+
policy = body.get("deliveryEditPolicy") if isinstance(body.get("deliveryEditPolicy"), dict) else {}
|
|
219
|
+
enrollment_count = int(policy.get("inFlightEnrollmentCount") or 0)
|
|
220
|
+
task_count = int(policy.get("activeEmailTaskCount") or 0)
|
|
221
|
+
outbox_count = int(policy.get("activeEmailOutboxCount") or 0)
|
|
222
|
+
print(f"ERROR: {message}", file=sys.stderr)
|
|
223
|
+
print(
|
|
224
|
+
f"Counts: active enrollments={enrollment_count}, email tasks={task_count}, queued email jobs={outbox_count}",
|
|
225
|
+
file=sys.stderr,
|
|
226
|
+
)
|
|
227
|
+
print(
|
|
228
|
+
f"Hint: clone the sequence with `autotouch sequences clone --sequence-id {args.sequence_id}` and apply the delivery change there.",
|
|
229
|
+
file=sys.stderr,
|
|
230
|
+
)
|
|
231
|
+
raise AutotouchAPIError(message, status_code=exc.status_code, response_body=exc.response_body) from exc
|
|
232
|
+
raise
|
|
162
233
|
runtime.print_json(data, args.compact)
|
|
163
234
|
|
|
164
235
|
|
|
@@ -258,6 +329,7 @@ def cmd_sequences_enroll(args: argparse.Namespace, *, runtime: SequenceCommandRu
|
|
|
258
329
|
output.setdefault("job_id", job_id)
|
|
259
330
|
output["job_status_url"] = status_url
|
|
260
331
|
output["watch_command"] = watch_command
|
|
332
|
+
output = _attach_sequence_enroll_hints(output, payload)
|
|
261
333
|
runtime.print_json(output, args.compact)
|
|
262
334
|
return
|
|
263
335
|
|
|
@@ -306,6 +378,7 @@ def cmd_sequences_enroll(args: argparse.Namespace, *, runtime: SequenceCommandRu
|
|
|
306
378
|
"timed_out": timed_out,
|
|
307
379
|
"polls": int(poll_result.get("polls") or 0),
|
|
308
380
|
}
|
|
381
|
+
output = _attach_sequence_enroll_hints(output, payload)
|
|
309
382
|
runtime.print_json(output, args.compact)
|
|
310
383
|
|
|
311
384
|
if timed_out:
|
|
@@ -386,7 +459,10 @@ def register_sequences_subcommands(
|
|
|
386
459
|
add_api_common_arguments(psqc)
|
|
387
460
|
psqc.set_defaults(func=handlers["create"])
|
|
388
461
|
|
|
389
|
-
psqu = sequences_sub.add_parser(
|
|
462
|
+
psqu = sequences_sub.add_parser(
|
|
463
|
+
"update",
|
|
464
|
+
help="Update a sequence from a JSON payload (schedule changes may rewindow unsent scheduled work)",
|
|
465
|
+
)
|
|
390
466
|
psqu.add_argument("--sequence-id", required=True)
|
|
391
467
|
psqu.add_argument("--data-json", help="Sequence update payload JSON")
|
|
392
468
|
psqu.add_argument("--data-file", help="Sequence update payload file path")
|
|
@@ -432,10 +508,17 @@ def register_sequences_subcommands(
|
|
|
432
508
|
add_api_common_arguments(psqar)
|
|
433
509
|
psqar.set_defaults(func=handlers["status"], target_status="ARCHIVED")
|
|
434
510
|
|
|
435
|
-
psqe = sequences_sub.add_parser(
|
|
511
|
+
psqe = sequences_sub.add_parser(
|
|
512
|
+
"enroll",
|
|
513
|
+
help="Enroll leads into a sequence (best for one-off/manual adds)",
|
|
514
|
+
description=(
|
|
515
|
+
"Direct enroll is best for one-off/manual adds. Use add_to_sequence when rows should "
|
|
516
|
+
"auto-enroll from a research table over time."
|
|
517
|
+
),
|
|
518
|
+
)
|
|
436
519
|
psqe.add_argument("--sequence-id", required=True)
|
|
437
|
-
psqe.add_argument("--data-json", help="Full enroll payload JSON")
|
|
438
|
-
psqe.add_argument("--data-file", help="Full enroll payload JSON file")
|
|
520
|
+
psqe.add_argument("--data-json", help="Full enroll payload JSON (advanced; overrides simpler flags)")
|
|
521
|
+
psqe.add_argument("--data-file", help="Full enroll payload JSON file (advanced; overrides simpler flags)")
|
|
439
522
|
psqe.add_argument(
|
|
440
523
|
"--lead-id",
|
|
441
524
|
action="append",
|
|
@@ -445,8 +528,31 @@ def register_sequences_subcommands(
|
|
|
445
528
|
psqe.add_argument("--lead-ids-file", help="Path to JSON/TXT/CSV file with lead ids")
|
|
446
529
|
psqe.add_argument("--lead-ids-column", default="lead_id", help="CSV column for lead ids (default: lead_id)")
|
|
447
530
|
psqe.add_argument("--start-date", help="Optional startDate ISO timestamp")
|
|
448
|
-
psqe.add_argument(
|
|
449
|
-
|
|
531
|
+
psqe.add_argument(
|
|
532
|
+
"--research-context-json",
|
|
533
|
+
"--attach-research-context-json",
|
|
534
|
+
dest="research_context_json",
|
|
535
|
+
help="Advanced raw researchContext JSON for workflow-scoped table context",
|
|
536
|
+
)
|
|
537
|
+
psqe.add_argument(
|
|
538
|
+
"--research-context-file",
|
|
539
|
+
"--attach-research-context-file",
|
|
540
|
+
dest="research_context_file",
|
|
541
|
+
help="Advanced raw researchContext JSON file for workflow-scoped table context",
|
|
542
|
+
)
|
|
543
|
+
psqe.add_argument(
|
|
544
|
+
"--attach-research-table-id",
|
|
545
|
+
help="Attach research context from this table for later tasks, sidecar views, and AI drafts",
|
|
546
|
+
)
|
|
547
|
+
psqe.add_argument(
|
|
548
|
+
"--attach-research-table-name",
|
|
549
|
+
help="Optional source table name to store alongside attached research context",
|
|
550
|
+
)
|
|
551
|
+
psqe.add_argument(
|
|
552
|
+
"--attach-research-field-id",
|
|
553
|
+
action="append",
|
|
554
|
+
help="Optional research field id to keep in attached context (repeatable; comma-separated also supported)",
|
|
555
|
+
)
|
|
450
556
|
psqe.add_argument("--variable-map-json", help="variableMap JSON")
|
|
451
557
|
psqe.add_argument("--variable-map-file", help="variableMap JSON file")
|
|
452
558
|
psqe.add_argument("--variable-overrides-json", help="variableOverrides JSON")
|
|
@@ -231,3 +231,101 @@ def cmd_tasks_schedule_email(args: argparse.Namespace, *, runtime: TaskCommandRu
|
|
|
231
231
|
verbose=args.verbose,
|
|
232
232
|
)
|
|
233
233
|
runtime.print_json(data, args.compact)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def cmd_tasks_prioritize_email(args: argparse.Namespace, *, runtime: TaskCommandRuntime) -> None:
|
|
237
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
238
|
+
data = runtime.request_api(
|
|
239
|
+
"POST",
|
|
240
|
+
f"/api/task-queue/{args.task_id}/email/prioritize",
|
|
241
|
+
base_url=args.base_url,
|
|
242
|
+
token=token,
|
|
243
|
+
use_x_api_key=args.use_x_api_key,
|
|
244
|
+
params=_task_mutation_params(args),
|
|
245
|
+
timeout=args.timeout,
|
|
246
|
+
verbose=args.verbose,
|
|
247
|
+
)
|
|
248
|
+
runtime.print_json(data, args.compact)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def cmd_tasks_reschedule_email(args: argparse.Namespace, *, runtime: TaskCommandRuntime) -> None:
|
|
252
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
253
|
+
payload = runtime.load_required_object_payload(args, noun="task email reschedule")
|
|
254
|
+
data = runtime.request_api(
|
|
255
|
+
"POST",
|
|
256
|
+
f"/api/task-queue/{args.task_id}/email/reschedule",
|
|
257
|
+
base_url=args.base_url,
|
|
258
|
+
token=token,
|
|
259
|
+
use_x_api_key=args.use_x_api_key,
|
|
260
|
+
params=_task_mutation_params(args),
|
|
261
|
+
payload=payload,
|
|
262
|
+
timeout=args.timeout,
|
|
263
|
+
verbose=args.verbose,
|
|
264
|
+
)
|
|
265
|
+
runtime.print_json(data, args.compact)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _load_optional_task_payload(
|
|
269
|
+
args: argparse.Namespace,
|
|
270
|
+
*,
|
|
271
|
+
runtime: TaskCommandRuntime,
|
|
272
|
+
noun: str,
|
|
273
|
+
) -> Dict[str, Any]:
|
|
274
|
+
payload = runtime.load_json_input(
|
|
275
|
+
inline_json=getattr(args, "data_json", None),
|
|
276
|
+
file_path=getattr(args, "data_file", None),
|
|
277
|
+
context="data",
|
|
278
|
+
default={},
|
|
279
|
+
)
|
|
280
|
+
if not isinstance(payload, dict):
|
|
281
|
+
raise AutotouchInputError(f"{noun} payload must be a JSON object")
|
|
282
|
+
return payload
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cmd_tasks_schedule_linkedin(args: argparse.Namespace, *, runtime: TaskCommandRuntime) -> None:
|
|
286
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
287
|
+
payload = _load_optional_task_payload(args, runtime=runtime, noun="task linkedin schedule")
|
|
288
|
+
data = runtime.request_api(
|
|
289
|
+
"POST",
|
|
290
|
+
f"/api/task-queue/{args.task_id}/linkedin/schedule",
|
|
291
|
+
base_url=args.base_url,
|
|
292
|
+
token=token,
|
|
293
|
+
use_x_api_key=args.use_x_api_key,
|
|
294
|
+
params=_task_mutation_params(args),
|
|
295
|
+
payload=payload,
|
|
296
|
+
timeout=args.timeout,
|
|
297
|
+
verbose=args.verbose,
|
|
298
|
+
)
|
|
299
|
+
runtime.print_json(data, args.compact)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cmd_tasks_prioritize_linkedin(args: argparse.Namespace, *, runtime: TaskCommandRuntime) -> None:
|
|
303
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
304
|
+
data = runtime.request_api(
|
|
305
|
+
"POST",
|
|
306
|
+
f"/api/task-queue/{args.task_id}/linkedin/prioritize",
|
|
307
|
+
base_url=args.base_url,
|
|
308
|
+
token=token,
|
|
309
|
+
use_x_api_key=args.use_x_api_key,
|
|
310
|
+
params=_task_mutation_params(args),
|
|
311
|
+
timeout=args.timeout,
|
|
312
|
+
verbose=args.verbose,
|
|
313
|
+
)
|
|
314
|
+
runtime.print_json(data, args.compact)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def cmd_tasks_reschedule_linkedin(args: argparse.Namespace, *, runtime: TaskCommandRuntime) -> None:
|
|
318
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
319
|
+
payload = runtime.load_required_object_payload(args, noun="task linkedin reschedule")
|
|
320
|
+
data = runtime.request_api(
|
|
321
|
+
"POST",
|
|
322
|
+
f"/api/task-queue/{args.task_id}/linkedin/reschedule",
|
|
323
|
+
base_url=args.base_url,
|
|
324
|
+
token=token,
|
|
325
|
+
use_x_api_key=args.use_x_api_key,
|
|
326
|
+
params=_task_mutation_params(args),
|
|
327
|
+
payload=payload,
|
|
328
|
+
timeout=args.timeout,
|
|
329
|
+
verbose=args.verbose,
|
|
330
|
+
)
|
|
331
|
+
runtime.print_json(data, args.compact)
|
|
@@ -54,6 +54,12 @@ def _print_error_body(body: Any) -> None:
|
|
|
54
54
|
print(str(body), file=sys.stderr)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def _serialize_error_body(body: Any) -> str:
|
|
58
|
+
if isinstance(body, (dict, list)):
|
|
59
|
+
return json.dumps(body, default=str)
|
|
60
|
+
return str(body)
|
|
61
|
+
|
|
62
|
+
|
|
57
63
|
def request_api(
|
|
58
64
|
method: str,
|
|
59
65
|
path: str,
|
|
@@ -129,7 +135,7 @@ def request_api(
|
|
|
129
135
|
raise AutotouchAPIError(
|
|
130
136
|
f"API {response.status_code}",
|
|
131
137
|
status_code=response.status_code,
|
|
132
|
-
response_body=
|
|
138
|
+
response_body=_serialize_error_body(body),
|
|
133
139
|
)
|
|
134
140
|
return body
|
|
135
141
|
|
|
@@ -212,6 +218,6 @@ def request_multipart_api(
|
|
|
212
218
|
raise AutotouchAPIError(
|
|
213
219
|
f"API {response.status_code}",
|
|
214
220
|
status_code=response.status_code,
|
|
215
|
-
response_body=
|
|
221
|
+
response_body=_serialize_error_body(body),
|
|
216
222
|
)
|
|
217
223
|
return body
|
|
@@ -55,8 +55,18 @@ def extract_add_to_crm_field_mappings(payload: Any) -> Optional[Dict[str, Any]]:
|
|
|
55
55
|
return {}
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def inspect_add_to_crm_field_mappings(
|
|
59
|
-
|
|
58
|
+
def inspect_add_to_crm_field_mappings(
|
|
59
|
+
field_mappings: Dict[str, Any],
|
|
60
|
+
*,
|
|
61
|
+
require_company_domain: bool = True,
|
|
62
|
+
) -> Dict[str, Any]:
|
|
63
|
+
raw_mode = str(field_mappings.get("mode") or "").strip().lower()
|
|
64
|
+
if raw_mode == "array":
|
|
65
|
+
mode = "array"
|
|
66
|
+
elif raw_mode == "multi":
|
|
67
|
+
mode = "multi"
|
|
68
|
+
else:
|
|
69
|
+
mode = "single"
|
|
60
70
|
required_missing: List[str] = []
|
|
61
71
|
optional_missing: List[str] = []
|
|
62
72
|
|
|
@@ -75,12 +85,20 @@ def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[st
|
|
|
75
85
|
return True
|
|
76
86
|
return False
|
|
77
87
|
|
|
88
|
+
if not require_company_domain and mode != "single":
|
|
89
|
+
required_missing.append("singleModeOnlyWhenCompanyDomainOptional")
|
|
90
|
+
return {
|
|
91
|
+
"mode": mode,
|
|
92
|
+
"required_missing": required_missing,
|
|
93
|
+
"optional_missing": optional_missing,
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
if mode == "multi":
|
|
79
97
|
common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
|
|
80
98
|
leads = field_mappings.get("leads") if isinstance(field_mappings.get("leads"), list) else []
|
|
81
99
|
|
|
82
100
|
common_domain = str((common or {}).get("companyDomain") or field_mappings.get("companyDomain") or "").strip()
|
|
83
|
-
if not common_domain:
|
|
101
|
+
if require_company_domain and not common_domain:
|
|
84
102
|
required_missing.append("common.companyDomain")
|
|
85
103
|
|
|
86
104
|
has_lead_identity = False
|
|
@@ -95,10 +113,24 @@ def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[st
|
|
|
95
113
|
optional_missing.append(f"leads[{idx}].{slot}")
|
|
96
114
|
if not has_lead_identity:
|
|
97
115
|
required_missing.append("leads[].identityFields")
|
|
116
|
+
elif mode == "array":
|
|
117
|
+
common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
|
|
118
|
+
template = field_mappings.get("template") if isinstance(field_mappings.get("template"), dict) else {}
|
|
119
|
+
if require_company_domain and not str((common or {}).get("companyDomain") or "").strip():
|
|
120
|
+
required_missing.append("common.companyDomain")
|
|
121
|
+
if not str(field_mappings.get("sourceColumn") or "").strip():
|
|
122
|
+
required_missing.append("sourceColumn")
|
|
123
|
+
if not _has_identity_mapping(template):
|
|
124
|
+
required_missing.append("template.identityFields")
|
|
98
125
|
else:
|
|
99
126
|
company_domain_key = str(field_mappings.get("companyDomain") or "").strip()
|
|
100
|
-
if not company_domain_key:
|
|
127
|
+
if require_company_domain and not company_domain_key:
|
|
101
128
|
required_missing.append("companyDomain")
|
|
129
|
+
if require_company_domain:
|
|
130
|
+
if not _has_identity_mapping(field_mappings):
|
|
131
|
+
required_missing.append("identityFields")
|
|
132
|
+
elif not str(field_mappings.get("linkedinUrl") or "").strip():
|
|
133
|
+
required_missing.append("linkedinUrl")
|
|
102
134
|
if not _has_identity_mapping(field_mappings):
|
|
103
135
|
required_missing.append("identityFields")
|
|
104
136
|
for slot in ["firstName", "lastName", "title", "companyName"]:
|
|
@@ -116,6 +148,7 @@ def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
|
|
|
116
148
|
field_mappings = extract_add_to_crm_field_mappings(payload)
|
|
117
149
|
if field_mappings is None:
|
|
118
150
|
return
|
|
151
|
+
config = payload.get("config") if isinstance(payload.get("config"), dict) else {}
|
|
119
152
|
|
|
120
153
|
kind_value = str(payload.get("kind") or "").strip().lower()
|
|
121
154
|
if kind_value != "enrichment":
|
|
@@ -130,10 +163,24 @@ def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
|
|
|
130
163
|
"Tip: run `autotouch columns recipe --type add_to_crm`."
|
|
131
164
|
)
|
|
132
165
|
|
|
133
|
-
|
|
166
|
+
require_company_domain = config.get("requireCompanyDomain", True)
|
|
167
|
+
if not isinstance(require_company_domain, bool):
|
|
168
|
+
raise AutotouchInputError(
|
|
169
|
+
"add_to_crm config.requireCompanyDomain must be a boolean when provided."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
issues = inspect_add_to_crm_field_mappings(
|
|
173
|
+
field_mappings,
|
|
174
|
+
require_company_domain=require_company_domain,
|
|
175
|
+
)
|
|
134
176
|
required_missing = issues.get("required_missing") or []
|
|
135
177
|
optional_missing = issues.get("optional_missing") or []
|
|
136
178
|
if required_missing:
|
|
179
|
+
if "singleModeOnlyWhenCompanyDomainOptional" in required_missing:
|
|
180
|
+
raise AutotouchInputError(
|
|
181
|
+
"add_to_crm with config.requireCompanyDomain=false only supports single-mode fieldMappings. "
|
|
182
|
+
"Tip: map config.fieldMappings.linkedinUrl and keep mode='single' for LinkedIn-First Sync to Leads."
|
|
183
|
+
)
|
|
137
184
|
missing = ", ".join(str(v) for v in required_missing)
|
|
138
185
|
raise AutotouchInputError(
|
|
139
186
|
f"add_to_crm fieldMappings missing required keys: {missing}. "
|
|
@@ -301,11 +348,20 @@ def run_precheck_for_add_to_crm(
|
|
|
301
348
|
if field_mappings is None:
|
|
302
349
|
return
|
|
303
350
|
|
|
304
|
-
|
|
351
|
+
config = column.get("config") if isinstance(column.get("config"), dict) else {}
|
|
352
|
+
require_company_domain = config.get("requireCompanyDomain", True)
|
|
353
|
+
issues = inspect_add_to_crm_field_mappings(
|
|
354
|
+
field_mappings,
|
|
355
|
+
require_company_domain=bool(require_company_domain) if isinstance(require_company_domain, bool) else True,
|
|
356
|
+
)
|
|
305
357
|
required_missing = issues.get("required_missing") or []
|
|
306
358
|
optional_missing = issues.get("optional_missing") or []
|
|
307
359
|
|
|
308
360
|
if required_missing:
|
|
361
|
+
if "singleModeOnlyWhenCompanyDomainOptional" in required_missing:
|
|
362
|
+
raise AutotouchInputError(
|
|
363
|
+
"add_to_crm column is misconfigured: config.requireCompanyDomain=false only supports single-mode fieldMappings for LinkedIn-First Sync to Leads."
|
|
364
|
+
)
|
|
309
365
|
missing = ", ".join(str(v) for v in required_missing)
|
|
310
366
|
raise AutotouchInputError(
|
|
311
367
|
f"add_to_crm column is misconfigured (missing required mappings: {missing}). "
|