autotouch-cli 0.2.34__tar.gz → 0.2.35__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.34 → autotouch_cli-0.2.35}/PKG-INFO +1 -1
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_cli/cli.py +342 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_cli.egg-info/PKG-INFO +1 -1
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_cli.egg-info/SOURCES.txt +2 -1
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_shared/linkedin_contract.py +24 -0
- autotouch_cli-0.2.35/autotouch_shared/search_contract.py +233 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/pyproject.toml +1 -1
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/MANIFEST.in +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/README.md +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_cli/__init__.py +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_cli.egg-info/dependency_links.txt +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_cli.egg-info/entry_points.txt +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_cli.egg-info/requires.txt +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_cli.egg-info/top_level.txt +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_shared/__init__.py +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/autotouch_shared/provider_registry.py +0 -0
- {autotouch_cli-0.2.34 → autotouch_cli-0.2.35}/setup.cfg +0 -0
|
@@ -38,6 +38,10 @@ from autotouch_shared.linkedin_contract import (
|
|
|
38
38
|
linkedin_recipe_entries,
|
|
39
39
|
linkedin_recipe_types,
|
|
40
40
|
)
|
|
41
|
+
from autotouch_shared.search_contract import (
|
|
42
|
+
search_recipe_entries,
|
|
43
|
+
search_recipe_types,
|
|
44
|
+
)
|
|
41
45
|
from autotouch_shared.provider_registry import (
|
|
42
46
|
automation_workflow_blueprints,
|
|
43
47
|
get_recipe_contract,
|
|
@@ -93,6 +97,8 @@ SETUP_BANNER = r"""
|
|
|
93
97
|
|
|
94
98
|
COLUMN_RECIPE_TYPES = recipe_types()
|
|
95
99
|
WORKFLOW_BLUEPRINT_TYPES = tuple(sorted(automation_workflow_blueprints().keys()))
|
|
100
|
+
SEARCH_RECIPE_TYPES = list(search_recipe_types())
|
|
101
|
+
SEARCH_RECIPES: Dict[str, Any] = search_recipe_entries()
|
|
96
102
|
|
|
97
103
|
|
|
98
104
|
def _build_column_recipe_catalog() -> Tuple[Dict[str, Dict[str, Any]], Dict[str, List[str]]]:
|
|
@@ -3585,6 +3591,7 @@ def cmd_capabilities(args: argparse.Namespace) -> None:
|
|
|
3585
3591
|
print("--------------")
|
|
3586
3592
|
print(", ".join(sorted(str(name) for name in providers.keys())))
|
|
3587
3593
|
linkedin = data.get("linkedin") if isinstance(data.get("linkedin"), dict) else None
|
|
3594
|
+
search = data.get("search") if isinstance(data.get("search"), dict) else None
|
|
3588
3595
|
if isinstance(linkedin, dict):
|
|
3589
3596
|
print("")
|
|
3590
3597
|
print("LinkedIn list building")
|
|
@@ -3596,7 +3603,44 @@ def cmd_capabilities(args: argparse.Namespace) -> None:
|
|
|
3596
3603
|
if caps:
|
|
3597
3604
|
for mode, cap in caps.items():
|
|
3598
3605
|
print(f" {mode:20s}: {cap}")
|
|
3606
|
+
best_for = linkedin.get("best_for")
|
|
3607
|
+
if isinstance(best_for, list) and best_for:
|
|
3608
|
+
print("Best for :")
|
|
3609
|
+
for item in best_for:
|
|
3610
|
+
print(f" - {item}")
|
|
3611
|
+
not_for = linkedin.get("not_for")
|
|
3612
|
+
if isinstance(not_for, list) and not_for:
|
|
3613
|
+
print("Not for :")
|
|
3614
|
+
for item in not_for:
|
|
3615
|
+
print(f" - {item}")
|
|
3599
3616
|
print("Tip: run `autotouch linkedin recipe` for example payloads.")
|
|
3617
|
+
if isinstance(search, dict):
|
|
3618
|
+
print("")
|
|
3619
|
+
print("Provider-hidden search")
|
|
3620
|
+
print("----------------------")
|
|
3621
|
+
pricing = search.get("pricing", {}).get("families", {})
|
|
3622
|
+
if isinstance(pricing, dict) and pricing:
|
|
3623
|
+
print("Pricing")
|
|
3624
|
+
for family, entry in pricing.items():
|
|
3625
|
+
if isinstance(entry, dict):
|
|
3626
|
+
print(f" {family:20s}: {entry.get('rule', '-')}")
|
|
3627
|
+
chooser = search.get("chooser_guidance") if isinstance(search.get("chooser_guidance"), dict) else {}
|
|
3628
|
+
company_first = chooser.get("use_company_search_when")
|
|
3629
|
+
if isinstance(company_first, list) and company_first:
|
|
3630
|
+
print("Use company search when:")
|
|
3631
|
+
for item in company_first:
|
|
3632
|
+
print(f" - {item}")
|
|
3633
|
+
people_first = chooser.get("use_people_search_when")
|
|
3634
|
+
if isinstance(people_first, list) and people_first:
|
|
3635
|
+
print("Use people search when:")
|
|
3636
|
+
for item in people_first:
|
|
3637
|
+
print(f" - {item}")
|
|
3638
|
+
linkedin_instead = chooser.get("use_linkedin_instead_when")
|
|
3639
|
+
if isinstance(linkedin_instead, list) and linkedin_instead:
|
|
3640
|
+
print("Use LinkedIn instead when:")
|
|
3641
|
+
for item in linkedin_instead:
|
|
3642
|
+
print(f" - {item}")
|
|
3643
|
+
print("Tip: run `autotouch search recipe` for example payloads.")
|
|
3600
3644
|
print("")
|
|
3601
3645
|
print("Tip: inspect automation.providers in JSON output for provider-specific setup contracts.")
|
|
3602
3646
|
print("Tip: for JSON enrichment columns, run with `--wait` and verify terminal status before `autotouch columns projections`.")
|
|
@@ -6494,6 +6538,224 @@ def _add_leads_selection_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
6494
6538
|
)
|
|
6495
6539
|
|
|
6496
6540
|
|
|
6541
|
+
# ---------------------------------------------------------------------------
|
|
6542
|
+
# Search commands
|
|
6543
|
+
# ---------------------------------------------------------------------------
|
|
6544
|
+
|
|
6545
|
+
|
|
6546
|
+
def cmd_search_recipe(args: argparse.Namespace) -> None:
|
|
6547
|
+
selected = str(args.type or "all")
|
|
6548
|
+
if selected == "all":
|
|
6549
|
+
output = {"recipes": SEARCH_RECIPES}
|
|
6550
|
+
if args.out_file:
|
|
6551
|
+
with open(args.out_file, "w", encoding="utf-8") as f:
|
|
6552
|
+
json.dump(output, f, indent=2)
|
|
6553
|
+
_print_json(output, compact=args.compact)
|
|
6554
|
+
return
|
|
6555
|
+
|
|
6556
|
+
recipe = SEARCH_RECIPES.get(selected)
|
|
6557
|
+
if not recipe:
|
|
6558
|
+
print(f"ERROR: unknown recipe type '{selected}'", file=sys.stderr)
|
|
6559
|
+
sys.exit(2)
|
|
6560
|
+
output = {"type": selected, **recipe}
|
|
6561
|
+
if args.out_file and recipe.get("payload") is not None:
|
|
6562
|
+
with open(args.out_file, "w", encoding="utf-8") as f:
|
|
6563
|
+
json.dump(recipe["payload"], f, indent=2)
|
|
6564
|
+
_print_json(output, compact=args.compact)
|
|
6565
|
+
|
|
6566
|
+
|
|
6567
|
+
def _load_search_payload(args: argparse.Namespace, *, default_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
6568
|
+
explicit = _load_json_input(
|
|
6569
|
+
inline_json=getattr(args, "data_json", None),
|
|
6570
|
+
file_path=getattr(args, "data_file", None),
|
|
6571
|
+
context="data",
|
|
6572
|
+
default=None,
|
|
6573
|
+
)
|
|
6574
|
+
if explicit is None:
|
|
6575
|
+
return default_payload
|
|
6576
|
+
if not isinstance(explicit, dict):
|
|
6577
|
+
print("ERROR: search payload must be a JSON object", file=sys.stderr)
|
|
6578
|
+
sys.exit(2)
|
|
6579
|
+
return explicit
|
|
6580
|
+
|
|
6581
|
+
|
|
6582
|
+
def cmd_search_companies(args: argparse.Namespace) -> None:
|
|
6583
|
+
token = _resolve_token(args.token, required=True)
|
|
6584
|
+
payload = _load_search_payload(
|
|
6585
|
+
args,
|
|
6586
|
+
default_payload={
|
|
6587
|
+
"query": str(getattr(args, "query", "") or "").strip(),
|
|
6588
|
+
"limit": max(1, min(int(getattr(args, "limit", 25) or 25), 100)),
|
|
6589
|
+
},
|
|
6590
|
+
)
|
|
6591
|
+
data = _request_api(
|
|
6592
|
+
"POST",
|
|
6593
|
+
"/api/search/companies",
|
|
6594
|
+
base_url=args.base_url,
|
|
6595
|
+
token=token,
|
|
6596
|
+
use_x_api_key=args.use_x_api_key,
|
|
6597
|
+
payload=payload,
|
|
6598
|
+
timeout=args.timeout,
|
|
6599
|
+
verbose=args.verbose,
|
|
6600
|
+
)
|
|
6601
|
+
_print_json(data, compact=args.compact)
|
|
6602
|
+
|
|
6603
|
+
|
|
6604
|
+
def cmd_search_people(args: argparse.Namespace) -> None:
|
|
6605
|
+
token = _resolve_token(args.token, required=True)
|
|
6606
|
+
payload: Dict[str, Any] = {
|
|
6607
|
+
"limit": max(1, min(int(getattr(args, "limit", 25) or 25), 100)),
|
|
6608
|
+
}
|
|
6609
|
+
query = str(getattr(args, "query", "") or "").strip()
|
|
6610
|
+
company = str(getattr(args, "company", "") or "").strip()
|
|
6611
|
+
location = str(getattr(args, "location", "") or "").strip()
|
|
6612
|
+
if query:
|
|
6613
|
+
payload["query"] = query
|
|
6614
|
+
if company:
|
|
6615
|
+
payload["company"] = company
|
|
6616
|
+
if location:
|
|
6617
|
+
payload["location"] = location
|
|
6618
|
+
raw_skills = getattr(args, "skill", None) or []
|
|
6619
|
+
skills: List[str] = []
|
|
6620
|
+
for raw in raw_skills:
|
|
6621
|
+
skills.extend([part.strip() for part in str(raw).split(",") if part.strip()])
|
|
6622
|
+
if skills:
|
|
6623
|
+
payload["skills"] = skills
|
|
6624
|
+
payload = _load_search_payload(args, default_payload=payload)
|
|
6625
|
+
data = _request_api(
|
|
6626
|
+
"POST",
|
|
6627
|
+
"/api/search/people",
|
|
6628
|
+
base_url=args.base_url,
|
|
6629
|
+
token=token,
|
|
6630
|
+
use_x_api_key=args.use_x_api_key,
|
|
6631
|
+
payload=payload,
|
|
6632
|
+
timeout=args.timeout,
|
|
6633
|
+
verbose=args.verbose,
|
|
6634
|
+
)
|
|
6635
|
+
_print_json(data, compact=args.compact)
|
|
6636
|
+
|
|
6637
|
+
|
|
6638
|
+
def cmd_search_similar_companies(args: argparse.Namespace) -> None:
|
|
6639
|
+
token = _resolve_token(args.token, required=True)
|
|
6640
|
+
payload = _load_search_payload(
|
|
6641
|
+
args,
|
|
6642
|
+
default_payload={
|
|
6643
|
+
"company_name": str(getattr(args, "company_name", "") or "").strip(),
|
|
6644
|
+
"limit": max(1, min(int(getattr(args, "limit", 25) or 25), 100)),
|
|
6645
|
+
},
|
|
6646
|
+
)
|
|
6647
|
+
data = _request_api(
|
|
6648
|
+
"POST",
|
|
6649
|
+
"/api/search/similar-companies",
|
|
6650
|
+
base_url=args.base_url,
|
|
6651
|
+
token=token,
|
|
6652
|
+
use_x_api_key=args.use_x_api_key,
|
|
6653
|
+
payload=payload,
|
|
6654
|
+
timeout=args.timeout,
|
|
6655
|
+
verbose=args.verbose,
|
|
6656
|
+
)
|
|
6657
|
+
_print_json(data, compact=args.compact)
|
|
6658
|
+
|
|
6659
|
+
|
|
6660
|
+
def cmd_search_news(args: argparse.Namespace) -> None:
|
|
6661
|
+
token = _resolve_token(args.token, required=True)
|
|
6662
|
+
payload = _load_search_payload(
|
|
6663
|
+
args,
|
|
6664
|
+
default_payload={
|
|
6665
|
+
"query": str(getattr(args, "query", "") or "").strip(),
|
|
6666
|
+
"days_back": max(1, min(int(getattr(args, "days_back", 7) or 7), 365)),
|
|
6667
|
+
"limit": max(1, min(int(getattr(args, "limit", 25) or 25), 100)),
|
|
6668
|
+
},
|
|
6669
|
+
)
|
|
6670
|
+
data = _request_api(
|
|
6671
|
+
"POST",
|
|
6672
|
+
"/api/search/news",
|
|
6673
|
+
base_url=args.base_url,
|
|
6674
|
+
token=token,
|
|
6675
|
+
use_x_api_key=args.use_x_api_key,
|
|
6676
|
+
payload=payload,
|
|
6677
|
+
timeout=args.timeout,
|
|
6678
|
+
verbose=args.verbose,
|
|
6679
|
+
)
|
|
6680
|
+
_print_json(data, compact=args.compact)
|
|
6681
|
+
|
|
6682
|
+
|
|
6683
|
+
def cmd_search_local_businesses(args: argparse.Namespace) -> None:
|
|
6684
|
+
token = _resolve_token(args.token, required=True)
|
|
6685
|
+
payload = _load_search_payload(
|
|
6686
|
+
args,
|
|
6687
|
+
default_payload={
|
|
6688
|
+
"query": str(getattr(args, "query", "") or "").strip(),
|
|
6689
|
+
"limit": max(1, min(int(getattr(args, "limit", 25) or 25), 100)),
|
|
6690
|
+
"country": str(getattr(args, "country", "us") or "us").strip(),
|
|
6691
|
+
"language": str(getattr(args, "language", "en") or "en").strip(),
|
|
6692
|
+
"page": max(1, int(getattr(args, "page", 1) or 1)),
|
|
6693
|
+
},
|
|
6694
|
+
)
|
|
6695
|
+
data = _request_api(
|
|
6696
|
+
"POST",
|
|
6697
|
+
"/api/search/local-businesses",
|
|
6698
|
+
base_url=args.base_url,
|
|
6699
|
+
token=token,
|
|
6700
|
+
use_x_api_key=args.use_x_api_key,
|
|
6701
|
+
payload=payload,
|
|
6702
|
+
timeout=args.timeout,
|
|
6703
|
+
verbose=args.verbose,
|
|
6704
|
+
)
|
|
6705
|
+
_print_json(data, compact=args.compact)
|
|
6706
|
+
|
|
6707
|
+
|
|
6708
|
+
def cmd_search_reviews(args: argparse.Namespace) -> None:
|
|
6709
|
+
token = _resolve_token(args.token, required=True)
|
|
6710
|
+
payload: Dict[str, Any] = {
|
|
6711
|
+
"sort_by": str(getattr(args, "sort_by", "newest") or "newest").strip(),
|
|
6712
|
+
"limit": max(1, min(int(getattr(args, "limit", 25) or 25), 100)),
|
|
6713
|
+
"country": str(getattr(args, "country", "us") or "us").strip(),
|
|
6714
|
+
"language": str(getattr(args, "language", "en") or "en").strip(),
|
|
6715
|
+
"page": max(1, int(getattr(args, "page", 1) or 1)),
|
|
6716
|
+
}
|
|
6717
|
+
for field_name in ("cid", "place_id", "fid", "next_page_token"):
|
|
6718
|
+
value = str(getattr(args, field_name, "") or "").strip()
|
|
6719
|
+
if value:
|
|
6720
|
+
payload[field_name] = value
|
|
6721
|
+
payload = _load_search_payload(args, default_payload=payload)
|
|
6722
|
+
data = _request_api(
|
|
6723
|
+
"POST",
|
|
6724
|
+
"/api/search/reviews",
|
|
6725
|
+
base_url=args.base_url,
|
|
6726
|
+
token=token,
|
|
6727
|
+
use_x_api_key=args.use_x_api_key,
|
|
6728
|
+
payload=payload,
|
|
6729
|
+
timeout=args.timeout,
|
|
6730
|
+
verbose=args.verbose,
|
|
6731
|
+
)
|
|
6732
|
+
_print_json(data, compact=args.compact)
|
|
6733
|
+
|
|
6734
|
+
|
|
6735
|
+
def cmd_search_filings(args: argparse.Namespace) -> None:
|
|
6736
|
+
token = _resolve_token(args.token, required=True)
|
|
6737
|
+
filing_types: List[str] = []
|
|
6738
|
+
for raw in getattr(args, "filing_type", None) or []:
|
|
6739
|
+
filing_types.extend([part.strip() for part in str(raw).split(",") if part.strip()])
|
|
6740
|
+
payload: Dict[str, Any] = {
|
|
6741
|
+
"company_name": str(getattr(args, "company_name", "") or "").strip(),
|
|
6742
|
+
}
|
|
6743
|
+
if filing_types:
|
|
6744
|
+
payload["filing_types"] = filing_types
|
|
6745
|
+
payload = _load_search_payload(args, default_payload=payload)
|
|
6746
|
+
data = _request_api(
|
|
6747
|
+
"POST",
|
|
6748
|
+
"/api/search/filings",
|
|
6749
|
+
base_url=args.base_url,
|
|
6750
|
+
token=token,
|
|
6751
|
+
use_x_api_key=args.use_x_api_key,
|
|
6752
|
+
payload=payload,
|
|
6753
|
+
timeout=args.timeout,
|
|
6754
|
+
verbose=args.verbose,
|
|
6755
|
+
)
|
|
6756
|
+
_print_json(data, compact=args.compact)
|
|
6757
|
+
|
|
6758
|
+
|
|
6497
6759
|
# ---------------------------------------------------------------------------
|
|
6498
6760
|
# LinkedIn commands
|
|
6499
6761
|
# ---------------------------------------------------------------------------
|
|
@@ -6868,6 +7130,86 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
6868
7130
|
_add_api_common_arguments(pc)
|
|
6869
7131
|
pc.set_defaults(func=cmd_capabilities)
|
|
6870
7132
|
|
|
7133
|
+
# search
|
|
7134
|
+
psrch = sub.add_parser("search", help="Provider-hidden search and list-building operations")
|
|
7135
|
+
search_sub = psrch.add_subparsers(dest="search_cmd", required=True)
|
|
7136
|
+
|
|
7137
|
+
psr = search_sub.add_parser("recipe", help="Print search payload recipes")
|
|
7138
|
+
psr.add_argument("--type", choices=["all", *SEARCH_RECIPE_TYPES], default="all")
|
|
7139
|
+
psr.add_argument("--out-file", help="Optional path to save the selected payload JSON")
|
|
7140
|
+
_add_api_common_arguments(psr)
|
|
7141
|
+
psr.set_defaults(func=cmd_search_recipe)
|
|
7142
|
+
|
|
7143
|
+
psc = search_sub.add_parser("companies", help="Find companies with provider-hidden neural search")
|
|
7144
|
+
psc.add_argument("--query", help="Free-text company query")
|
|
7145
|
+
psc.add_argument("--limit", type=int, default=25, help="Max results, 1-100 (default: 25)")
|
|
7146
|
+
psc.add_argument("--data-json", help="Explicit request payload JSON")
|
|
7147
|
+
psc.add_argument("--data-file", help="Path to request payload JSON file")
|
|
7148
|
+
_add_api_common_arguments(psc)
|
|
7149
|
+
psc.set_defaults(func=cmd_search_companies)
|
|
7150
|
+
|
|
7151
|
+
psp = search_sub.add_parser("people", help="Find people with provider-hidden neural search")
|
|
7152
|
+
psp.add_argument("--query", help="Free-text role/persona query")
|
|
7153
|
+
psp.add_argument("--company", help="Optional company name")
|
|
7154
|
+
psp.add_argument("--location", help="Optional location")
|
|
7155
|
+
psp.add_argument("--skill", action="append", help="Optional skill (repeatable; comma-separated also supported)")
|
|
7156
|
+
psp.add_argument("--limit", type=int, default=25, help="Max results, 1-100 (default: 25)")
|
|
7157
|
+
psp.add_argument("--data-json", help="Explicit request payload JSON")
|
|
7158
|
+
psp.add_argument("--data-file", help="Path to request payload JSON file")
|
|
7159
|
+
_add_api_common_arguments(psp)
|
|
7160
|
+
psp.set_defaults(func=cmd_search_people)
|
|
7161
|
+
|
|
7162
|
+
pss = search_sub.add_parser("similar-companies", help="Find companies similar to a seed company")
|
|
7163
|
+
pss.add_argument("--company-name", help="Seed company name")
|
|
7164
|
+
pss.add_argument("--limit", type=int, default=25, help="Max results, 1-100 (default: 25)")
|
|
7165
|
+
pss.add_argument("--data-json", help="Explicit request payload JSON")
|
|
7166
|
+
pss.add_argument("--data-file", help="Path to request payload JSON file")
|
|
7167
|
+
_add_api_common_arguments(pss)
|
|
7168
|
+
pss.set_defaults(func=cmd_search_similar_companies)
|
|
7169
|
+
|
|
7170
|
+
psn = search_sub.add_parser("news", help="Search recent news")
|
|
7171
|
+
psn.add_argument("--query", help="News query")
|
|
7172
|
+
psn.add_argument("--days-back", type=int, default=7, help="Lookback window in days (default: 7)")
|
|
7173
|
+
psn.add_argument("--limit", type=int, default=25, help="Max results, 1-100 (default: 25)")
|
|
7174
|
+
psn.add_argument("--data-json", help="Explicit request payload JSON")
|
|
7175
|
+
psn.add_argument("--data-file", help="Path to request payload JSON file")
|
|
7176
|
+
_add_api_common_arguments(psn)
|
|
7177
|
+
psn.set_defaults(func=cmd_search_news)
|
|
7178
|
+
|
|
7179
|
+
pslb = search_sub.add_parser("local-businesses", help="Search local businesses")
|
|
7180
|
+
pslb.add_argument("--query", help="Local business query")
|
|
7181
|
+
pslb.add_argument("--limit", type=int, default=25, help="Max results, 1-100 (default: 25)")
|
|
7182
|
+
pslb.add_argument("--country", default="us", help="Country code (default: us)")
|
|
7183
|
+
pslb.add_argument("--language", default="en", help="Language code (default: en)")
|
|
7184
|
+
pslb.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
|
|
7185
|
+
pslb.add_argument("--data-json", help="Explicit request payload JSON")
|
|
7186
|
+
pslb.add_argument("--data-file", help="Path to request payload JSON file")
|
|
7187
|
+
_add_api_common_arguments(pslb)
|
|
7188
|
+
pslb.set_defaults(func=cmd_search_local_businesses)
|
|
7189
|
+
|
|
7190
|
+
psrv = search_sub.add_parser("reviews", help="Fetch reviews for a known listing")
|
|
7191
|
+
psrv.add_argument("--cid", help="Listing CID")
|
|
7192
|
+
psrv.add_argument("--place-id", help="Listing place_id")
|
|
7193
|
+
psrv.add_argument("--fid", help="Listing fid")
|
|
7194
|
+
psrv.add_argument("--sort-by", default="newest", help="Sort order (default: newest)")
|
|
7195
|
+
psrv.add_argument("--limit", type=int, default=25, help="Max results, 1-100 (default: 25)")
|
|
7196
|
+
psrv.add_argument("--country", default="us", help="Country code (default: us)")
|
|
7197
|
+
psrv.add_argument("--language", default="en", help="Language code (default: en)")
|
|
7198
|
+
psrv.add_argument("--next-page-token", help="Pagination token")
|
|
7199
|
+
psrv.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
|
|
7200
|
+
psrv.add_argument("--data-json", help="Explicit request payload JSON")
|
|
7201
|
+
psrv.add_argument("--data-file", help="Path to request payload JSON file")
|
|
7202
|
+
_add_api_common_arguments(psrv)
|
|
7203
|
+
psrv.set_defaults(func=cmd_search_reviews)
|
|
7204
|
+
|
|
7205
|
+
psf = search_sub.add_parser("filings", help="Research public company filings")
|
|
7206
|
+
psf.add_argument("--company-name", help="Public company name")
|
|
7207
|
+
psf.add_argument("--filing-type", action="append", help="Filing type (repeatable; comma-separated also supported)")
|
|
7208
|
+
psf.add_argument("--data-json", help="Explicit request payload JSON")
|
|
7209
|
+
psf.add_argument("--data-file", help="Path to request payload JSON file")
|
|
7210
|
+
_add_api_common_arguments(psf)
|
|
7211
|
+
psf.set_defaults(func=cmd_search_filings)
|
|
7212
|
+
|
|
6871
7213
|
# workflows
|
|
6872
7214
|
pw = sub.add_parser("workflows", help="Common multi-step workflow blueprints")
|
|
6873
7215
|
workflows_sub = pw.add_subparsers(dest="workflows_cmd", required=True)
|
|
@@ -11,4 +11,5 @@ autotouch_cli.egg-info/requires.txt
|
|
|
11
11
|
autotouch_cli.egg-info/top_level.txt
|
|
12
12
|
autotouch_shared/__init__.py
|
|
13
13
|
autotouch_shared/linkedin_contract.py
|
|
14
|
-
autotouch_shared/provider_registry.py
|
|
14
|
+
autotouch_shared/provider_registry.py
|
|
15
|
+
autotouch_shared/search_contract.py
|
|
@@ -90,9 +90,33 @@ _LINKEDIN_ENDPOINTS: Dict[str, Dict[str, Any]] = {
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
_LINKEDIN_CAPABILITIES: Dict[str, Any] = {
|
|
93
|
+
"surface_type": "connected_linkedin_search",
|
|
93
94
|
"access_gate": "linkedin_access flag on user",
|
|
94
95
|
"connection_required": True,
|
|
96
|
+
"requires_connected_account": True,
|
|
95
97
|
"api_modes": ["classic", "sales_navigator"],
|
|
98
|
+
"best_for": [
|
|
99
|
+
"replaying a LinkedIn or Sales Navigator search URL",
|
|
100
|
+
"searching LinkedIn-native people, companies, jobs, or posts with a connected account",
|
|
101
|
+
"building engagement lists from LinkedIn posts, comments, or reactions",
|
|
102
|
+
],
|
|
103
|
+
"not_for": [
|
|
104
|
+
"generic public-web company discovery",
|
|
105
|
+
"provider-hidden semantic people discovery when the source of truth is not specifically LinkedIn",
|
|
106
|
+
],
|
|
107
|
+
"chooser_guidance": {
|
|
108
|
+
"use_linkedin_when": [
|
|
109
|
+
"the list source of truth is LinkedIn itself",
|
|
110
|
+
"you want to replay a LinkedIn or Sales Navigator search URL",
|
|
111
|
+
"you want comments, reactions, or recent post activity from LinkedIn",
|
|
112
|
+
],
|
|
113
|
+
"company_first_when": [
|
|
114
|
+
"you need to discover the target accounts before looking for contacts",
|
|
115
|
+
],
|
|
116
|
+
"people_first_when": [
|
|
117
|
+
"you already know the target companies and want role- or persona-based contacts",
|
|
118
|
+
],
|
|
119
|
+
},
|
|
96
120
|
"list_building_endpoints": [
|
|
97
121
|
"/api/integrations/linkedin/search",
|
|
98
122
|
"/api/integrations/linkedin/search/parameters",
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Shared provider-hidden search contract for API capabilities, CLI recipes, and docs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from typing import Any, Dict, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SEARCH_SCOPE = "search:run"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_SEARCH_ENDPOINTS: Dict[str, Dict[str, Any]] = {
|
|
13
|
+
"search_companies": {
|
|
14
|
+
"method": "POST",
|
|
15
|
+
"path": "/api/search/companies",
|
|
16
|
+
"required_scope": SEARCH_SCOPE,
|
|
17
|
+
"body": {
|
|
18
|
+
"query": "required free-text company search query",
|
|
19
|
+
"limit": "optional 1-100 (default 25)",
|
|
20
|
+
},
|
|
21
|
+
"response": {"items_field": "items", "item_type": "company"},
|
|
22
|
+
},
|
|
23
|
+
"search_people": {
|
|
24
|
+
"method": "POST",
|
|
25
|
+
"path": "/api/search/people",
|
|
26
|
+
"required_scope": SEARCH_SCOPE,
|
|
27
|
+
"body": {
|
|
28
|
+
"query": "optional free-text person/role query",
|
|
29
|
+
"company": "optional target company name",
|
|
30
|
+
"location": "optional location string",
|
|
31
|
+
"skills": "optional string[]",
|
|
32
|
+
"limit": "optional 1-100 (default 25)",
|
|
33
|
+
},
|
|
34
|
+
"response": {"items_field": "items", "item_type": "person"},
|
|
35
|
+
},
|
|
36
|
+
"search_similar_companies": {
|
|
37
|
+
"method": "POST",
|
|
38
|
+
"path": "/api/search/similar-companies",
|
|
39
|
+
"required_scope": SEARCH_SCOPE,
|
|
40
|
+
"body": {
|
|
41
|
+
"company_name": "required seed company name",
|
|
42
|
+
"limit": "optional 1-100 (default 25)",
|
|
43
|
+
},
|
|
44
|
+
"response": {"items_field": "items", "item_type": "company"},
|
|
45
|
+
},
|
|
46
|
+
"search_news": {
|
|
47
|
+
"method": "POST",
|
|
48
|
+
"path": "/api/search/news",
|
|
49
|
+
"required_scope": SEARCH_SCOPE,
|
|
50
|
+
"body": {
|
|
51
|
+
"query": "required free-text news query",
|
|
52
|
+
"days_back": "optional lookback window in days (default 7)",
|
|
53
|
+
"limit": "optional 1-100 (default 25)",
|
|
54
|
+
},
|
|
55
|
+
"response": {"items_field": "items", "item_type": "news_article"},
|
|
56
|
+
},
|
|
57
|
+
"search_local_businesses": {
|
|
58
|
+
"method": "POST",
|
|
59
|
+
"path": "/api/search/local-businesses",
|
|
60
|
+
"required_scope": SEARCH_SCOPE,
|
|
61
|
+
"body": {
|
|
62
|
+
"query": "required local business query",
|
|
63
|
+
"limit": "optional 1-100 (default 25)",
|
|
64
|
+
"country": "optional country code (default us)",
|
|
65
|
+
"language": "optional language code (default en)",
|
|
66
|
+
"page": "optional page number (default 1)",
|
|
67
|
+
},
|
|
68
|
+
"response": {"items_field": "items", "item_type": "local_business"},
|
|
69
|
+
},
|
|
70
|
+
"search_reviews": {
|
|
71
|
+
"method": "POST",
|
|
72
|
+
"path": "/api/search/reviews",
|
|
73
|
+
"required_scope": SEARCH_SCOPE,
|
|
74
|
+
"body": {
|
|
75
|
+
"cid": "optional listing CID",
|
|
76
|
+
"place_id": "optional place identifier",
|
|
77
|
+
"fid": "optional listing fid",
|
|
78
|
+
"sort_by": "optional newest|relevance|rating (default newest)",
|
|
79
|
+
"limit": "optional 1-100 (default 25)",
|
|
80
|
+
"country": "optional country code (default us)",
|
|
81
|
+
"language": "optional language code (default en)",
|
|
82
|
+
"next_page_token": "optional pagination token",
|
|
83
|
+
"page": "optional page number (default 1)",
|
|
84
|
+
},
|
|
85
|
+
"response": {"items_field": "items", "item_type": "review"},
|
|
86
|
+
},
|
|
87
|
+
"search_filings": {
|
|
88
|
+
"method": "POST",
|
|
89
|
+
"path": "/api/search/filings",
|
|
90
|
+
"required_scope": SEARCH_SCOPE,
|
|
91
|
+
"body": {
|
|
92
|
+
"company_name": "required company name",
|
|
93
|
+
"filing_types": "optional string[] (default ['10-K','10-Q'])",
|
|
94
|
+
},
|
|
95
|
+
"response": {"items_field": "items", "item_type": "filing"},
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_SEARCH_CAPABILITIES: Dict[str, Any] = {
|
|
101
|
+
"surface_type": "provider_hidden_search",
|
|
102
|
+
"connection_required": False,
|
|
103
|
+
"requires_connected_account": False,
|
|
104
|
+
"recommended_scope": SEARCH_SCOPE,
|
|
105
|
+
"chooser_guidance": {
|
|
106
|
+
"use_company_search_when": [
|
|
107
|
+
"you need to discover target accounts before looking for contacts",
|
|
108
|
+
"the search criteria are market, product, or industry driven",
|
|
109
|
+
],
|
|
110
|
+
"use_people_search_when": [
|
|
111
|
+
"you already know the target companies",
|
|
112
|
+
"the search criteria are role, persona, or skill driven",
|
|
113
|
+
],
|
|
114
|
+
"use_linkedin_instead_when": [
|
|
115
|
+
"the source of truth is a LinkedIn or Sales Navigator search URL",
|
|
116
|
+
"you need LinkedIn-native posts, comments, reactions, or engagement lists",
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
"pricing": {
|
|
120
|
+
"charge_on": "actual_results_returned",
|
|
121
|
+
"families": {
|
|
122
|
+
"companies": {"credits_per_bucket": 1, "bucket_size": 10, "rule": "1 credit per 10 results"},
|
|
123
|
+
"people": {"credits_per_bucket": 1, "bucket_size": 10, "rule": "1 credit per 10 results"},
|
|
124
|
+
"similar_companies": {"credits_per_bucket": 1, "bucket_size": 10, "rule": "1 credit per 10 results"},
|
|
125
|
+
"news": {"credits_per_bucket": 1, "bucket_size": 25, "rule": "1 credit per 25 results"},
|
|
126
|
+
"local_businesses": {"credits_per_bucket": 1, "bucket_size": 25, "rule": "1 credit per 25 results"},
|
|
127
|
+
"reviews": {"credits_per_bucket": 1, "bucket_size": 25, "rule": "1 credit per 25 results"},
|
|
128
|
+
"filings": {"credits_per_bucket": 1, "bucket_size": 1, "rule": "1 credit per search"},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
"recommended_cli": [
|
|
132
|
+
"autotouch search companies --query 'remote IT asset management' --limit 25",
|
|
133
|
+
"autotouch search people --query 'VP IT' --company 'Rippling' --limit 25",
|
|
134
|
+
"autotouch search recipe --type companies --out-file companies.json",
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_SEARCH_RECIPES: Dict[str, Dict[str, Any]] = {
|
|
140
|
+
"companies": {
|
|
141
|
+
"payload": {"query": "remote IT asset management", "limit": 25},
|
|
142
|
+
"usage": "autotouch search companies --data-file <payload.json>",
|
|
143
|
+
"notes": [
|
|
144
|
+
"Use this when you need to discover target accounts first.",
|
|
145
|
+
"Pricing: 1 credit per 10 returned companies.",
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
"people": {
|
|
149
|
+
"payload": {
|
|
150
|
+
"query": "VP IT",
|
|
151
|
+
"company": "Rippling",
|
|
152
|
+
"location": "United States",
|
|
153
|
+
"skills": ["endpoint management", "IT operations"],
|
|
154
|
+
"limit": 25,
|
|
155
|
+
},
|
|
156
|
+
"usage": "autotouch search people --data-file <payload.json>",
|
|
157
|
+
"notes": [
|
|
158
|
+
"Use this when you already know the target companies or persona.",
|
|
159
|
+
"Pricing: 1 credit per 10 returned people.",
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
"similar_companies": {
|
|
163
|
+
"payload": {"company_name": "Rippling", "limit": 25},
|
|
164
|
+
"usage": "autotouch search similar-companies --data-file <payload.json>",
|
|
165
|
+
"notes": [
|
|
166
|
+
"Good for expanding a seed account list.",
|
|
167
|
+
"Pricing: 1 credit per 10 returned companies.",
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
"news": {
|
|
171
|
+
"payload": {"query": "device management startups", "days_back": 14, "limit": 25},
|
|
172
|
+
"usage": "autotouch search news --data-file <payload.json>",
|
|
173
|
+
"notes": [
|
|
174
|
+
"Use for recent intent signals and market monitoring.",
|
|
175
|
+
"Pricing: 1 credit per 25 returned articles.",
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
"local_businesses": {
|
|
179
|
+
"payload": {"query": "managed IT services austin texas", "limit": 25, "country": "us", "language": "en"},
|
|
180
|
+
"usage": "autotouch search local-businesses --data-file <payload.json>",
|
|
181
|
+
"notes": [
|
|
182
|
+
"Best for local business or maps-style discovery.",
|
|
183
|
+
"Pricing: 1 credit per 25 returned businesses.",
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
"reviews": {
|
|
187
|
+
"payload": {"cid": "<GOOGLE_MAPS_CID>", "limit": 25, "sort_by": "newest"},
|
|
188
|
+
"usage": "autotouch search reviews --data-file <payload.json>",
|
|
189
|
+
"notes": [
|
|
190
|
+
"Resolve the listing first from local-business search, then fetch reviews.",
|
|
191
|
+
"Pricing: 1 credit per 25 returned reviews.",
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
"filings": {
|
|
195
|
+
"payload": {"company_name": "Snowflake", "filing_types": ["10-K", "10-Q"]},
|
|
196
|
+
"usage": "autotouch search filings --data-file <payload.json>",
|
|
197
|
+
"notes": [
|
|
198
|
+
"Use for public-company filings and SEC research.",
|
|
199
|
+
"Pricing: 1 credit per successful search.",
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def search_endpoint_entries() -> Dict[str, Dict[str, Any]]:
|
|
206
|
+
return deepcopy(_SEARCH_ENDPOINTS)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def search_capabilities_contract() -> Dict[str, Any]:
|
|
210
|
+
return deepcopy(_SEARCH_CAPABILITIES)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def search_recipe_types() -> Tuple[str, ...]:
|
|
214
|
+
return tuple(_SEARCH_RECIPES.keys())
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def search_recipe_entries() -> Dict[str, Dict[str, Any]]:
|
|
218
|
+
return deepcopy(_SEARCH_RECIPES)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def search_recipe_entry(recipe_type: str) -> Dict[str, Any] | None:
|
|
222
|
+
recipe = _SEARCH_RECIPES.get(str(recipe_type or "").strip())
|
|
223
|
+
return deepcopy(recipe) if isinstance(recipe, dict) else None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
__all__ = [
|
|
227
|
+
"SEARCH_SCOPE",
|
|
228
|
+
"search_capabilities_contract",
|
|
229
|
+
"search_endpoint_entries",
|
|
230
|
+
"search_recipe_entries",
|
|
231
|
+
"search_recipe_entry",
|
|
232
|
+
"search_recipe_types",
|
|
233
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|