discolike-cli 0.1.0__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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ .venv/
3
+ dist/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ *.egg-info/
@@ -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,65 @@
1
+ # discolike-cli
2
+
3
+ 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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install discolike-cli
9
+ ```
10
+
11
+ Or run it without installing:
12
+
13
+ ```bash
14
+ uvx --from discolike-cli discolike --help
15
+ ```
16
+
17
+ Requires Python 3.10+. Installing this package gives you the `discolike` command.
18
+
19
+ ## Authentication
20
+
21
+ ```bash
22
+ discolike auth login
23
+ ```
24
+
25
+ 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.
26
+
27
+ ## Quickstart
28
+
29
+ ```bash
30
+ discolike discover --icp-text "managed IT services for SMBs" --country US --max-records 25
31
+ discolike match "Stripe Inc" --city "San Francisco"
32
+ discolike match --file companies.csv --name-column company_name --wait
33
+ discolike count --phrase-match "book a demo" --country US
34
+ discolike company data stripe.com
35
+ discolike extract https://stripe.com/enterprise
36
+ ```
37
+
38
+ Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `company`, `contacts`, `discogen`, `queries`, and `account` command groups.
39
+
40
+ ### Conventions
41
+
42
+ - Results print as JSON to stdout; errors print as JSON (`error`, `message`, `status_code`) to stderr.
43
+ - Pass `--format table` for a human-readable table — used automatically when stdout is a TTY.
44
+ - 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`).
45
+
46
+ ### Exit codes
47
+
48
+ | Exit code | Meaning |
49
+ |---|---|
50
+ | 0 | Success |
51
+ | 1 | Server error or unexpected failure |
52
+ | 2 | Validation error |
53
+ | 3 | Authentication or plan-access error |
54
+ | 4 | Rate limited |
55
+ | 5 | Network error |
56
+ | 6 | Not found |
57
+
58
+ ## Links
59
+
60
+ - **API documentation**: [docs.discolike.com](https://docs.discolike.com)
61
+ - **Source**: [github.com/Discolike/discolike-python](https://github.com/Discolike/discolike-python)
62
+
63
+ ## License
64
+
65
+ [MIT](https://github.com/Discolike/discolike-python/blob/main/LICENSE)
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "discolike-cli"
7
+ version = "0.1.0"
8
+ description = "Official CLI for the DiscoLike API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "DiscoLike", email = "support@discolike.com" }]
13
+ dependencies = [
14
+ "discolike>=0.1.0",
15
+ "typer>=0.12",
16
+ "rich>=13.0",
17
+ ]
18
+ keywords = ["discolike", "cli", "business-data", "enrichment"]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ ]
29
+
30
+ [project.scripts]
31
+ discolike = "discolike_cli.main:app"
32
+
33
+ [project.urls]
34
+ Homepage = "https://www.discolike.com"
35
+ Documentation = "https://docs.discolike.com"
36
+ Repository = "https://github.com/Discolike/discolike-python"
37
+
38
+ [tool.uv.sources]
39
+ discolike = { workspace = true }
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest>=8",
44
+ "pytest-asyncio>=0.24",
45
+ "ruff>=0.8",
46
+ "ty",
47
+ ]
48
+
49
+ [tool.pytest.ini_options]
50
+ asyncio_mode = "auto"
51
+ testpaths = ["tests"]
File without changes
@@ -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())
@@ -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())
@@ -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})
@@ -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))