discolike-cli 0.4.1__tar.gz → 0.6.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.
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/PKG-INFO +30 -6
- discolike_cli-0.6.0/README.md +103 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/pyproject.toml +3 -4
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/_help.py +3 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/_output.py +3 -2
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/auth.py +2 -2
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/bulk.py +2 -2
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/contacts.py +27 -9
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/discogen.py +1 -1
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/email.py +5 -5
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/main.py +2 -0
- discolike_cli-0.6.0/src/discolike_cli/prospecting.py +438 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/queries.py +2 -2
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_contacts_cli.py +68 -2
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_output.py +3 -3
- discolike_cli-0.6.0/tests/test_prospecting_cli.py +549 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_sdk_parity.py +1 -0
- discolike_cli-0.4.1/README.md +0 -78
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/.gitignore +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/LICENSE +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/__init__.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/_inputs.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/_loopback.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/account.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/company.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/discover.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/enrich.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/match.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/providers.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/py.typed +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/src/discolike_cli/signup.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/conftest.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_account.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_auth.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_auth_oauth.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_bulk_cli.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_company.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_discogen_cli.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_discover.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_email_cli.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_enrich_cli.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_entrypoint.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_file_inputs_cli.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_help.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_inputs.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_match_cli.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_providers_cli.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_queries_cli.py +0 -0
- {discolike_cli-0.4.1 → discolike_cli-0.6.0}/tests/test_signup_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: discolike-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Official CLI for the DiscoLike API
|
|
5
5
|
Project-URL: Homepage, https://www.discolike.com
|
|
6
6
|
Project-URL: Documentation, https://docs.discolike.com
|
|
@@ -17,15 +17,14 @@ Classifier: Intended Audience :: Developers
|
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
19
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
25
24
|
Classifier: Topic :: Utilities
|
|
26
25
|
Classifier: Typing :: Typed
|
|
27
|
-
Requires-Python: >=3.
|
|
28
|
-
Requires-Dist: discolike==0.
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: discolike==0.6.0
|
|
29
28
|
Requires-Dist: rich>=13.0
|
|
30
29
|
Requires-Dist: typer<0.28,>=0.26.1
|
|
31
30
|
Description-Content-Type: text/markdown
|
|
@@ -46,7 +45,7 @@ Or run it without installing:
|
|
|
46
45
|
uvx --from discolike-cli discolike --help
|
|
47
46
|
```
|
|
48
47
|
|
|
49
|
-
Requires Python 3.
|
|
48
|
+
Requires Python 3.11+. Installing this package gives you the `discolike` command.
|
|
50
49
|
|
|
51
50
|
## Authentication
|
|
52
51
|
|
|
@@ -67,7 +66,7 @@ discolike company data stripe.com
|
|
|
67
66
|
discolike extract https://stripe.com/enterprise
|
|
68
67
|
```
|
|
69
68
|
|
|
70
|
-
Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `bulk`, `company`, `contacts`, `discogen`, `queries`, `account`, `search-providers`, and `llm-providers` command groups.
|
|
69
|
+
Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `bulk`, `company`, `contacts`, `discogen`, `prospecting`, `queries`, `account`, `search-providers`, and `llm-providers` command groups.
|
|
71
70
|
|
|
72
71
|
### Volume pulls
|
|
73
72
|
|
|
@@ -81,6 +80,30 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary
|
|
|
81
80
|
|
|
82
81
|
`companies` saves each page as an exclusion list (`<run-name>-round-N`) and excludes it from the next page; rerunning with the same `--out` resumes from the CSV. `contacts` slices the domain list at `10000 / per-company` domains per call and records finished slices in `<out>.checkpoint`. Both keep one call in flight under `--rate-limit` (default 10/min, the Pro rate on `/discover` and `/contacts`), retry on 429/5xx, and print a JSON summary at the end. Filters come from `--params-file`, `--param` and the common flags; the paging fields are managed for you.
|
|
83
82
|
|
|
83
|
+
### Managed prospecting
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1
|
|
87
|
+
discolike prospecting start --brief "Find lookalikes of our customers and their CTOs" --customers-file customers.csv --idempotency-key seeded-1
|
|
88
|
+
discolike prospecting wait RUN_ID
|
|
89
|
+
# Review the proposed plan; change its engines or size if needed (returns a new plan_version):
|
|
90
|
+
discolike prospecting plan RUN_ID --plan-version 1 --contact-integration-id LLM_ID --search-provider-id SEARCH_ID
|
|
91
|
+
# Then approve the exact version you saw:
|
|
92
|
+
discolike prospecting approve RUN_ID --plan-version 2
|
|
93
|
+
discolike prospecting list --limit 20
|
|
94
|
+
discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1
|
|
95
|
+
discolike prospecting status RUN_ID --events-after 12 --messages-after 8 --limit 100
|
|
96
|
+
discolike prospecting cancel RUN_ID
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`wait` returns on `proposed`, `needs_input`, `completed`, `failed`, or `cancelled`. A timeout stops polling only. Inspect status and stop reason; completion does not guarantee full coverage. Message replies arrive through `status --messages-after`; follow `next_message_seq` and `reply_pending`. A "segment these" request keeps `reply_pending` true until the segments message is posted, which can take more than an hour. If grouping stops without an outcome, the flag clears on its own after about 90 minutes.
|
|
100
|
+
|
|
101
|
+
`plan` changes a proposed plan before approval and sends only the flags you pass: `--contact-integration-id`, `--validation-integration-id` and `--search-provider-id` take option ids from the plan message's `contact_engine`, `company_check_engine` and `search_provider` (`--search-provider-id none` skips web research), and `--target-companies`, `--contacts-per-company`, `--deliverable`, `--goal`, `--max-spend-usd` and `--max-provider-spend-usd` (`0` removes a limit) resize or re-scope it. When your organization has several AI or search providers and none is the default, the plan shows that engine with `selected: null` and `approve` returns 409 until `plan` picks one. The run comes back re-estimated at a new `plan_version`; approve that one.
|
|
102
|
+
|
|
103
|
+
`start` and `approve` default to pausing at checkpoints, like the web chat: a pilot check on large lists (`pilot`), a search drifting off target (`tail_quality`), candidates running out short of the target (`short`), the target being met (`target_reached`), another round of companies when the ones checked yield too few contacts (`top_up`), and, on wave runs, the researched contacts found before their emails are looked up (`contacts_review`, asked with `--auto` too; contacts already in DiscoLike's database get their lookups during research, which approving the plan starts; picking "Find emails" by number sends the question's `seq`, which the API needs to accept it). Pass `--auto` to never pause (except at a `top_up` round that would pass a spending limit); a poor pilot is then sharpened once and the run continues with a notice, stopping with `pilot_failed` only if the re-pilot fit is still under 20%. If sharpening itself fails, the run continues on the original criteria. On a terminal, `wait` shows the question, any sample companies and numbered replies at a checkpoint, sends your pick or your own text, and keeps waiting. Without a terminal, or with `--no-input`, it prints the run on stdout, a `needs_input` envelope (`message`, `stop_reason`, `suggested_replies`, `sample`) on stderr, and exits 7; answer with `prospecting message --text "<reply>"` (add `--question-seq` with the question's `seq` to answer only that question) and run `wait` again.
|
|
104
|
+
|
|
105
|
+
Omit `--target-companies` and `--contacts-per-company` to infer counts from the brief (fallback 1,000 and 3; up to 10 contacts per company). Explicit values override the text. `--deliverable accounts` returns the checked companies only, with no people; `--goal companies` takes `--target-companies` matching companies and works only those instead of adding rounds until the contact target is met. Both default to `leads`. `--max-candidates` and `--max-actions` are automatic when omitted or `0`; their maxima are 100,000 and 10,000. Targets allow up to 10,000 companies, status pages up to 500 rows, and lists up to 50 runs. Work caps do not cap provider charges.
|
|
106
|
+
|
|
84
107
|
### Conventions
|
|
85
108
|
|
|
86
109
|
- Results print as JSON to stdout; errors print as JSON (`error`, `message`, `status_code`) to stderr.
|
|
@@ -99,6 +122,7 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary
|
|
|
99
122
|
| 4 | Rate limited |
|
|
100
123
|
| 5 | Network error |
|
|
101
124
|
| 6 | Not found |
|
|
125
|
+
| 7 | Needs input: `prospecting wait` reached a checkpoint with no terminal to ask (or `--no-input`) |
|
|
102
126
|
|
|
103
127
|
## Links
|
|
104
128
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# discolike-cli
|
|
2
|
+
|
|
3
|
+
Official CLI for the [DiscoLike API](https://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.11+. Installing this package gives you the `discolike` command.
|
|
18
|
+
|
|
19
|
+
## Authentication
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
discolike auth login
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Opens your browser to log in (add `--no-browser` to print the URL instead, `--port` to pin the loopback port when forwarding over SSH). To use an API key instead, pass `--api-key KEY` or `--method api_key` to be prompted; 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-prompt "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`, `bulk`, `company`, `contacts`, `discogen`, `prospecting`, `queries`, `account`, `search-providers`, and `llm-providers` command groups.
|
|
39
|
+
|
|
40
|
+
### Volume pulls
|
|
41
|
+
|
|
42
|
+
`discolike bulk` walks past the 10,000-per-search ceiling and pulls contacts for a whole domain list into one CSV, with checkpoint and resume:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
discolike bulk companies --params-file form.json --max-companies 50000 --run-name agencies --out companies.csv
|
|
46
|
+
discolike bulk estimate --domains-file companies.csv --per-company 10 # free size check
|
|
47
|
+
discolike bulk contacts --domains-file companies.csv --per-company 10 --summary "growth marketing" --out contacts.csv
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`companies` saves each page as an exclusion list (`<run-name>-round-N`) and excludes it from the next page; rerunning with the same `--out` resumes from the CSV. `contacts` slices the domain list at `10000 / per-company` domains per call and records finished slices in `<out>.checkpoint`. Both keep one call in flight under `--rate-limit` (default 10/min, the Pro rate on `/discover` and `/contacts`), retry on 429/5xx, and print a JSON summary at the end. Filters come from `--params-file`, `--param` and the common flags; the paging fields are managed for you.
|
|
51
|
+
|
|
52
|
+
### Managed prospecting
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1
|
|
56
|
+
discolike prospecting start --brief "Find lookalikes of our customers and their CTOs" --customers-file customers.csv --idempotency-key seeded-1
|
|
57
|
+
discolike prospecting wait RUN_ID
|
|
58
|
+
# Review the proposed plan; change its engines or size if needed (returns a new plan_version):
|
|
59
|
+
discolike prospecting plan RUN_ID --plan-version 1 --contact-integration-id LLM_ID --search-provider-id SEARCH_ID
|
|
60
|
+
# Then approve the exact version you saw:
|
|
61
|
+
discolike prospecting approve RUN_ID --plan-version 2
|
|
62
|
+
discolike prospecting list --limit 20
|
|
63
|
+
discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1
|
|
64
|
+
discolike prospecting status RUN_ID --events-after 12 --messages-after 8 --limit 100
|
|
65
|
+
discolike prospecting cancel RUN_ID
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`wait` returns on `proposed`, `needs_input`, `completed`, `failed`, or `cancelled`. A timeout stops polling only. Inspect status and stop reason; completion does not guarantee full coverage. Message replies arrive through `status --messages-after`; follow `next_message_seq` and `reply_pending`. A "segment these" request keeps `reply_pending` true until the segments message is posted, which can take more than an hour. If grouping stops without an outcome, the flag clears on its own after about 90 minutes.
|
|
69
|
+
|
|
70
|
+
`plan` changes a proposed plan before approval and sends only the flags you pass: `--contact-integration-id`, `--validation-integration-id` and `--search-provider-id` take option ids from the plan message's `contact_engine`, `company_check_engine` and `search_provider` (`--search-provider-id none` skips web research), and `--target-companies`, `--contacts-per-company`, `--deliverable`, `--goal`, `--max-spend-usd` and `--max-provider-spend-usd` (`0` removes a limit) resize or re-scope it. When your organization has several AI or search providers and none is the default, the plan shows that engine with `selected: null` and `approve` returns 409 until `plan` picks one. The run comes back re-estimated at a new `plan_version`; approve that one.
|
|
71
|
+
|
|
72
|
+
`start` and `approve` default to pausing at checkpoints, like the web chat: a pilot check on large lists (`pilot`), a search drifting off target (`tail_quality`), candidates running out short of the target (`short`), the target being met (`target_reached`), another round of companies when the ones checked yield too few contacts (`top_up`), and, on wave runs, the researched contacts found before their emails are looked up (`contacts_review`, asked with `--auto` too; contacts already in DiscoLike's database get their lookups during research, which approving the plan starts; picking "Find emails" by number sends the question's `seq`, which the API needs to accept it). Pass `--auto` to never pause (except at a `top_up` round that would pass a spending limit); a poor pilot is then sharpened once and the run continues with a notice, stopping with `pilot_failed` only if the re-pilot fit is still under 20%. If sharpening itself fails, the run continues on the original criteria. On a terminal, `wait` shows the question, any sample companies and numbered replies at a checkpoint, sends your pick or your own text, and keeps waiting. Without a terminal, or with `--no-input`, it prints the run on stdout, a `needs_input` envelope (`message`, `stop_reason`, `suggested_replies`, `sample`) on stderr, and exits 7; answer with `prospecting message --text "<reply>"` (add `--question-seq` with the question's `seq` to answer only that question) and run `wait` again.
|
|
73
|
+
|
|
74
|
+
Omit `--target-companies` and `--contacts-per-company` to infer counts from the brief (fallback 1,000 and 3; up to 10 contacts per company). Explicit values override the text. `--deliverable accounts` returns the checked companies only, with no people; `--goal companies` takes `--target-companies` matching companies and works only those instead of adding rounds until the contact target is met. Both default to `leads`. `--max-candidates` and `--max-actions` are automatic when omitted or `0`; their maxima are 100,000 and 10,000. Targets allow up to 10,000 companies, status pages up to 500 rows, and lists up to 50 runs. Work caps do not cap provider charges.
|
|
75
|
+
|
|
76
|
+
### Conventions
|
|
77
|
+
|
|
78
|
+
- Results print as JSON to stdout; errors print as JSON (`error`, `message`, `status_code`) to stderr.
|
|
79
|
+
- Pass `--format table` for a human-readable table — used automatically when stdout is a TTY.
|
|
80
|
+
- Volume inputs come from files: `--domains-file companies.csv` (a `domain` column, or one domain per line) on `queries create-exclusion-list`, `contacts discover|search|count|generate`, `discogen run` and `validate-icp`; `--params-file form.json` (a JSON object of API parameter names, e.g. an app form) on `discover`, `count` and `contacts discover|search|count`; `--exclude-domains-file` on `discover`. Precedence: file < `--param` < flags.
|
|
81
|
+
- 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`).
|
|
82
|
+
|
|
83
|
+
### Exit codes
|
|
84
|
+
|
|
85
|
+
| Exit code | Meaning |
|
|
86
|
+
|---|---|
|
|
87
|
+
| 0 | Success |
|
|
88
|
+
| 1 | Server error or unexpected failure |
|
|
89
|
+
| 2 | Validation error |
|
|
90
|
+
| 3 | Authentication or plan-access error |
|
|
91
|
+
| 4 | Rate limited |
|
|
92
|
+
| 5 | Network error |
|
|
93
|
+
| 6 | Not found |
|
|
94
|
+
| 7 | Needs input: `prospecting wait` reached a checkpoint with no terminal to ask (or `--no-input`) |
|
|
95
|
+
|
|
96
|
+
## Links
|
|
97
|
+
|
|
98
|
+
- **API documentation**: [docs.discolike.com](https://docs.discolike.com)
|
|
99
|
+
- **Source**: [github.com/Discolike/discolike-python](https://github.com/Discolike/discolike-python)
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
[MIT](https://github.com/Discolike/discolike-python/blob/main/LICENSE)
|
|
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "discolike-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Official CLI for the DiscoLike API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
|
-
requires-python = ">=3.
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
13
|
authors = [{ name = "DiscoLike", email = "support@discolike.com" }]
|
|
14
14
|
dependencies = [
|
|
15
|
-
"discolike==0.
|
|
15
|
+
"discolike==0.6.0",
|
|
16
16
|
"typer>=0.26.1,<0.28",
|
|
17
17
|
"rich>=13.0",
|
|
18
18
|
]
|
|
@@ -21,7 +21,6 @@ classifiers = [
|
|
|
21
21
|
"Development Status :: 4 - Beta",
|
|
22
22
|
"Intended Audience :: Developers",
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
|
-
"Programming Language :: Python :: 3.10",
|
|
25
24
|
"Programming Language :: Python :: 3.11",
|
|
26
25
|
"Programming Language :: Python :: 3.12",
|
|
27
26
|
"Programming Language :: Python :: 3.13",
|
|
@@ -74,6 +74,9 @@ Exit codes:
|
|
|
74
74
|
4 rate_limited HTTP 429; wait "retry_after" seconds, then retry
|
|
75
75
|
5 network_error could not reach the API
|
|
76
76
|
6 not_found HTTP 404
|
|
77
|
+
7 needs_input `prospecting wait` stopped at a checkpoint with no terminal to ask
|
|
78
|
+
(or --no-input): the run is on stdout, the question and
|
|
79
|
+
"suggested_replies" in the stderr envelope
|
|
77
80
|
|
|
78
81
|
Environment:
|
|
79
82
|
{ENV_API_KEY} API key; overrides the config file written by `discolike auth login`.
|
|
@@ -43,6 +43,7 @@ EXIT_CODES: dict[type, int] = {
|
|
|
43
43
|
NotFoundError: 6,
|
|
44
44
|
}
|
|
45
45
|
DEFAULT_EXIT_CODE = 1
|
|
46
|
+
NEEDS_INPUT_EXIT_CODE = 7
|
|
46
47
|
|
|
47
48
|
# Stable, snake_case error codes for agents and scripts to branch on. The class
|
|
48
49
|
# name in `error` is kept for backwards compatibility; `code` is the contract.
|
|
@@ -82,7 +83,7 @@ class SupportsWait(Protocol):
|
|
|
82
83
|
task_id: str
|
|
83
84
|
task_family: str
|
|
84
85
|
|
|
85
|
-
def wait(self, *,
|
|
86
|
+
def wait(self, *, max_wait: float, on_poll: Callable[[_JobStatusLike], None] | None = None) -> _JobStatusLike: ...
|
|
86
87
|
|
|
87
88
|
|
|
88
89
|
def _normalize(data: Any) -> Any: # noqa: ANN401 -- accepts arbitrary JSON-serializable CLI output data
|
|
@@ -203,5 +204,5 @@ def run_job(job: SupportsWait, *, wait: bool, timeout: float, fmt: str | None =
|
|
|
203
204
|
def _on_poll(status: _JobStatusLike) -> None:
|
|
204
205
|
sys.stderr.write(f"progress: {status.progress}%\n")
|
|
205
206
|
|
|
206
|
-
final = job.wait(
|
|
207
|
+
final = job.wait(max_wait=timeout, on_poll=_on_poll)
|
|
207
208
|
emit(final.results if final.results is not None else final.to_dict(), fmt=fmt)
|
|
@@ -4,8 +4,8 @@ import json
|
|
|
4
4
|
import secrets
|
|
5
5
|
import sys
|
|
6
6
|
import webbrowser
|
|
7
|
+
from datetime import UTC
|
|
7
8
|
from datetime import datetime
|
|
8
|
-
from datetime import timezone
|
|
9
9
|
from typing import Any
|
|
10
10
|
from typing import NoReturn
|
|
11
11
|
from urllib.parse import urlparse
|
|
@@ -88,7 +88,7 @@ def _offer_signup(ctx: typer.Context) -> None:
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def _iso(epoch_seconds: float) -> str:
|
|
91
|
-
return datetime.fromtimestamp(epoch_seconds, tz=
|
|
91
|
+
return datetime.fromtimestamp(epoch_seconds, tz=UTC).isoformat()
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def _global_key_source(ctx: typer.Context) -> str:
|
|
@@ -382,8 +382,8 @@ def _persona_ids_in(path: pathlib.Path) -> set[str]:
|
|
|
382
382
|
|
|
383
383
|
|
|
384
384
|
ICP_PROMPT_HELP = "Natural-language ICP prompt used to derive contact filters."
|
|
385
|
-
SUMMARY_HELP = "
|
|
386
|
-
NEGATE_SUMMARY_HELP = "Exclude contacts
|
|
385
|
+
SUMMARY_HELP = "Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it. Ranks who comes back per company."
|
|
386
|
+
NEGATE_SUMMARY_HELP = "Exclude contacts whose profile summary contains every word of any one term, in any order."
|
|
387
387
|
HAS_EMAIL_HELP = "Only contacts with an email address (on by default)."
|
|
388
388
|
|
|
389
389
|
|
|
@@ -30,15 +30,25 @@ WAIT_HELP = "Block until the job finishes, streaming progress to stderr."
|
|
|
30
30
|
TIMEOUT_HELP = "Max seconds to wait with --wait."
|
|
31
31
|
PARAM_HELP = "Extra key=value query parameter forwarded to the SDK (repeatable)."
|
|
32
32
|
JOBSTART_DATE_HELP = "Job start date filter: min date or 'min,max' range, e.g. 2025-01-01 or 2025-01-01,2025-06-30."
|
|
33
|
-
SUMMARY_HELP = "
|
|
34
|
-
NEGATE_SUMMARY_HELP = "Exclude contacts
|
|
33
|
+
SUMMARY_HELP = "Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it."
|
|
34
|
+
NEGATE_SUMMARY_HELP = "Exclude contacts whose profile summary contains every word of any one term, in any order."
|
|
35
35
|
NAME_HELP = "Filter by contact name (partial match supported)."
|
|
36
36
|
SKILLS_HELP = "Filter by skill (repeatable)."
|
|
37
37
|
FILTER_STATE_HELP = "Filter by company state/region (repeatable)."
|
|
38
38
|
NEGATE_FILTER_STATE_HELP = "Exclude contacts at companies in this state (repeatable)."
|
|
39
39
|
PERSON_STATE_HELP = "Filter by contact state/region (repeatable)."
|
|
40
40
|
PERSONA_ID_HELP = "Filter by persona ID (repeatable)."
|
|
41
|
-
EMAIL_VALIDATED_HELP =
|
|
41
|
+
EMAIL_VALIDATED_HELP = (
|
|
42
|
+
"Deprecated. Treated as has_email: contact search no longer filters on email verification; verify emails instead."
|
|
43
|
+
)
|
|
44
|
+
EMAIL_VALIDATED_WARNING = (
|
|
45
|
+
"warning: --email-validated is deprecated. Treated as has_email: "
|
|
46
|
+
"contact search no longer filters on email verification; verify emails instead."
|
|
47
|
+
)
|
|
48
|
+
NO_EMAIL_VALIDATED_WARNING = (
|
|
49
|
+
"warning: --no-email-validated is deprecated and has no effect. "
|
|
50
|
+
"Contact search no longer filters on email verification; verify emails instead."
|
|
51
|
+
)
|
|
42
52
|
HAS_PHONE_HELP = "Only contacts with (or without) a phone number."
|
|
43
53
|
HAS_MOBILE_HELP = "Only contacts with (or without) a mobile phone number."
|
|
44
54
|
HAS_LINKEDIN_HELP = "Only contacts with (or without) a LinkedIn profile."
|
|
@@ -50,6 +60,17 @@ RESULTS_BY_COMPANY_HELP = "Maximum contacts per company domain (default 5; 0 rem
|
|
|
50
60
|
INCLUDE_SEARCH_CONTACTS_HELP = "Include contacts from the search index (broader coverage)."
|
|
51
61
|
CONSENSUS_HELP = "Number of query vectors to combine for consensus search."
|
|
52
62
|
|
|
63
|
+
|
|
64
|
+
def _email_validated_as_has_email(*, email_validated: bool | None, has_email: bool | None) -> bool | None:
|
|
65
|
+
if email_validated is None:
|
|
66
|
+
return has_email
|
|
67
|
+
if not email_validated:
|
|
68
|
+
typer.echo(NO_EMAIL_VALIDATED_WARNING, err=True)
|
|
69
|
+
return has_email
|
|
70
|
+
typer.echo(EMAIL_VALIDATED_WARNING, err=True)
|
|
71
|
+
return True if has_email is None else has_email
|
|
72
|
+
|
|
73
|
+
|
|
53
74
|
app = typer.Typer(
|
|
54
75
|
help="Find contacts: search and count by filters, look up or match individuals, and run bulk/generative discovery jobs."
|
|
55
76
|
)
|
|
@@ -137,8 +158,7 @@ def search_command(
|
|
|
137
158
|
filter_state=filter_state,
|
|
138
159
|
negate_filter_state=negate_filter_state,
|
|
139
160
|
employee_range=employee_range,
|
|
140
|
-
has_email=has_email,
|
|
141
|
-
email_validated=email_validated,
|
|
161
|
+
has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
|
|
142
162
|
has_phone=has_phone,
|
|
143
163
|
has_mobile=has_mobile,
|
|
144
164
|
has_linkedin=has_linkedin,
|
|
@@ -238,8 +258,7 @@ def count_command(
|
|
|
238
258
|
filter_state=filter_state,
|
|
239
259
|
negate_filter_state=negate_filter_state,
|
|
240
260
|
employee_range=employee_range,
|
|
241
|
-
has_email=has_email,
|
|
242
|
-
email_validated=email_validated,
|
|
261
|
+
has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
|
|
243
262
|
has_phone=has_phone,
|
|
244
263
|
has_mobile=has_mobile,
|
|
245
264
|
has_linkedin=has_linkedin,
|
|
@@ -413,8 +432,7 @@ def discover_command(
|
|
|
413
432
|
filter_state=filter_state,
|
|
414
433
|
negate_filter_state=negate_filter_state,
|
|
415
434
|
employee_range=employee_range,
|
|
416
|
-
has_email=has_email,
|
|
417
|
-
email_validated=email_validated,
|
|
435
|
+
has_email=_email_validated_as_has_email(email_validated=email_validated, has_email=has_email),
|
|
418
436
|
has_phone=has_phone,
|
|
419
437
|
has_mobile=has_mobile,
|
|
420
438
|
has_linkedin=has_linkedin,
|
|
@@ -44,7 +44,7 @@ TASK_ID_HELP = "Task ID returned when the job was started."
|
|
|
44
44
|
app = typer.Typer(help="Run DiscoGen research jobs and check status of or cancel any async task (see --family)")
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class TaskFamily(
|
|
47
|
+
class TaskFamily(enum.StrEnum):
|
|
48
48
|
discogen = "discogen"
|
|
49
49
|
bulkmatch = "bulkmatch"
|
|
50
50
|
contactmatch = "contactmatch"
|
|
@@ -71,7 +71,7 @@ def _fetch_batch_snapshot(batch: EmailBatch) -> EmailBatchResults:
|
|
|
71
71
|
"""Fetch the batch results once, whether or not the batch has finished."""
|
|
72
72
|
snapshot: dict[str, EmailBatchResults] = {}
|
|
73
73
|
try:
|
|
74
|
-
return batch.results(
|
|
74
|
+
return batch.results(max_wait=0, on_poll=lambda results: snapshot.update(latest=results))
|
|
75
75
|
except JobTimeoutError:
|
|
76
76
|
return snapshot["latest"]
|
|
77
77
|
|
|
@@ -101,7 +101,7 @@ def find_command(
|
|
|
101
101
|
if not wait:
|
|
102
102
|
emit({"job_id": job.job_id, "hint": f"poll with: discolike email job {job.job_id}"})
|
|
103
103
|
return
|
|
104
|
-
emit(job.wait(
|
|
104
|
+
emit(job.wait(max_wait=timeout, on_poll=_job_status_to_stderr), fmt=fmt)
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
@app.command("find-batch")
|
|
@@ -136,7 +136,7 @@ def find_batch_command(
|
|
|
136
136
|
if not wait:
|
|
137
137
|
emit({"batch_id": batch.batch_id, "hint": f"fetch with: discolike email results {batch.batch_id}"})
|
|
138
138
|
return
|
|
139
|
-
emit(batch.results(
|
|
139
|
+
emit(batch.results(max_wait=timeout, on_poll=_batch_progress_to_stderr), fmt=fmt)
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
@app.command("results")
|
|
@@ -156,7 +156,7 @@ def results_command(
|
|
|
156
156
|
raise typer.BadParameter(f"--kind must be one of: {', '.join(EMAIL_KINDS)}")
|
|
157
157
|
batch = get_client(ctx).email.batch(batch_id, kind=kind) # type: ignore[arg-type]
|
|
158
158
|
if wait:
|
|
159
|
-
emit(batch.results(
|
|
159
|
+
emit(batch.results(max_wait=timeout, on_poll=_batch_progress_to_stderr), fmt=fmt)
|
|
160
160
|
return
|
|
161
161
|
emit(_fetch_batch_snapshot(batch), fmt=fmt)
|
|
162
162
|
|
|
@@ -175,6 +175,6 @@ def job_command(
|
|
|
175
175
|
|
|
176
176
|
job = get_client(ctx).email.job(job_id)
|
|
177
177
|
if wait:
|
|
178
|
-
emit(job.wait(
|
|
178
|
+
emit(job.wait(max_wait=timeout, on_poll=_job_status_to_stderr), fmt=fmt)
|
|
179
179
|
return
|
|
180
180
|
emit(job.status(), fmt=fmt)
|
|
@@ -23,6 +23,7 @@ from discolike_cli import discover
|
|
|
23
23
|
from discolike_cli import email
|
|
24
24
|
from discolike_cli import enrich
|
|
25
25
|
from discolike_cli import match
|
|
26
|
+
from discolike_cli import prospecting
|
|
26
27
|
from discolike_cli import providers
|
|
27
28
|
from discolike_cli import queries
|
|
28
29
|
from discolike_cli import signup
|
|
@@ -85,6 +86,7 @@ app.add_typer(contacts.app, name="contacts")
|
|
|
85
86
|
app.add_typer(discogen.app, name="discogen")
|
|
86
87
|
app.add_typer(email.app, name="email")
|
|
87
88
|
app.add_typer(queries.app, name="queries")
|
|
89
|
+
app.add_typer(prospecting.app, name="prospecting")
|
|
88
90
|
app.add_typer(account.app, name="account")
|
|
89
91
|
app.add_typer(providers.search_providers_app, name="search-providers")
|
|
90
92
|
app.add_typer(providers.llm_providers_app, name="llm-providers")
|