discolike-cli 0.1.0__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.
- discolike_cli/__init__.py +0 -0
- discolike_cli/_output.py +135 -0
- discolike_cli/account.py +16 -0
- discolike_cli/auth.py +60 -0
- discolike_cli/company.py +87 -0
- discolike_cli/contacts.py +230 -0
- discolike_cli/discogen.py +119 -0
- discolike_cli/discover.py +111 -0
- discolike_cli/enrich.py +91 -0
- discolike_cli/main.py +59 -0
- discolike_cli/match.py +54 -0
- discolike_cli/queries.py +65 -0
- discolike_cli-0.1.0.dist-info/METADATA +89 -0
- discolike_cli-0.1.0.dist-info/RECORD +16 -0
- discolike_cli-0.1.0.dist-info/WHEEL +4 -0
- discolike_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
discolike_cli/_output.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
from typing import ParamSpec
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
from typing import TypeVar
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from discolike._exceptions import APIConnectionError
|
|
17
|
+
from discolike._exceptions import AuthenticationError
|
|
18
|
+
from discolike._exceptions import DiscolikeError
|
|
19
|
+
from discolike._exceptions import NotFoundError
|
|
20
|
+
from discolike._exceptions import PlanAccessError
|
|
21
|
+
from discolike._exceptions import RateLimitError
|
|
22
|
+
from discolike._exceptions import ValidationError
|
|
23
|
+
from discolike._models import DiscolikeModel
|
|
24
|
+
|
|
25
|
+
P = ParamSpec("P")
|
|
26
|
+
R = TypeVar("R")
|
|
27
|
+
|
|
28
|
+
EXIT_CODES: dict[type, int] = {
|
|
29
|
+
ValidationError: 2,
|
|
30
|
+
AuthenticationError: 3,
|
|
31
|
+
PlanAccessError: 3,
|
|
32
|
+
RateLimitError: 4,
|
|
33
|
+
APIConnectionError: 5,
|
|
34
|
+
NotFoundError: 6,
|
|
35
|
+
}
|
|
36
|
+
DEFAULT_EXIT_CODE = 1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _JobStatusLike(Protocol):
|
|
40
|
+
results: Any
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def progress(self) -> int | None: ...
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, Any]: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SupportsWait(Protocol):
|
|
49
|
+
task_id: str
|
|
50
|
+
task_family: str
|
|
51
|
+
|
|
52
|
+
def wait(self, *, timeout: float, on_poll: Callable[[_JobStatusLike], None] | None = None) -> _JobStatusLike: ...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _normalize(data: Any) -> Any: # noqa: ANN401 -- accepts arbitrary JSON-serializable CLI output data
|
|
56
|
+
if isinstance(data, DiscolikeModel):
|
|
57
|
+
return data.to_dict()
|
|
58
|
+
if isinstance(data, list):
|
|
59
|
+
return [item.to_dict() if isinstance(item, DiscolikeModel) else item for item in data]
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_flat_dict(value: Any) -> bool: # noqa: ANN401 -- runtime-narrowed via isinstance, not a fixed shape
|
|
64
|
+
return isinstance(value, dict) and not any(isinstance(v, (dict, list)) for v in value.values())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _qualifies_for_table(data: Any) -> bool: # noqa: ANN401 -- runtime-narrowed via isinstance, not a fixed shape
|
|
68
|
+
return isinstance(data, list) and len(data) > 0 and all(_is_flat_dict(item) for item in data)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _render_table(rows: list[dict[str, Any]]) -> None:
|
|
72
|
+
table = Table()
|
|
73
|
+
columns = list(rows[0].keys())
|
|
74
|
+
for column in columns:
|
|
75
|
+
table.add_column(column)
|
|
76
|
+
for row in rows:
|
|
77
|
+
table.add_row(*(str(row.get(column, "")) for column in columns))
|
|
78
|
+
Console().print(table)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def emit(data: Any, *, fmt: str | None = None) -> None: # noqa: ANN401 -- accepts arbitrary JSON-serializable CLI output data
|
|
82
|
+
normalized = _normalize(data)
|
|
83
|
+
want_table = fmt == "table" or (fmt is None and sys.stdout.isatty())
|
|
84
|
+
if want_table and _qualifies_for_table(normalized):
|
|
85
|
+
_render_table(normalized)
|
|
86
|
+
return
|
|
87
|
+
print(json.dumps(normalized, indent=2, default=str))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def fail(exc: DiscolikeError) -> typer.Exit:
|
|
91
|
+
payload: dict[str, Any] = {
|
|
92
|
+
"error": type(exc).__name__,
|
|
93
|
+
"message": str(exc),
|
|
94
|
+
"status_code": exc.status_code,
|
|
95
|
+
}
|
|
96
|
+
if isinstance(exc, RateLimitError) and exc.retry_after is not None:
|
|
97
|
+
payload["retry_after"] = exc.retry_after
|
|
98
|
+
print(json.dumps(payload), file=sys.stderr)
|
|
99
|
+
return typer.Exit(code=EXIT_CODES.get(type(exc), DEFAULT_EXIT_CODE))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def call_typed(fn: Callable[..., R], **kwargs: Any) -> R: # noqa: ANN401 -- forwarded to a typed SDK method signature
|
|
103
|
+
try:
|
|
104
|
+
return fn(**kwargs)
|
|
105
|
+
except TypeError as exc:
|
|
106
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def handle_errors(fn: Callable[P, R]) -> Callable[P, R]:
|
|
110
|
+
@functools.wraps(fn)
|
|
111
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
112
|
+
try:
|
|
113
|
+
return fn(*args, **kwargs)
|
|
114
|
+
except DiscolikeError as exc:
|
|
115
|
+
raise fail(exc) from exc
|
|
116
|
+
|
|
117
|
+
return wrapper
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run_job(job: SupportsWait, *, wait: bool, timeout: float) -> None:
|
|
121
|
+
if not wait:
|
|
122
|
+
emit(
|
|
123
|
+
{
|
|
124
|
+
"task_id": job.task_id,
|
|
125
|
+
"task_family": job.task_family,
|
|
126
|
+
"hint": f"poll with: discolike discogen status {job.task_id} --family {job.task_family}",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
def _on_poll(status: _JobStatusLike) -> None:
|
|
132
|
+
sys.stderr.write(f"progress: {status.progress}%\n")
|
|
133
|
+
|
|
134
|
+
final = job.wait(timeout=timeout, on_poll=_on_poll)
|
|
135
|
+
emit(final.results if final.results is not None else final.to_dict())
|
discolike_cli/account.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from discolike_cli._output import emit
|
|
6
|
+
from discolike_cli._output import handle_errors
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Account usage and billing")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("usage")
|
|
12
|
+
@handle_errors
|
|
13
|
+
def usage_command(ctx: typer.Context) -> None:
|
|
14
|
+
from discolike_cli.main import get_client
|
|
15
|
+
|
|
16
|
+
emit(get_client(ctx).account.usage())
|
discolike_cli/auth.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from discolike._config import ENV_API_KEY
|
|
10
|
+
from discolike._config import KEYS_URL
|
|
11
|
+
from discolike._config import delete_config
|
|
12
|
+
from discolike._config import load_config
|
|
13
|
+
from discolike._config import resolve_api_key
|
|
14
|
+
from discolike._config import save_config
|
|
15
|
+
from discolike_cli._output import emit
|
|
16
|
+
from discolike_cli._output import handle_errors
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Manage API credentials")
|
|
19
|
+
|
|
20
|
+
MASKED_VISIBLE_CHARS = 4
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _mask(key: str) -> str:
|
|
24
|
+
return "…" + key[-MASKED_VISIBLE_CHARS:]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
@handle_errors
|
|
29
|
+
def login(
|
|
30
|
+
api_key: str | None = typer.Option(None, help=f"API key. Create one at {KEYS_URL}"),
|
|
31
|
+
) -> None:
|
|
32
|
+
from discolike_cli.main import build_client
|
|
33
|
+
|
|
34
|
+
key = api_key or typer.prompt("API key", hide_input=True)
|
|
35
|
+
build_client(api_key=key).account.usage()
|
|
36
|
+
save_config({"auth_method": "api_key", "api_key": key})
|
|
37
|
+
print(json.dumps({"logged_in": True, "source": "api_key"}), file=sys.stderr)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
@handle_errors
|
|
42
|
+
def status() -> None:
|
|
43
|
+
from discolike_cli.main import build_client
|
|
44
|
+
|
|
45
|
+
key = os.environ.get(ENV_API_KEY)
|
|
46
|
+
source = "env"
|
|
47
|
+
if not key:
|
|
48
|
+
key = load_config().get("api_key")
|
|
49
|
+
source = "config"
|
|
50
|
+
if not key:
|
|
51
|
+
resolve_api_key(None)
|
|
52
|
+
build_client(api_key=key).account.usage()
|
|
53
|
+
emit({"source": source, "api_key": _mask(str(key)), "valid": True})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command()
|
|
57
|
+
@handle_errors
|
|
58
|
+
def logout() -> None:
|
|
59
|
+
delete_config()
|
|
60
|
+
emit({"logged_out": True})
|
discolike_cli/company.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from discolike_cli._output import emit
|
|
6
|
+
from discolike_cli._output import handle_errors
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Look up company data by domain")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command()
|
|
12
|
+
@handle_errors
|
|
13
|
+
def data(ctx: typer.Context, domain: str) -> None:
|
|
14
|
+
from discolike_cli.main import get_client
|
|
15
|
+
|
|
16
|
+
emit(get_client(ctx).companies.data(domain=domain))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
@handle_errors
|
|
21
|
+
def score(ctx: typer.Context, domain: str) -> None:
|
|
22
|
+
from discolike_cli.main import get_client
|
|
23
|
+
|
|
24
|
+
emit(get_client(ctx).companies.score(domain=domain))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
@handle_errors
|
|
29
|
+
def growth(ctx: typer.Context, domain: str) -> None:
|
|
30
|
+
from discolike_cli.main import get_client
|
|
31
|
+
|
|
32
|
+
emit(get_client(ctx).companies.growth(domain=domain))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command()
|
|
36
|
+
@handle_errors
|
|
37
|
+
def metrics(ctx: typer.Context, domain: str) -> None:
|
|
38
|
+
from discolike_cli.main import get_client
|
|
39
|
+
|
|
40
|
+
emit(get_client(ctx).companies.metrics(domain=domain))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
@handle_errors
|
|
45
|
+
def history(ctx: typer.Context, domain: str, max_records: int | None = typer.Option(None, "--max-records")) -> None:
|
|
46
|
+
from discolike_cli.main import get_client
|
|
47
|
+
|
|
48
|
+
emit(get_client(ctx).companies.history(domain=domain, max_records=max_records))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command()
|
|
52
|
+
@handle_errors
|
|
53
|
+
def redirects(ctx: typer.Context, domain: str, match: str | None = typer.Option(None, "--match")) -> None:
|
|
54
|
+
from discolike_cli.main import get_client
|
|
55
|
+
|
|
56
|
+
emit(get_client(ctx).companies.redirects(domain=domain, match=match))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command()
|
|
60
|
+
@handle_errors
|
|
61
|
+
def vendors(ctx: typer.Context, domain: str, match: str | None = typer.Option(None, "--match")) -> None:
|
|
62
|
+
from discolike_cli.main import get_client
|
|
63
|
+
|
|
64
|
+
emit(get_client(ctx).companies.vendors(domain=domain, match=match))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
@handle_errors
|
|
69
|
+
def subsidiaries(ctx: typer.Context, domain: str, match: str | None = typer.Option(None, "--match")) -> None:
|
|
70
|
+
from discolike_cli.main import get_client
|
|
71
|
+
|
|
72
|
+
emit(get_client(ctx).companies.subsidiaries(domain=domain, match=match))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command(name="public-links")
|
|
76
|
+
@handle_errors
|
|
77
|
+
def public_links(ctx: typer.Context, domain: str, source: str = typer.Option(..., "--source")) -> None:
|
|
78
|
+
from discolike_cli.main import get_client
|
|
79
|
+
|
|
80
|
+
emit(get_client(ctx).companies.public_links(domain=domain, source=source))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@handle_errors
|
|
84
|
+
def extract_command(ctx: typer.Context, url: str) -> None:
|
|
85
|
+
from discolike_cli.main import get_client
|
|
86
|
+
|
|
87
|
+
emit(get_client(ctx).companies.extract(url=url))
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from discolike_cli._output import call_typed
|
|
9
|
+
from discolike_cli._output import emit
|
|
10
|
+
from discolike_cli._output import handle_errors
|
|
11
|
+
from discolike_cli._output import run_job
|
|
12
|
+
from discolike_cli.discover import _merge_params
|
|
13
|
+
|
|
14
|
+
DEFAULT_WAIT_TIMEOUT_SECONDS = 900.0
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Search, match, and enrich contacts")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("search")
|
|
20
|
+
@handle_errors
|
|
21
|
+
def search_command(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
icp_prompt: str | None = typer.Option(None),
|
|
24
|
+
icp_text: str | None = typer.Option(None),
|
|
25
|
+
seniority: list[str] | None = typer.Option(None),
|
|
26
|
+
department: list[str] | None = typer.Option(None),
|
|
27
|
+
title: list[str] | None = typer.Option(None),
|
|
28
|
+
domain: list[str] | None = typer.Option(None),
|
|
29
|
+
person_country: list[str] | None = typer.Option(None),
|
|
30
|
+
filter_industry: list[str] | None = typer.Option(None),
|
|
31
|
+
filter_country: list[str] | None = typer.Option(None),
|
|
32
|
+
employee_range: str | None = typer.Option(None),
|
|
33
|
+
has_email: bool | None = typer.Option(None, "--has-email/--no-has-email"),
|
|
34
|
+
max_records: int | None = typer.Option(None),
|
|
35
|
+
offset: int | None = typer.Option(None),
|
|
36
|
+
fmt: str | None = typer.Option(None, "--format"),
|
|
37
|
+
param: list[str] | None = typer.Option(None, "--param"),
|
|
38
|
+
) -> None:
|
|
39
|
+
from discolike_cli.main import get_client
|
|
40
|
+
|
|
41
|
+
kwargs = _merge_params(
|
|
42
|
+
param,
|
|
43
|
+
icp_prompt=icp_prompt,
|
|
44
|
+
icp_text=icp_text,
|
|
45
|
+
seniority=seniority,
|
|
46
|
+
department=department,
|
|
47
|
+
title=title,
|
|
48
|
+
domain=domain,
|
|
49
|
+
person_country=person_country,
|
|
50
|
+
filter_industry=filter_industry,
|
|
51
|
+
filter_country=filter_country,
|
|
52
|
+
employee_range=employee_range,
|
|
53
|
+
has_email=has_email,
|
|
54
|
+
max_records=max_records,
|
|
55
|
+
offset=offset,
|
|
56
|
+
)
|
|
57
|
+
emit(call_typed(get_client(ctx).contacts.search, **kwargs), fmt=fmt)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command("count")
|
|
61
|
+
@handle_errors
|
|
62
|
+
def count_command(
|
|
63
|
+
ctx: typer.Context,
|
|
64
|
+
icp_prompt: str | None = typer.Option(None),
|
|
65
|
+
icp_text: str | None = typer.Option(None),
|
|
66
|
+
seniority: list[str] | None = typer.Option(None),
|
|
67
|
+
department: list[str] | None = typer.Option(None),
|
|
68
|
+
title: list[str] | None = typer.Option(None),
|
|
69
|
+
domain: list[str] | None = typer.Option(None),
|
|
70
|
+
person_country: list[str] | None = typer.Option(None),
|
|
71
|
+
filter_industry: list[str] | None = typer.Option(None),
|
|
72
|
+
filter_country: list[str] | None = typer.Option(None),
|
|
73
|
+
employee_range: str | None = typer.Option(None),
|
|
74
|
+
has_email: bool | None = typer.Option(None, "--has-email/--no-has-email"),
|
|
75
|
+
fmt: str | None = typer.Option(None, "--format"),
|
|
76
|
+
param: list[str] | None = typer.Option(None, "--param"),
|
|
77
|
+
) -> None:
|
|
78
|
+
from discolike_cli.main import get_client
|
|
79
|
+
|
|
80
|
+
kwargs = _merge_params(
|
|
81
|
+
param,
|
|
82
|
+
icp_prompt=icp_prompt,
|
|
83
|
+
icp_text=icp_text,
|
|
84
|
+
seniority=seniority,
|
|
85
|
+
department=department,
|
|
86
|
+
title=title,
|
|
87
|
+
domain=domain,
|
|
88
|
+
person_country=person_country,
|
|
89
|
+
filter_industry=filter_industry,
|
|
90
|
+
filter_country=filter_country,
|
|
91
|
+
employee_range=employee_range,
|
|
92
|
+
has_email=has_email,
|
|
93
|
+
)
|
|
94
|
+
emit(call_typed(get_client(ctx).contacts.count, **kwargs), fmt=fmt)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.command("lookup")
|
|
98
|
+
@handle_errors
|
|
99
|
+
def lookup_command(
|
|
100
|
+
ctx: typer.Context,
|
|
101
|
+
persona_id: int | None = typer.Option(None, "--persona-id"),
|
|
102
|
+
linkedin: str | None = typer.Option(None, "--linkedin"),
|
|
103
|
+
email: str | None = typer.Option(None, "--email"),
|
|
104
|
+
) -> None:
|
|
105
|
+
from discolike_cli.main import get_client
|
|
106
|
+
|
|
107
|
+
emit(get_client(ctx).contacts.lookup(persona_id=persona_id, linkedin=linkedin, email=email))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command("match")
|
|
111
|
+
@handle_errors
|
|
112
|
+
def match_command(
|
|
113
|
+
ctx: typer.Context,
|
|
114
|
+
name: str = typer.Argument(...),
|
|
115
|
+
company_name: str | None = typer.Option(None, "--company-name"),
|
|
116
|
+
domain: str | None = typer.Option(None, "--domain"),
|
|
117
|
+
person_country: str | None = typer.Option(None, "--person-country"),
|
|
118
|
+
limit: int | None = typer.Option(None, "--limit"),
|
|
119
|
+
) -> None:
|
|
120
|
+
from discolike_cli.main import get_client
|
|
121
|
+
|
|
122
|
+
emit(
|
|
123
|
+
get_client(ctx).contacts.match(
|
|
124
|
+
name=name,
|
|
125
|
+
company_name=company_name,
|
|
126
|
+
domain=domain,
|
|
127
|
+
person_country=person_country,
|
|
128
|
+
limit=limit,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command("bulk-match")
|
|
134
|
+
@handle_errors
|
|
135
|
+
def bulk_match_command(
|
|
136
|
+
ctx: typer.Context,
|
|
137
|
+
queries_file: pathlib.Path = typer.Option(..., "--queries-file"),
|
|
138
|
+
enrich: bool | None = typer.Option(None, "--enrich/--no-enrich"),
|
|
139
|
+
limit: int | None = typer.Option(None, "--limit"),
|
|
140
|
+
wait: bool = typer.Option(False, "--wait"),
|
|
141
|
+
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout"),
|
|
142
|
+
) -> None:
|
|
143
|
+
from discolike_cli.main import get_client
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
queries = json.loads(queries_file.read_text())
|
|
147
|
+
except json.JSONDecodeError as exc:
|
|
148
|
+
raise typer.BadParameter(f"--queries-file must contain valid JSON: {exc}") from exc
|
|
149
|
+
if not isinstance(queries, list):
|
|
150
|
+
raise typer.BadParameter("--queries-file must contain a JSON array of objects")
|
|
151
|
+
|
|
152
|
+
job = get_client(ctx).contacts.bulk_match(queries=queries, enrich=enrich, limit=limit)
|
|
153
|
+
run_job(job, wait=wait, timeout=timeout)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command("discover")
|
|
157
|
+
@handle_errors
|
|
158
|
+
def discover_command(
|
|
159
|
+
ctx: typer.Context,
|
|
160
|
+
icp_prompt: str | None = typer.Option(None),
|
|
161
|
+
icp_text: str | None = typer.Option(None),
|
|
162
|
+
seniority: list[str] | None = typer.Option(None),
|
|
163
|
+
department: list[str] | None = typer.Option(None),
|
|
164
|
+
title: list[str] | None = typer.Option(None),
|
|
165
|
+
domain: list[str] | None = typer.Option(None),
|
|
166
|
+
person_country: list[str] | None = typer.Option(None),
|
|
167
|
+
filter_industry: list[str] | None = typer.Option(None),
|
|
168
|
+
filter_country: list[str] | None = typer.Option(None),
|
|
169
|
+
employee_range: str | None = typer.Option(None),
|
|
170
|
+
has_email: bool | None = typer.Option(None, "--has-email/--no-has-email"),
|
|
171
|
+
max_records: int | None = typer.Option(None),
|
|
172
|
+
offset: int | None = typer.Option(None),
|
|
173
|
+
results_by_company: int | None = typer.Option(None, "--results-by-company"),
|
|
174
|
+
include_search_contacts: bool | None = typer.Option(None, "--include-search-contacts/--no-include-search-contacts"),
|
|
175
|
+
consensus: int | None = typer.Option(None, "--consensus"),
|
|
176
|
+
fmt: str | None = typer.Option(None, "--format"),
|
|
177
|
+
param: list[str] | None = typer.Option(None, "--param"),
|
|
178
|
+
) -> None:
|
|
179
|
+
from discolike_cli.main import get_client
|
|
180
|
+
|
|
181
|
+
kwargs = _merge_params(
|
|
182
|
+
param,
|
|
183
|
+
icp_prompt=icp_prompt,
|
|
184
|
+
icp_text=icp_text,
|
|
185
|
+
seniority=seniority,
|
|
186
|
+
department=department,
|
|
187
|
+
title=title,
|
|
188
|
+
domain=domain,
|
|
189
|
+
person_country=person_country,
|
|
190
|
+
filter_industry=filter_industry,
|
|
191
|
+
filter_country=filter_country,
|
|
192
|
+
employee_range=employee_range,
|
|
193
|
+
has_email=has_email,
|
|
194
|
+
max_records=max_records,
|
|
195
|
+
offset=offset,
|
|
196
|
+
results_by_company=results_by_company,
|
|
197
|
+
include_search_contacts=include_search_contacts,
|
|
198
|
+
consensus=consensus,
|
|
199
|
+
)
|
|
200
|
+
emit(call_typed(get_client(ctx).contacts.discover, **kwargs), fmt=fmt)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command("generate")
|
|
204
|
+
@handle_errors
|
|
205
|
+
def generate_command(
|
|
206
|
+
ctx: typer.Context,
|
|
207
|
+
icp_text: str = typer.Option(..., "--icp-text"),
|
|
208
|
+
domain: list[str] = typer.Option(..., "--domain"),
|
|
209
|
+
context_mode: str | None = typer.Option(None, "--context-mode"),
|
|
210
|
+
integration_id: str | None = typer.Option(None, "--integration-id"),
|
|
211
|
+
search_provider_id: str | None = typer.Option(None, "--search-provider-id"),
|
|
212
|
+
search_context_size: str | None = typer.Option(None, "--search-context-size"),
|
|
213
|
+
max_contacts_per_domain: int | None = typer.Option(None, "--max-contacts-per-domain"),
|
|
214
|
+
max_company_records: int | None = typer.Option(None, "--max-company-records"),
|
|
215
|
+
wait: bool = typer.Option(False, "--wait"),
|
|
216
|
+
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout"),
|
|
217
|
+
) -> None:
|
|
218
|
+
from discolike_cli.main import get_client
|
|
219
|
+
|
|
220
|
+
job = get_client(ctx).contacts.generate(
|
|
221
|
+
icp_text=icp_text,
|
|
222
|
+
domains=domain,
|
|
223
|
+
context_mode=context_mode,
|
|
224
|
+
integration_id=integration_id,
|
|
225
|
+
search_provider_id=search_provider_id,
|
|
226
|
+
search_context_size=search_context_size,
|
|
227
|
+
max_contacts_per_domain=max_contacts_per_domain,
|
|
228
|
+
max_company_records=max_company_records,
|
|
229
|
+
)
|
|
230
|
+
run_job(job, wait=wait, timeout=timeout)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from discolike._jobs import Job
|
|
8
|
+
from discolike_cli._output import emit
|
|
9
|
+
from discolike_cli._output import handle_errors
|
|
10
|
+
from discolike_cli._output import run_job
|
|
11
|
+
|
|
12
|
+
DEFAULT_WAIT_TIMEOUT_SECONDS = 900.0
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Run DiscoGen research and manage async tasks")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskFamily(str, enum.Enum):
|
|
18
|
+
discogen = "discogen"
|
|
19
|
+
bulkmatch = "bulkmatch"
|
|
20
|
+
contactmatch = "contactmatch"
|
|
21
|
+
segment = "segment"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("run")
|
|
25
|
+
@handle_errors
|
|
26
|
+
def run_command(
|
|
27
|
+
ctx: typer.Context,
|
|
28
|
+
query: str = typer.Option(..., "--query"),
|
|
29
|
+
domain: list[str] = typer.Option(..., "--domain"),
|
|
30
|
+
integration_id: str | None = typer.Option(None, "--integration-id"),
|
|
31
|
+
web_search: bool | None = typer.Option(None, "--web-search/--no-web-search"),
|
|
32
|
+
context_mode: str | None = typer.Option(None, "--context-mode"),
|
|
33
|
+
include_x_search: bool | None = typer.Option(None, "--include-x-search/--no-include-x-search"),
|
|
34
|
+
search_provider_id: str | None = typer.Option(None, "--search-provider-id"),
|
|
35
|
+
search_context_size: str | None = typer.Option(None, "--search-context-size"),
|
|
36
|
+
wait: bool = typer.Option(False, "--wait"),
|
|
37
|
+
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout"),
|
|
38
|
+
) -> None:
|
|
39
|
+
from discolike_cli.main import get_client
|
|
40
|
+
|
|
41
|
+
job = get_client(ctx).discogen.process(
|
|
42
|
+
query=query,
|
|
43
|
+
domains=domain,
|
|
44
|
+
integration_id=integration_id,
|
|
45
|
+
web_search=web_search,
|
|
46
|
+
context_mode=context_mode,
|
|
47
|
+
include_x_search=include_x_search,
|
|
48
|
+
search_provider_id=search_provider_id,
|
|
49
|
+
search_context_size=search_context_size,
|
|
50
|
+
)
|
|
51
|
+
run_job(job, wait=wait, timeout=timeout)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command("run-personas")
|
|
55
|
+
@handle_errors
|
|
56
|
+
def run_personas_command(
|
|
57
|
+
ctx: typer.Context,
|
|
58
|
+
query: str = typer.Option(..., "--query"),
|
|
59
|
+
persona_id: list[int] = typer.Option(..., "--persona-id"),
|
|
60
|
+
integration_id: str | None = typer.Option(None, "--integration-id"),
|
|
61
|
+
web_search: bool | None = typer.Option(None, "--web-search/--no-web-search"),
|
|
62
|
+
context_mode: str | None = typer.Option(None, "--context-mode"),
|
|
63
|
+
include_x_search: bool | None = typer.Option(None, "--include-x-search/--no-include-x-search"),
|
|
64
|
+
search_provider_id: str | None = typer.Option(None, "--search-provider-id"),
|
|
65
|
+
search_context_size: str | None = typer.Option(None, "--search-context-size"),
|
|
66
|
+
wait: bool = typer.Option(False, "--wait"),
|
|
67
|
+
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout"),
|
|
68
|
+
) -> None:
|
|
69
|
+
from discolike_cli.main import get_client
|
|
70
|
+
|
|
71
|
+
job = get_client(ctx).discogen.process_personas(
|
|
72
|
+
query=query,
|
|
73
|
+
persona_ids=persona_id,
|
|
74
|
+
integration_id=integration_id,
|
|
75
|
+
web_search=web_search,
|
|
76
|
+
context_mode=context_mode,
|
|
77
|
+
include_x_search=include_x_search,
|
|
78
|
+
search_provider_id=search_provider_id,
|
|
79
|
+
search_context_size=search_context_size,
|
|
80
|
+
)
|
|
81
|
+
run_job(job, wait=wait, timeout=timeout)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("models")
|
|
85
|
+
@handle_errors
|
|
86
|
+
def models_command(ctx: typer.Context) -> None:
|
|
87
|
+
from discolike_cli.main import get_client
|
|
88
|
+
|
|
89
|
+
emit(get_client(ctx).discogen.models())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _build_job(ctx: typer.Context, family: TaskFamily, task_id: str) -> Job:
|
|
93
|
+
from discolike_cli.main import get_client
|
|
94
|
+
|
|
95
|
+
client = get_client(ctx)
|
|
96
|
+
if family is TaskFamily.discogen:
|
|
97
|
+
return client.discogen.job(task_id)
|
|
98
|
+
return Job(client._transport, task_family=family.value, task_id=task_id)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("status")
|
|
102
|
+
@handle_errors
|
|
103
|
+
def status_command(
|
|
104
|
+
ctx: typer.Context,
|
|
105
|
+
task_id: str = typer.Argument(...),
|
|
106
|
+
family: TaskFamily = typer.Option(TaskFamily.discogen, "--family"),
|
|
107
|
+
) -> None:
|
|
108
|
+
emit(_build_job(ctx, family, task_id).status())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("cancel")
|
|
112
|
+
@handle_errors
|
|
113
|
+
def cancel_command(
|
|
114
|
+
ctx: typer.Context,
|
|
115
|
+
task_id: str = typer.Argument(...),
|
|
116
|
+
family: TaskFamily = typer.Option(TaskFamily.discogen, "--family"),
|
|
117
|
+
) -> None:
|
|
118
|
+
_build_job(ctx, family, task_id).cancel()
|
|
119
|
+
emit({"cancelled": task_id})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from discolike_cli._output import call_typed
|
|
8
|
+
from discolike_cli._output import emit
|
|
9
|
+
from discolike_cli._output import handle_errors
|
|
10
|
+
|
|
11
|
+
PARAM_SEPARATOR = "="
|
|
12
|
+
LIST_VALUE_SEPARATOR = ","
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_param(raw: str) -> tuple[str, str | list[str]]:
|
|
16
|
+
if PARAM_SEPARATOR not in raw:
|
|
17
|
+
raise typer.BadParameter(f"--param must be in KEY=VALUE form, got {raw!r}")
|
|
18
|
+
key, _, value = raw.partition(PARAM_SEPARATOR)
|
|
19
|
+
if LIST_VALUE_SEPARATOR in value:
|
|
20
|
+
return key, value.split(LIST_VALUE_SEPARATOR)
|
|
21
|
+
return key, value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _merge_params(param: list[str] | None, **options: Any) -> dict[str, Any]: # noqa: ANN401 -- forwarded as **kwargs to typed resource/client methods
|
|
25
|
+
kwargs: dict[str, Any] = dict(_parse_param(raw) for raw in param or [])
|
|
26
|
+
kwargs.update({key: value for key, value in options.items() if value is not None})
|
|
27
|
+
return kwargs
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@handle_errors
|
|
31
|
+
def discover_command(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
icp_prompt: str | None = typer.Option(None),
|
|
34
|
+
icp_text: str | None = typer.Option(None),
|
|
35
|
+
domain: list[str] | None = typer.Option(None),
|
|
36
|
+
phrase_match: list[str] | None = typer.Option(None),
|
|
37
|
+
category: list[str] | None = typer.Option(None),
|
|
38
|
+
country: list[str] | None = typer.Option(None),
|
|
39
|
+
state: list[str] | None = typer.Option(None),
|
|
40
|
+
employee_range: str | None = typer.Option(None),
|
|
41
|
+
revenue_range: str | None = typer.Option(None),
|
|
42
|
+
business_model: list[str] | None = typer.Option(None),
|
|
43
|
+
tech_stack: list[str] | None = typer.Option(None),
|
|
44
|
+
min_digital_footprint: int | None = typer.Option(None),
|
|
45
|
+
max_digital_footprint: int | None = typer.Option(None),
|
|
46
|
+
exclude_domain: list[str] | None = typer.Option(None),
|
|
47
|
+
exclusion_query_id: list[str] | None = typer.Option(None),
|
|
48
|
+
max_records: int | None = typer.Option(None),
|
|
49
|
+
offset: int | None = typer.Option(None),
|
|
50
|
+
fmt: str | None = typer.Option(None, "--format"),
|
|
51
|
+
param: list[str] | None = typer.Option(None, "--param"),
|
|
52
|
+
) -> None:
|
|
53
|
+
from discolike_cli.main import get_client
|
|
54
|
+
|
|
55
|
+
kwargs = _merge_params(
|
|
56
|
+
param,
|
|
57
|
+
icp_prompt=icp_prompt,
|
|
58
|
+
icp_text=icp_text,
|
|
59
|
+
domain=domain,
|
|
60
|
+
phrase_match=phrase_match,
|
|
61
|
+
category=category,
|
|
62
|
+
country=country,
|
|
63
|
+
state=state,
|
|
64
|
+
employee_range=employee_range,
|
|
65
|
+
revenue_range=revenue_range,
|
|
66
|
+
business_model=business_model,
|
|
67
|
+
tech_stack=tech_stack,
|
|
68
|
+
min_digital_footprint=min_digital_footprint,
|
|
69
|
+
max_digital_footprint=max_digital_footprint,
|
|
70
|
+
exclude_domain=exclude_domain,
|
|
71
|
+
exclusion_query_id=exclusion_query_id,
|
|
72
|
+
max_records=max_records,
|
|
73
|
+
offset=offset,
|
|
74
|
+
)
|
|
75
|
+
companies = call_typed(get_client(ctx).discover, **kwargs)
|
|
76
|
+
emit(companies, fmt=fmt)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@handle_errors
|
|
80
|
+
def count_command(
|
|
81
|
+
ctx: typer.Context,
|
|
82
|
+
phrase_match: list[str] | None = typer.Option(None),
|
|
83
|
+
category: list[str] | None = typer.Option(None),
|
|
84
|
+
country: list[str] | None = typer.Option(None),
|
|
85
|
+
state: list[str] | None = typer.Option(None),
|
|
86
|
+
employee_range: str | None = typer.Option(None),
|
|
87
|
+
revenue_range: str | None = typer.Option(None),
|
|
88
|
+
business_model: list[str] | None = typer.Option(None),
|
|
89
|
+
tech_stack: list[str] | None = typer.Option(None),
|
|
90
|
+
min_digital_footprint: int | None = typer.Option(None),
|
|
91
|
+
max_digital_footprint: int | None = typer.Option(None),
|
|
92
|
+
fmt: str | None = typer.Option(None, "--format"),
|
|
93
|
+
param: list[str] | None = typer.Option(None, "--param"),
|
|
94
|
+
) -> None:
|
|
95
|
+
from discolike_cli.main import get_client
|
|
96
|
+
|
|
97
|
+
kwargs = _merge_params(
|
|
98
|
+
param,
|
|
99
|
+
phrase_match=phrase_match,
|
|
100
|
+
category=category,
|
|
101
|
+
country=country,
|
|
102
|
+
state=state,
|
|
103
|
+
employee_range=employee_range,
|
|
104
|
+
revenue_range=revenue_range,
|
|
105
|
+
business_model=business_model,
|
|
106
|
+
tech_stack=tech_stack,
|
|
107
|
+
min_digital_footprint=min_digital_footprint,
|
|
108
|
+
max_digital_footprint=max_digital_footprint,
|
|
109
|
+
)
|
|
110
|
+
count = call_typed(get_client(ctx).count, **kwargs)
|
|
111
|
+
emit(count, fmt=fmt)
|
discolike_cli/enrich.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from discolike_cli._output import emit
|
|
8
|
+
from discolike_cli._output import handle_errors
|
|
9
|
+
from discolike_cli._output import run_job
|
|
10
|
+
|
|
11
|
+
DEFAULT_WAIT_TIMEOUT_SECONDS = 900.0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@handle_errors
|
|
15
|
+
def validate_icp_command(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
icp: str = typer.Option(..., "--icp"),
|
|
18
|
+
domain: list[str] | None = typer.Option(None, "--domain"),
|
|
19
|
+
file: pathlib.Path | None = typer.Option(None, "--file"),
|
|
20
|
+
context_mode: str | None = typer.Option(None, "--context-mode"),
|
|
21
|
+
integration_id: str | None = typer.Option(None, "--integration-id"),
|
|
22
|
+
web_search: bool | None = typer.Option(None, "--web-search/--no-web-search"),
|
|
23
|
+
search_provider_id: str | None = typer.Option(None, "--search-provider-id"),
|
|
24
|
+
wait: bool = typer.Option(False, "--wait"),
|
|
25
|
+
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout"),
|
|
26
|
+
) -> None:
|
|
27
|
+
from discolike_cli.main import get_client
|
|
28
|
+
|
|
29
|
+
if (not domain) == (file is None):
|
|
30
|
+
raise typer.BadParameter("Provide exactly one of --domain or --file")
|
|
31
|
+
|
|
32
|
+
if file is not None:
|
|
33
|
+
domains = [line.strip() for line in file.read_text().splitlines() if line.strip()]
|
|
34
|
+
else:
|
|
35
|
+
assert domain is not None
|
|
36
|
+
domains = domain
|
|
37
|
+
|
|
38
|
+
job = get_client(ctx).validate_icp(
|
|
39
|
+
icp_text=icp,
|
|
40
|
+
domains=domains,
|
|
41
|
+
context_mode=context_mode,
|
|
42
|
+
integration_id=integration_id,
|
|
43
|
+
web_search=web_search,
|
|
44
|
+
search_provider_id=search_provider_id,
|
|
45
|
+
)
|
|
46
|
+
run_job(job, wait=wait, timeout=timeout)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@handle_errors
|
|
50
|
+
def append_command(
|
|
51
|
+
ctx: typer.Context,
|
|
52
|
+
file: pathlib.Path = typer.Argument(...),
|
|
53
|
+
dataset: list[str] | None = typer.Option(None, "--dataset"),
|
|
54
|
+
domain_column: str | None = typer.Option(None, "--domain-column"),
|
|
55
|
+
csv: bool | None = typer.Option(None, "--csv/--no-csv"),
|
|
56
|
+
output: pathlib.Path | None = typer.Option(None, "--output"),
|
|
57
|
+
) -> None:
|
|
58
|
+
from discolike_cli.main import get_client
|
|
59
|
+
|
|
60
|
+
result = get_client(ctx).append(file=file, dataset=dataset, domain_column=domain_column, csv=csv)
|
|
61
|
+
if isinstance(result, bytes):
|
|
62
|
+
if output is None:
|
|
63
|
+
raise typer.BadParameter("--output is required when the response is CSV bytes")
|
|
64
|
+
output.write_bytes(result)
|
|
65
|
+
emit({"written": str(output), "bytes": len(result)})
|
|
66
|
+
return
|
|
67
|
+
emit(result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@handle_errors
|
|
71
|
+
def segment_command(
|
|
72
|
+
ctx: typer.Context,
|
|
73
|
+
domain: list[str] | None = typer.Option(None, "--domain"),
|
|
74
|
+
file: pathlib.Path | None = typer.Option(None, "--file"),
|
|
75
|
+
domain_column: str | None = typer.Option(None, "--domain-column"),
|
|
76
|
+
max_segments: int | None = typer.Option(None, "--max-segments"),
|
|
77
|
+
wait: bool = typer.Option(False, "--wait"),
|
|
78
|
+
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout"),
|
|
79
|
+
) -> None:
|
|
80
|
+
from discolike_cli.main import get_client
|
|
81
|
+
|
|
82
|
+
if (not domain) == (file is None):
|
|
83
|
+
raise typer.BadParameter("Provide exactly one of --domain or --file")
|
|
84
|
+
|
|
85
|
+
job = get_client(ctx).segment(
|
|
86
|
+
domains=domain or None,
|
|
87
|
+
file=file,
|
|
88
|
+
domain_column=domain_column,
|
|
89
|
+
max_segments=max_segments,
|
|
90
|
+
)
|
|
91
|
+
run_job(job, wait=wait, timeout=timeout)
|
discolike_cli/main.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as package_version
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from discolike import Discolike
|
|
9
|
+
from discolike import __version__ as sdk_version
|
|
10
|
+
from discolike_cli import account
|
|
11
|
+
from discolike_cli import auth
|
|
12
|
+
from discolike_cli import company
|
|
13
|
+
from discolike_cli import contacts
|
|
14
|
+
from discolike_cli import discogen
|
|
15
|
+
from discolike_cli import discover
|
|
16
|
+
from discolike_cli import enrich
|
|
17
|
+
from discolike_cli import match
|
|
18
|
+
from discolike_cli import queries
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="discolike",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
help="DiscoLike API from your terminal — https://docs.discolike.com",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
build_client = Discolike
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.callback(invoke_without_command=True)
|
|
30
|
+
def main(
|
|
31
|
+
ctx: typer.Context,
|
|
32
|
+
api_key: str | None = typer.Option(None, envvar="DISCOLIKE_API_KEY"),
|
|
33
|
+
base_url: str | None = typer.Option(None),
|
|
34
|
+
version: bool = typer.Option(False, "--version"),
|
|
35
|
+
) -> None:
|
|
36
|
+
if version:
|
|
37
|
+
typer.echo(f"discolike-cli {package_version('discolike-cli')} (discolike {sdk_version})")
|
|
38
|
+
raise typer.Exit
|
|
39
|
+
ctx.obj = {"api_key": api_key, "base_url": base_url}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_client(ctx: typer.Context) -> Discolike:
|
|
43
|
+
kwargs: dict[str, Any] = {k: v for k, v in ctx.obj.items() if v is not None}
|
|
44
|
+
return build_client(**kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
app.add_typer(auth.app, name="auth")
|
|
48
|
+
app.add_typer(company.app, name="company")
|
|
49
|
+
app.add_typer(contacts.app, name="contacts")
|
|
50
|
+
app.add_typer(discogen.app, name="discogen")
|
|
51
|
+
app.add_typer(queries.app, name="queries")
|
|
52
|
+
app.add_typer(account.app, name="account")
|
|
53
|
+
app.command(name="discover")(discover.discover_command)
|
|
54
|
+
app.command(name="count")(discover.count_command)
|
|
55
|
+
app.command(name="match")(match.match_command)
|
|
56
|
+
app.command(name="extract")(company.extract_command)
|
|
57
|
+
app.command(name="validate-icp")(enrich.validate_icp_command)
|
|
58
|
+
app.command(name="append")(enrich.append_command)
|
|
59
|
+
app.command(name="segment")(enrich.segment_command)
|
discolike_cli/match.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from discolike_cli._output import emit
|
|
8
|
+
from discolike_cli._output import handle_errors
|
|
9
|
+
from discolike_cli._output import run_job
|
|
10
|
+
|
|
11
|
+
DEFAULT_WAIT_TIMEOUT_SECONDS = 900.0
|
|
12
|
+
DEFAULT_NAME_COLUMN = "name"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@handle_errors
|
|
16
|
+
def match_command(
|
|
17
|
+
ctx: typer.Context,
|
|
18
|
+
name: str | None = typer.Argument(None),
|
|
19
|
+
phone: str | None = typer.Option(None),
|
|
20
|
+
city: str | None = typer.Option(None),
|
|
21
|
+
state: str | None = typer.Option(None),
|
|
22
|
+
country: str | None = typer.Option(None),
|
|
23
|
+
zip_code: str | None = typer.Option(None),
|
|
24
|
+
strict: bool | None = typer.Option(None, "--strict/--no-strict"),
|
|
25
|
+
local_mode: bool | None = typer.Option(None, "--local-mode/--no-local-mode"),
|
|
26
|
+
file: pathlib.Path | None = typer.Option(None, "--file"),
|
|
27
|
+
name_column: str = typer.Option(DEFAULT_NAME_COLUMN, "--name-column"),
|
|
28
|
+
wait: bool = typer.Option(False, "--wait"),
|
|
29
|
+
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout"),
|
|
30
|
+
fmt: str | None = typer.Option(None, "--format"),
|
|
31
|
+
) -> None:
|
|
32
|
+
from discolike_cli.main import get_client
|
|
33
|
+
|
|
34
|
+
if (name is None) == (file is None):
|
|
35
|
+
raise typer.BadParameter("Provide exactly one of NAME or --file")
|
|
36
|
+
|
|
37
|
+
client = get_client(ctx)
|
|
38
|
+
if name is not None:
|
|
39
|
+
response = client.match.company(
|
|
40
|
+
name=name,
|
|
41
|
+
phone=phone,
|
|
42
|
+
city=city,
|
|
43
|
+
state=state,
|
|
44
|
+
country=country,
|
|
45
|
+
zip_code=zip_code,
|
|
46
|
+
strict=strict,
|
|
47
|
+
local_mode=local_mode,
|
|
48
|
+
)
|
|
49
|
+
emit(response, fmt=fmt)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
assert file is not None
|
|
53
|
+
job = client.match.bulk(file=file, name_column=name_column, strict=strict, local_mode=local_mode)
|
|
54
|
+
run_job(job, wait=wait, timeout=timeout)
|
discolike_cli/queries.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from discolike_cli._output import emit
|
|
6
|
+
from discolike_cli._output import handle_errors
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Manage saved queries and exclusion lists")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("list")
|
|
12
|
+
@handle_errors
|
|
13
|
+
def list_command(
|
|
14
|
+
ctx: typer.Context,
|
|
15
|
+
max_records: int | None = typer.Option(None, "--max-records"),
|
|
16
|
+
offset: int | None = typer.Option(None, "--offset"),
|
|
17
|
+
action: str | None = typer.Option(None, "--action"),
|
|
18
|
+
tag: list[str] | None = typer.Option(None, "--tag"),
|
|
19
|
+
) -> None:
|
|
20
|
+
from discolike_cli.main import get_client
|
|
21
|
+
|
|
22
|
+
emit(get_client(ctx).queries.list(max_records=max_records, offset=offset, action=action, tags=tag))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("create-exclusion-list")
|
|
26
|
+
@handle_errors
|
|
27
|
+
def create_exclusion_list_command(
|
|
28
|
+
ctx: typer.Context,
|
|
29
|
+
name: str = typer.Option(..., "--name"),
|
|
30
|
+
domain: list[str] | None = typer.Option(None, "--domain"),
|
|
31
|
+
persona_id: list[int] | None = typer.Option(None, "--persona-id"),
|
|
32
|
+
tag: list[str] | None = typer.Option(None, "--tag"),
|
|
33
|
+
) -> None:
|
|
34
|
+
from discolike_cli.main import get_client
|
|
35
|
+
|
|
36
|
+
emit(
|
|
37
|
+
get_client(ctx).queries.create_exclusion_list(
|
|
38
|
+
query_name=name,
|
|
39
|
+
domains=domain,
|
|
40
|
+
persona_ids=persona_id,
|
|
41
|
+
tags=tag,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("update")
|
|
47
|
+
@handle_errors
|
|
48
|
+
def update_command(
|
|
49
|
+
ctx: typer.Context,
|
|
50
|
+
query_id: str = typer.Argument(...),
|
|
51
|
+
name: str | None = typer.Option(None, "--name"),
|
|
52
|
+
tag: list[str] | None = typer.Option(None, "--tag"),
|
|
53
|
+
) -> None:
|
|
54
|
+
from discolike_cli.main import get_client
|
|
55
|
+
|
|
56
|
+
emit(get_client(ctx).queries.update(query_id=query_id, query_name=name, tags=tag))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("delete")
|
|
60
|
+
@handle_errors
|
|
61
|
+
def delete_command(ctx: typer.Context, query_id: str = typer.Argument(...)) -> None:
|
|
62
|
+
from discolike_cli.main import get_client
|
|
63
|
+
|
|
64
|
+
get_client(ctx).queries.delete(query_id=query_id)
|
|
65
|
+
emit({"deleted": query_id})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: discolike-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official CLI for the DiscoLike API
|
|
5
|
+
Project-URL: Homepage, https://www.discolike.com
|
|
6
|
+
Project-URL: Documentation, https://docs.discolike.com
|
|
7
|
+
Project-URL: Repository, https://github.com/Discolike/discolike-python
|
|
8
|
+
Author-email: DiscoLike <support@discolike.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: business-data,cli,discolike,enrichment
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: discolike>=0.1.0
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Requires-Dist: typer>=0.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# discolike-cli
|
|
26
|
+
|
|
27
|
+
Official CLI for the [DiscoLike API](https://www.discolike.com) — the `discolike` command from your terminal. Built on the [`discolike`](https://pypi.org/project/discolike/) SDK.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install discolike-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or run it without installing:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uvx --from discolike-cli discolike --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10+. Installing this package gives you the `discolike` command.
|
|
42
|
+
|
|
43
|
+
## Authentication
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
discolike auth login
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Prompts for an API key (or pass `--api-key`) and verifies it against your account. Create a key at [app.discolike.com/account/management/keys](https://app.discolike.com/account/management/keys). You can also set `DISCOLIKE_API_KEY` in the environment instead.
|
|
50
|
+
|
|
51
|
+
## Quickstart
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
discolike discover --icp-text "managed IT services for SMBs" --country US --max-records 25
|
|
55
|
+
discolike match "Stripe Inc" --city "San Francisco"
|
|
56
|
+
discolike match --file companies.csv --name-column company_name --wait
|
|
57
|
+
discolike count --phrase-match "book a demo" --country US
|
|
58
|
+
discolike company data stripe.com
|
|
59
|
+
discolike extract https://stripe.com/enterprise
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `company`, `contacts`, `discogen`, `queries`, and `account` command groups.
|
|
63
|
+
|
|
64
|
+
### Conventions
|
|
65
|
+
|
|
66
|
+
- Results print as JSON to stdout; errors print as JSON (`error`, `message`, `status_code`) to stderr.
|
|
67
|
+
- Pass `--format table` for a human-readable table — used automatically when stdout is a TTY.
|
|
68
|
+
- Async endpoints (`match --file`, `discogen run`, `discogen run-personas`, `segment`, `validate-icp`) take `--wait` to block until the job finishes. Without it, you get a `task_id` back to poll with `discolike discogen status <task_id> --family <family>`. `append` is synchronous — it returns enriched rows directly (or writes CSV bytes to `--output`).
|
|
69
|
+
|
|
70
|
+
### Exit codes
|
|
71
|
+
|
|
72
|
+
| Exit code | Meaning |
|
|
73
|
+
|---|---|
|
|
74
|
+
| 0 | Success |
|
|
75
|
+
| 1 | Server error or unexpected failure |
|
|
76
|
+
| 2 | Validation error |
|
|
77
|
+
| 3 | Authentication or plan-access error |
|
|
78
|
+
| 4 | Rate limited |
|
|
79
|
+
| 5 | Network error |
|
|
80
|
+
| 6 | Not found |
|
|
81
|
+
|
|
82
|
+
## Links
|
|
83
|
+
|
|
84
|
+
- **API documentation**: [docs.discolike.com](https://docs.discolike.com)
|
|
85
|
+
- **Source**: [github.com/Discolike/discolike-python](https://github.com/Discolike/discolike-python)
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
[MIT](https://github.com/Discolike/discolike-python/blob/main/LICENSE)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
discolike_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
discolike_cli/_output.py,sha256=e1hjf1RTNxiXFJzUd0-3iEk8objeqTX59SgKsUQyfx4,4270
|
|
3
|
+
discolike_cli/account.py,sha256=l6Xnx4D77CsiamTNyhfpJmxErU_TGD8rh-uoJdJ7J30,365
|
|
4
|
+
discolike_cli/auth.py,sha256=Lm099QKljDgOM3EEh29yd7RYZzikXN_G5lAJbriEjKc,1534
|
|
5
|
+
discolike_cli/company.py,sha256=ctE-zqQUAinS4VLqxVm_-WszhPWOZZgMJu94Q-8ldeU,2470
|
|
6
|
+
discolike_cli/contacts.py,sha256=HJafEy3hkWdLuPYfivsZFKfyF35hoBWnY3spmb6WtbQ,8423
|
|
7
|
+
discolike_cli/discogen.py,sha256=3a9MzP_ev1xsYhbVtUQF8GX5fK06QW_WXRiMMYDX_8I,4035
|
|
8
|
+
discolike_cli/discover.py,sha256=HbW_sUIn54QnThoM-4OW-gjKZQn8p6bFNUj_WFfuzHU,4085
|
|
9
|
+
discolike_cli/enrich.py,sha256=tlxlvRNkMnPP6WAnzA9ZfPyeRs6vYivJRr3SGifgLQQ,3200
|
|
10
|
+
discolike_cli/main.py,sha256=yomidRmXoGsXa6MsQ-H9tQyNud0YFPttFi8qDLqhZTM,1943
|
|
11
|
+
discolike_cli/match.py,sha256=MeIiiNyHMMB94763s9NaQqcyUVl9vBT38J9E7uSw1V8,1780
|
|
12
|
+
discolike_cli/queries.py,sha256=WHuhihvRs6SS1HFMe3GOYMJ2wdtf17NsLgIDtIOryBY,1927
|
|
13
|
+
discolike_cli-0.1.0.dist-info/METADATA,sha256=k_Uijf6u-kvxXdd5k8gfU2prR4X9804Uo0u4Hu4x9oU,3330
|
|
14
|
+
discolike_cli-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
15
|
+
discolike_cli-0.1.0.dist-info/entry_points.txt,sha256=j3pX_qdZxguS3cAlIQwl8QYNyG9-eOWg4R1aOpk6zis,53
|
|
16
|
+
discolike_cli-0.1.0.dist-info/RECORD,,
|