discolike-cli 0.5.0__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.5.0 → discolike_cli-0.6.0}/PKG-INFO +8 -4
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/README.md +6 -2
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/pyproject.toml +2 -2
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/contacts.py +25 -7
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/prospecting.py +76 -1
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_contacts_cli.py +68 -2
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_prospecting_cli.py +92 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/.gitignore +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/LICENSE +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/__init__.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/_help.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/_inputs.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/_loopback.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/_output.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/account.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/auth.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/bulk.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/company.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/discogen.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/discover.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/email.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/enrich.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/main.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/match.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/providers.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/py.typed +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/queries.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/src/discolike_cli/signup.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/conftest.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_account.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_auth.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_auth_oauth.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_bulk_cli.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_company.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_discogen_cli.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_discover.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_email_cli.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_enrich_cli.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_entrypoint.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_file_inputs_cli.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_help.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_inputs.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_match_cli.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_output.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_providers_cli.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_queries_cli.py +0 -0
- {discolike_cli-0.5.0 → discolike_cli-0.6.0}/tests/test_sdk_parity.py +0 -0
- {discolike_cli-0.5.0 → 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
|
|
@@ -24,7 +24,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
24
24
|
Classifier: Topic :: Utilities
|
|
25
25
|
Classifier: Typing :: Typed
|
|
26
26
|
Requires-Python: >=3.11
|
|
27
|
-
Requires-Dist: discolike==0.
|
|
27
|
+
Requires-Dist: discolike==0.6.0
|
|
28
28
|
Requires-Dist: rich>=13.0
|
|
29
29
|
Requires-Dist: typer<0.28,>=0.26.1
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
@@ -86,8 +86,10 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary
|
|
|
86
86
|
discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1
|
|
87
87
|
discolike prospecting start --brief "Find lookalikes of our customers and their CTOs" --customers-file customers.csv --idempotency-key seeded-1
|
|
88
88
|
discolike prospecting wait RUN_ID
|
|
89
|
-
# Review the proposed plan
|
|
90
|
-
discolike prospecting
|
|
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
|
|
91
93
|
discolike prospecting list --limit 20
|
|
92
94
|
discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1
|
|
93
95
|
discolike prospecting status RUN_ID --events-after 12 --messages-after 8 --limit 100
|
|
@@ -96,6 +98,8 @@ discolike prospecting cancel RUN_ID
|
|
|
96
98
|
|
|
97
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.
|
|
98
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
|
+
|
|
99
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.
|
|
100
104
|
|
|
101
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.
|
|
@@ -55,8 +55,10 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary
|
|
|
55
55
|
discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1
|
|
56
56
|
discolike prospecting start --brief "Find lookalikes of our customers and their CTOs" --customers-file customers.csv --idempotency-key seeded-1
|
|
57
57
|
discolike prospecting wait RUN_ID
|
|
58
|
-
# Review the proposed plan
|
|
59
|
-
discolike prospecting
|
|
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
|
|
60
62
|
discolike prospecting list --limit 20
|
|
61
63
|
discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1
|
|
62
64
|
discolike prospecting status RUN_ID --events-after 12 --messages-after 8 --limit 100
|
|
@@ -65,6 +67,8 @@ discolike prospecting cancel RUN_ID
|
|
|
65
67
|
|
|
66
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.
|
|
67
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
|
+
|
|
68
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.
|
|
69
73
|
|
|
70
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.
|
|
@@ -4,7 +4,7 @@ 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"
|
|
@@ -12,7 +12,7 @@ license-files = ["LICENSE"]
|
|
|
12
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
|
]
|
|
@@ -38,7 +38,17 @@ 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,
|
|
@@ -19,6 +19,7 @@ from discolike.requests import ProspectingBrief
|
|
|
19
19
|
from discolike.requests import ProspectingGetParams
|
|
20
20
|
from discolike.requests import ProspectingListParams
|
|
21
21
|
from discolike.requests import ProspectingMessageRequest
|
|
22
|
+
from discolike.requests import ProspectingPlanSettings
|
|
22
23
|
from discolike.resources.prospecting import ProspectingMessage
|
|
23
24
|
from discolike.resources.prospecting import ProspectingRun
|
|
24
25
|
from discolike_cli._inputs import read_domains_file
|
|
@@ -43,6 +44,18 @@ CUSTOMERS_FILE_HELP = (
|
|
|
43
44
|
SEED_SEGMENT_HELP = (
|
|
44
45
|
"A customer segment id from the plan card to expand (repeatable); omitted keeps the card's selection."
|
|
45
46
|
)
|
|
47
|
+
PLAN_DELIVERABLE_HELP = "leads finds people at the companies; accounts returns the checked companies only."
|
|
48
|
+
PLAN_GOAL_HELP = (
|
|
49
|
+
"leads keeps adding rounds until the target is met; companies works only --target-companies matching companies."
|
|
50
|
+
)
|
|
51
|
+
SEARCH_PROVIDER_HELP = "An option id from the plan's search_provider; none skips web research."
|
|
52
|
+
SPEND_HELP_SUFFIX = "0 removes the limit."
|
|
53
|
+
PLAN_HELP = (
|
|
54
|
+
"Change a proposed plan's engines, size, deliverable or spending limits before approving it. Flags left out "
|
|
55
|
+
"keep their current choice; engine ids come from the latest plan message's contact_engine, "
|
|
56
|
+
"company_check_engine and search_provider options, and an engine shown with selected null must be picked "
|
|
57
|
+
"here before approve. The run comes back re-estimated at a new plan_version: approve that one."
|
|
58
|
+
)
|
|
46
59
|
RESULTS_SEGMENT_HELP = "Group the finished results into segments; omitted keeps the brief's setting."
|
|
47
60
|
QUESTION_SEQ_HELP = (
|
|
48
61
|
"The seq of the question this suggested reply answers; if a newer question was posted since, it answers none."
|
|
@@ -322,7 +335,10 @@ def approve_command(
|
|
|
322
335
|
seed_segment: list[int] | None = typer.Option(None, "--seed-segment", help=SEED_SEGMENT_HELP),
|
|
323
336
|
segment: bool | None = typer.Option(None, "--segment/--no-segment", help=RESULTS_SEGMENT_HELP),
|
|
324
337
|
) -> None:
|
|
325
|
-
"""Approve the reviewed plan version and start research.
|
|
338
|
+
"""Approve the reviewed plan version and start research.
|
|
339
|
+
|
|
340
|
+
A plan with an engine left unselected (selected null) returns 409 until `prospecting plan` picks one.
|
|
341
|
+
"""
|
|
326
342
|
from discolike_cli.main import get_client
|
|
327
343
|
|
|
328
344
|
emit(
|
|
@@ -342,6 +358,65 @@ def approve_command(
|
|
|
342
358
|
)
|
|
343
359
|
|
|
344
360
|
|
|
361
|
+
@app.command("plan", help=PLAN_HELP)
|
|
362
|
+
@handle_errors
|
|
363
|
+
def plan_command(
|
|
364
|
+
ctx: typer.Context,
|
|
365
|
+
run_id: str = typer.Argument(...),
|
|
366
|
+
plan_version: int = typer.Option(..., "--plan-version", min=1, help="The plan version you reviewed."),
|
|
367
|
+
contact_integration_id: str | None = typer.Option(
|
|
368
|
+
None, "--contact-integration-id", help="An option id from the plan's contact_engine."
|
|
369
|
+
),
|
|
370
|
+
validation_integration_id: str | None = typer.Option(
|
|
371
|
+
None, "--validation-integration-id", help="An option id from the plan's company_check_engine."
|
|
372
|
+
),
|
|
373
|
+
search_provider_id: str | None = typer.Option(None, "--search-provider-id", help=SEARCH_PROVIDER_HELP),
|
|
374
|
+
target_companies: int | None = typer.Option(
|
|
375
|
+
None, "--target-companies", min=1, max=10000, help="Qualified companies to aim for."
|
|
376
|
+
),
|
|
377
|
+
contacts_per_company: int | None = typer.Option(
|
|
378
|
+
None, "--contacts-per-company", min=1, max=10, help="Most contacts to find at each company."
|
|
379
|
+
),
|
|
380
|
+
deliverable: str | None = typer.Option(None, "--deliverable", help=PLAN_DELIVERABLE_HELP),
|
|
381
|
+
goal: str | None = typer.Option(None, "--goal", help=PLAN_GOAL_HELP),
|
|
382
|
+
max_spend_usd: float | None = typer.Option(
|
|
383
|
+
None,
|
|
384
|
+
"--max-spend-usd",
|
|
385
|
+
min=0,
|
|
386
|
+
help=f"USD limit on DiscoLike records and per-call fees at your plan's rates; {SPEND_HELP_SUFFIX}",
|
|
387
|
+
),
|
|
388
|
+
max_provider_spend_usd: float | None = typer.Option(
|
|
389
|
+
None,
|
|
390
|
+
"--max-provider-spend-usd",
|
|
391
|
+
min=0,
|
|
392
|
+
help=f"USD limit on your own AI and search provider keys; {SPEND_HELP_SUFFIX}",
|
|
393
|
+
),
|
|
394
|
+
) -> None:
|
|
395
|
+
from discolike_cli.main import get_client
|
|
396
|
+
|
|
397
|
+
emit(
|
|
398
|
+
get_client(ctx).prospecting.update_plan(
|
|
399
|
+
run_id,
|
|
400
|
+
build_request(
|
|
401
|
+
ProspectingPlanSettings,
|
|
402
|
+
_merge_params(
|
|
403
|
+
None,
|
|
404
|
+
plan_version=plan_version,
|
|
405
|
+
contact_integration_id=contact_integration_id,
|
|
406
|
+
validation_integration_id=validation_integration_id,
|
|
407
|
+
search_provider_id=search_provider_id,
|
|
408
|
+
target_companies=target_companies,
|
|
409
|
+
contacts_per_company=contacts_per_company,
|
|
410
|
+
deliverable=deliverable,
|
|
411
|
+
goal=goal,
|
|
412
|
+
max_spend_usd=max_spend_usd,
|
|
413
|
+
max_provider_spend_usd=max_provider_spend_usd,
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
345
420
|
@app.command("message")
|
|
346
421
|
@handle_errors
|
|
347
422
|
def message_command(
|
|
@@ -6,6 +6,7 @@ from collections.abc import Callable
|
|
|
6
6
|
import httpx2
|
|
7
7
|
import pytest
|
|
8
8
|
from typer.testing import CliRunner
|
|
9
|
+
from typer.testing import Result
|
|
9
10
|
|
|
10
11
|
from discolike_cli.main import app
|
|
11
12
|
from discolike_testkit import Handler
|
|
@@ -394,7 +395,8 @@ def _assert_new_contact_filters(params: httpx2.QueryParams) -> None:
|
|
|
394
395
|
assert params.get_list("persona_id") == ["7"]
|
|
395
396
|
assert params.get_list("filter_state") == ["TX"]
|
|
396
397
|
assert params.get_list("negate_filter_state") == ["NY"]
|
|
397
|
-
assert
|
|
398
|
+
assert "email_validated" not in params
|
|
399
|
+
assert params.get("has_email") == "true"
|
|
398
400
|
assert params.get("has_phone") == "true"
|
|
399
401
|
assert params.get("has_mobile") == "false"
|
|
400
402
|
assert params.get("has_linkedin") == "true"
|
|
@@ -455,7 +457,7 @@ def test_contacts_discover_forwards_every_new_flag(install_build_client: Callabl
|
|
|
455
457
|
"persona_id": [7],
|
|
456
458
|
"filter_state": ["TX"],
|
|
457
459
|
"negate_filter_state": ["NY"],
|
|
458
|
-
"
|
|
460
|
+
"has_email": True,
|
|
459
461
|
"has_phone": True,
|
|
460
462
|
"has_mobile": False,
|
|
461
463
|
"has_linkedin": True,
|
|
@@ -469,6 +471,70 @@ def test_contacts_discover_forwards_every_new_flag(install_build_client: Callabl
|
|
|
469
471
|
}
|
|
470
472
|
|
|
471
473
|
|
|
474
|
+
EMAIL_VALIDATED_RESPONSES = {
|
|
475
|
+
"search": [],
|
|
476
|
+
"count": {"count": 1},
|
|
477
|
+
"discover": {"results": {}, "total_contacts": 0},
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _invoke_contacts(
|
|
482
|
+
install_build_client: Callable[[Handler], None], command: str, *flags: str
|
|
483
|
+
) -> tuple[Result, dict[str, object]]:
|
|
484
|
+
captured: list[httpx2.Request] = []
|
|
485
|
+
|
|
486
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
487
|
+
captured.append(request)
|
|
488
|
+
return httpx2.Response(200, json=EMAIL_VALIDATED_RESPONSES[command])
|
|
489
|
+
|
|
490
|
+
install_build_client(handler)
|
|
491
|
+
result = runner.invoke(app, ["contacts", command, "--name", "Jane", *flags])
|
|
492
|
+
assert result.exit_code == 0, result.output
|
|
493
|
+
(request,) = captured
|
|
494
|
+
if request.method == "GET":
|
|
495
|
+
return result, {
|
|
496
|
+
key: json.loads(value) if value in ("true", "false") else value for key, value in request.url.params.items()
|
|
497
|
+
}
|
|
498
|
+
return result, json.loads(request.content)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@pytest.mark.parametrize("command", sorted(EMAIL_VALIDATED_RESPONSES))
|
|
502
|
+
def test_contacts_email_validated_warns_and_sends_has_email(
|
|
503
|
+
install_build_client: Callable[[Handler], None], command: str
|
|
504
|
+
) -> None:
|
|
505
|
+
result, sent = _invoke_contacts(install_build_client, command, "--email-validated")
|
|
506
|
+
assert "--email-validated is deprecated. Treated as has_email" in result.stderr
|
|
507
|
+
assert sent["has_email"] is True
|
|
508
|
+
assert "email_validated" not in sent
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@pytest.mark.parametrize("command", sorted(EMAIL_VALIDATED_RESPONSES))
|
|
512
|
+
def test_contacts_no_email_validated_warns_and_sends_nothing(
|
|
513
|
+
install_build_client: Callable[[Handler], None], command: str
|
|
514
|
+
) -> None:
|
|
515
|
+
result, sent = _invoke_contacts(install_build_client, command, "--no-email-validated")
|
|
516
|
+
assert "--no-email-validated is deprecated and has no effect" in result.stderr
|
|
517
|
+
assert "has_email" not in sent
|
|
518
|
+
assert "email_validated" not in sent
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@pytest.mark.parametrize(("has_email_flag", "expected"), [("--has-email", True), ("--no-has-email", False)])
|
|
522
|
+
def test_contacts_explicit_has_email_wins_over_email_validated(
|
|
523
|
+
install_build_client: Callable[[Handler], None], has_email_flag: str, expected: bool
|
|
524
|
+
) -> None:
|
|
525
|
+
result, sent = _invoke_contacts(install_build_client, "search", has_email_flag, "--email-validated")
|
|
526
|
+
assert "--email-validated is deprecated" in result.stderr
|
|
527
|
+
assert sent["has_email"] is expected
|
|
528
|
+
assert "email_validated" not in sent
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_contacts_without_email_validated_does_not_warn(install_build_client: Callable[[Handler], None]) -> None:
|
|
532
|
+
install_build_client(lambda request: httpx2.Response(200, json=[]))
|
|
533
|
+
result = runner.invoke(app, ["contacts", "search", "--name", "Jane"])
|
|
534
|
+
assert result.exit_code == 0, result.output
|
|
535
|
+
assert "deprecated" not in result.stderr
|
|
536
|
+
|
|
537
|
+
|
|
472
538
|
def test_contacts_generate_forwards_full_and_partial_domains(install_build_client: Callable[[Handler], None]) -> None:
|
|
473
539
|
captured: dict[str, object] = {}
|
|
474
540
|
|
|
@@ -189,6 +189,10 @@ def test_message_forwards_question_seq(install_build_client: Callable[[Handler],
|
|
|
189
189
|
"arguments",
|
|
190
190
|
[
|
|
191
191
|
["approve", RUN_ID],
|
|
192
|
+
["plan", RUN_ID],
|
|
193
|
+
["plan", RUN_ID, "--plan-version", "1", "--contacts-per-company", "11"],
|
|
194
|
+
["plan", RUN_ID, "--plan-version", "1", "--deliverable", "people"],
|
|
195
|
+
["plan", RUN_ID, "--plan-version", "1", "--max-spend-usd", "-1"],
|
|
192
196
|
["message", RUN_ID, "--text", "Continue"],
|
|
193
197
|
["message", RUN_ID, "--text", "Continue", "--idempotency-key", "k", "--question-seq", "0"],
|
|
194
198
|
["list", "--limit", "51"],
|
|
@@ -455,3 +459,91 @@ def test_approve_sends_the_chosen_segments_and_results_grouping(
|
|
|
455
459
|
{"plan_version": 1, "checkpoints": "ask", "seed_segments": [1, 2], "segment": True},
|
|
456
460
|
{"plan_version": 1, "checkpoints": "ask"},
|
|
457
461
|
]
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def test_plan_sends_only_the_settings_passed(install_build_client: Callable[[Handler], None]) -> None:
|
|
465
|
+
seen = []
|
|
466
|
+
|
|
467
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
468
|
+
seen.append(request)
|
|
469
|
+
return httpx2.Response(200, json=run_payload("proposed") | {"plan_version": 4})
|
|
470
|
+
|
|
471
|
+
install_build_client(handler)
|
|
472
|
+
result = runner.invoke(
|
|
473
|
+
app,
|
|
474
|
+
[
|
|
475
|
+
"prospecting",
|
|
476
|
+
"plan",
|
|
477
|
+
RUN_ID,
|
|
478
|
+
"--plan-version",
|
|
479
|
+
"3",
|
|
480
|
+
"--contact-integration-id",
|
|
481
|
+
"llm-1",
|
|
482
|
+
"--search-provider-id",
|
|
483
|
+
"none",
|
|
484
|
+
],
|
|
485
|
+
)
|
|
486
|
+
assert result.exit_code == 0, result.output
|
|
487
|
+
assert seen[0].method == "PATCH"
|
|
488
|
+
assert seen[0].url.path.endswith(f"/prospecting/runs/{RUN_ID}/plan")
|
|
489
|
+
assert json.loads(seen[0].content) == {
|
|
490
|
+
"plan_version": 3,
|
|
491
|
+
"contact_integration_id": "llm-1",
|
|
492
|
+
"search_provider_id": "none",
|
|
493
|
+
}
|
|
494
|
+
printed = json.loads(result.stdout)
|
|
495
|
+
assert (printed["run_id"], printed["plan_version"]) == (RUN_ID, 4)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_plan_forwards_every_setting(install_build_client: Callable[[Handler], None]) -> None:
|
|
499
|
+
bodies = []
|
|
500
|
+
|
|
501
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
502
|
+
bodies.append(json.loads(request.content))
|
|
503
|
+
return httpx2.Response(200, json=run_payload("proposed") | {"plan_version": 3})
|
|
504
|
+
|
|
505
|
+
install_build_client(handler)
|
|
506
|
+
result = runner.invoke(
|
|
507
|
+
app,
|
|
508
|
+
[
|
|
509
|
+
"prospecting",
|
|
510
|
+
"plan",
|
|
511
|
+
RUN_ID,
|
|
512
|
+
"--plan-version",
|
|
513
|
+
"2",
|
|
514
|
+
"--contact-integration-id",
|
|
515
|
+
"llm-1",
|
|
516
|
+
"--validation-integration-id",
|
|
517
|
+
"llm-2",
|
|
518
|
+
"--search-provider-id",
|
|
519
|
+
"search-1",
|
|
520
|
+
"--target-companies",
|
|
521
|
+
"250",
|
|
522
|
+
"--contacts-per-company",
|
|
523
|
+
"5",
|
|
524
|
+
"--deliverable",
|
|
525
|
+
"accounts",
|
|
526
|
+
"--goal",
|
|
527
|
+
"companies",
|
|
528
|
+
"--max-spend-usd",
|
|
529
|
+
"0",
|
|
530
|
+
"--max-provider-spend-usd",
|
|
531
|
+
"12.5",
|
|
532
|
+
],
|
|
533
|
+
)
|
|
534
|
+
assert result.exit_code == 0, result.output
|
|
535
|
+
assert bodies == [
|
|
536
|
+
{
|
|
537
|
+
"plan_version": 2,
|
|
538
|
+
"contact_integration_id": "llm-1",
|
|
539
|
+
"validation_integration_id": "llm-2",
|
|
540
|
+
"search_provider_id": "search-1",
|
|
541
|
+
"target_companies": 250,
|
|
542
|
+
"contacts_per_company": 5,
|
|
543
|
+
"deliverable": "accounts",
|
|
544
|
+
"goal": "companies",
|
|
545
|
+
"max_spend_usd": 0.0,
|
|
546
|
+
"max_provider_spend_usd": 12.5,
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
assert json.loads(result.stdout)["plan_version"] == 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|