discolike-cli 0.3.1__tar.gz → 0.3.2__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.3.1 → discolike_cli-0.3.2}/PKG-INFO +2 -2
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/pyproject.toml +2 -2
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/company.py +10 -5
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/contacts.py +149 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/discover.py +89 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/enrich.py +11 -3
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/match.py +25 -1
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_company.py +24 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_contacts_cli.py +162 -9
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_discover.py +114 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_enrich_cli.py +37 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_match_cli.py +55 -0
- discolike_cli-0.3.2/tests/test_sdk_parity.py +99 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/.gitignore +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/LICENSE +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/README.md +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/__init__.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/_loopback.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/_output.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/account.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/auth.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/discogen.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/email.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/main.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/providers.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/py.typed +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/queries.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/src/discolike_cli/signup.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/conftest.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_account.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_auth.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_auth_oauth.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_discogen_cli.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_email_cli.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_output.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_providers_cli.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/tests/test_queries_cli.py +0 -0
- {discolike_cli-0.3.1 → discolike_cli-0.3.2}/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.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -25,7 +25,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
25
25
|
Classifier: Topic :: Utilities
|
|
26
26
|
Classifier: Typing :: Typed
|
|
27
27
|
Requires-Python: >=3.10
|
|
28
|
-
Requires-Dist: discolike==0.3.
|
|
28
|
+
Requires-Dist: discolike==0.3.2
|
|
29
29
|
Requires-Dist: rich>=13.0
|
|
30
30
|
Requires-Dist: typer>=0.12
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "discolike-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
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.10"
|
|
13
13
|
authors = [{ name = "DiscoLike", email = "support@discolike.com" }]
|
|
14
14
|
dependencies = [
|
|
15
|
-
"discolike==0.3.
|
|
15
|
+
"discolike==0.3.2",
|
|
16
16
|
"typer>=0.12",
|
|
17
17
|
"rich>=13.0",
|
|
18
18
|
]
|
|
@@ -134,12 +134,17 @@ def public_links(
|
|
|
134
134
|
@handle_errors
|
|
135
135
|
def extract_command(
|
|
136
136
|
ctx: typer.Context,
|
|
137
|
-
url: str = typer.Argument(
|
|
137
|
+
url: str | None = typer.Argument(None, help="Page URL to extract content from"),
|
|
138
|
+
domain: str | None = typer.Option(
|
|
139
|
+
None, "--domain", help="Bare domain to extract instead of URL (alias for https://DOMAIN, served from cache)."
|
|
140
|
+
),
|
|
138
141
|
fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
139
142
|
) -> None:
|
|
140
|
-
"""Extract page content from a URL."""
|
|
143
|
+
"""Extract page content from a URL or a bare domain."""
|
|
141
144
|
from discolike_cli.main import get_client
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
if (url is None) == (domain is None):
|
|
147
|
+
raise typer.BadParameter("Provide exactly one of URL or --domain")
|
|
148
|
+
|
|
149
|
+
request = build_request(CompaniesExtractParams, _merge_params(None, url=url, domain=domain))
|
|
150
|
+
emit(get_client(ctx).companies.extract(request), fmt=fmt)
|
|
@@ -25,6 +25,25 @@ WAIT_HELP = "Block until the job finishes, streaming progress to stderr."
|
|
|
25
25
|
TIMEOUT_HELP = "Max seconds to wait with --wait."
|
|
26
26
|
PARAM_HELP = "Extra key=value query parameter forwarded to the SDK (repeatable)."
|
|
27
27
|
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."
|
|
28
|
+
SUMMARY_HELP = "Filter by profile summary text (semantic search)."
|
|
29
|
+
NEGATE_SUMMARY_HELP = "Exclude contacts matching this summary description."
|
|
30
|
+
NAME_HELP = "Filter by contact name (partial match supported)."
|
|
31
|
+
SKILLS_HELP = "Filter by skill (repeatable)."
|
|
32
|
+
FILTER_STATE_HELP = "Filter by company state/region (repeatable)."
|
|
33
|
+
NEGATE_FILTER_STATE_HELP = "Exclude contacts at companies in this state (repeatable)."
|
|
34
|
+
PERSON_STATE_HELP = "Filter by contact state/region (repeatable)."
|
|
35
|
+
PERSONA_ID_HELP = "Filter by persona ID (repeatable)."
|
|
36
|
+
EMAIL_VALIDATED_HELP = "Only contacts with (or without) a validated email address."
|
|
37
|
+
HAS_PHONE_HELP = "Only contacts with (or without) a phone number."
|
|
38
|
+
HAS_MOBILE_HELP = "Only contacts with (or without) a mobile phone number."
|
|
39
|
+
HAS_LINKEDIN_HELP = "Only contacts with (or without) a LinkedIn profile."
|
|
40
|
+
MIN_CONNECTIONS_HELP = "Minimum LinkedIn connections required."
|
|
41
|
+
INCLUSION_QUERY_ID_HELP = "Include only contacts from companies in this saved query (repeatable)."
|
|
42
|
+
EXCLUSION_QUERY_ID_HELP = "Exclude contacts from companies in this saved query (repeatable)."
|
|
43
|
+
MAX_COMPANIES_HELP = "Maximum number of enriched companies to return; cannot be combined with --max-records."
|
|
44
|
+
RESULTS_BY_COMPANY_HELP = "Maximum contacts per company domain (default 5; 0 removes the cap)."
|
|
45
|
+
INCLUDE_SEARCH_CONTACTS_HELP = "Include contacts from the search index (broader coverage)."
|
|
46
|
+
CONSENSUS_HELP = "Number of query vectors to combine for consensus search."
|
|
28
47
|
|
|
29
48
|
app = typer.Typer(
|
|
30
49
|
help="Find contacts: search and count by filters, look up or match individuals, and run bulk/generative discovery jobs."
|
|
@@ -42,20 +61,43 @@ def search_command(
|
|
|
42
61
|
negate_department: list[str] | None = typer.Option(None, help="Exclude departments (repeatable)."),
|
|
43
62
|
title: list[str] | None = typer.Option(None, help="Filter by job title (repeatable)."),
|
|
44
63
|
negate_title: list[str] | None = typer.Option(None, help="Exclude job titles (repeatable)."),
|
|
64
|
+
name: str | None = typer.Option(None, help=NAME_HELP),
|
|
65
|
+
summary: str | None = typer.Option(None, help=SUMMARY_HELP),
|
|
66
|
+
negate_summary: str | None = typer.Option(None, help=NEGATE_SUMMARY_HELP),
|
|
67
|
+
skills: list[str] | None = typer.Option(None, help=SKILLS_HELP),
|
|
45
68
|
domain: list[str] | None = typer.Option(None, help="Filter by company domain (repeatable)."),
|
|
46
69
|
person_country: list[str] | None = typer.Option(None, help="Filter by contact country (repeatable)."),
|
|
47
70
|
negate_person_country: list[str] | None = typer.Option(None, help="Exclude contact countries (repeatable)."),
|
|
71
|
+
person_state: list[str] | None = typer.Option(None, help=PERSON_STATE_HELP),
|
|
72
|
+
persona_id: list[int] | None = typer.Option(None, help=PERSONA_ID_HELP),
|
|
48
73
|
filter_industry: list[str] | None = typer.Option(None, help="Filter by company industry (repeatable)."),
|
|
49
74
|
negate_filter_industry: list[str] | None = typer.Option(None, help="Exclude company industries (repeatable)."),
|
|
50
75
|
filter_country: list[str] | None = typer.Option(None, help="Filter by company country (repeatable)."),
|
|
51
76
|
negate_filter_country: list[str] | None = typer.Option(None, help="Exclude company countries (repeatable)."),
|
|
77
|
+
filter_state: list[str] | None = typer.Option(None, help=FILTER_STATE_HELP),
|
|
78
|
+
negate_filter_state: list[str] | None = typer.Option(None, help=NEGATE_FILTER_STATE_HELP),
|
|
52
79
|
employee_range: str | None = typer.Option(None, help="Company employee range, e.g. 50-200."),
|
|
53
80
|
has_email: bool | None = typer.Option(
|
|
54
81
|
None, "--has-email/--no-has-email", help="Only contacts with (or without) an email address."
|
|
55
82
|
),
|
|
83
|
+
email_validated: bool | None = typer.Option(
|
|
84
|
+
None, "--email-validated/--no-email-validated", help=EMAIL_VALIDATED_HELP
|
|
85
|
+
),
|
|
86
|
+
has_phone: bool | None = typer.Option(None, "--has-phone/--no-has-phone", help=HAS_PHONE_HELP),
|
|
87
|
+
has_mobile: bool | None = typer.Option(None, "--has-mobile/--no-has-mobile", help=HAS_MOBILE_HELP),
|
|
88
|
+
has_linkedin: bool | None = typer.Option(None, "--has-linkedin/--no-has-linkedin", help=HAS_LINKEDIN_HELP),
|
|
89
|
+
min_connections: int | None = typer.Option(None, help=MIN_CONNECTIONS_HELP),
|
|
56
90
|
jobstart_date: str | None = typer.Option(None, "--jobstart-date", help=JOBSTART_DATE_HELP),
|
|
91
|
+
inclusion_query_id: list[str] | None = typer.Option(None, help=INCLUSION_QUERY_ID_HELP),
|
|
92
|
+
exclusion_query_id: list[str] | None = typer.Option(None, help=EXCLUSION_QUERY_ID_HELP),
|
|
57
93
|
max_records: int | None = typer.Option(None, help="Maximum number of contacts to return."),
|
|
94
|
+
max_companies: int | None = typer.Option(None, help=MAX_COMPANIES_HELP),
|
|
58
95
|
offset: int | None = typer.Option(None, help="Number of records to skip for pagination."),
|
|
96
|
+
results_by_company: int | None = typer.Option(None, help=RESULTS_BY_COMPANY_HELP),
|
|
97
|
+
include_search_contacts: bool | None = typer.Option(
|
|
98
|
+
None, "--include-search-contacts/--no-include-search-contacts", help=INCLUDE_SEARCH_CONTACTS_HELP
|
|
99
|
+
),
|
|
100
|
+
consensus: int | None = typer.Option(None, help=CONSENSUS_HELP),
|
|
59
101
|
fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
60
102
|
param: list[str] | None = typer.Option(None, "--param", help=PARAM_HELP),
|
|
61
103
|
) -> None:
|
|
@@ -71,18 +113,37 @@ def search_command(
|
|
|
71
113
|
negate_department=negate_department,
|
|
72
114
|
title=title,
|
|
73
115
|
negate_title=negate_title,
|
|
116
|
+
name=name,
|
|
117
|
+
summary=summary,
|
|
118
|
+
negate_summary=negate_summary,
|
|
119
|
+
skills=skills,
|
|
74
120
|
domain=domain,
|
|
75
121
|
person_country=person_country,
|
|
76
122
|
negate_person_country=negate_person_country,
|
|
123
|
+
person_state=person_state,
|
|
124
|
+
persona_id=persona_id,
|
|
77
125
|
filter_industry=filter_industry,
|
|
78
126
|
negate_filter_industry=negate_filter_industry,
|
|
79
127
|
filter_country=filter_country,
|
|
80
128
|
negate_filter_country=negate_filter_country,
|
|
129
|
+
filter_state=filter_state,
|
|
130
|
+
negate_filter_state=negate_filter_state,
|
|
81
131
|
employee_range=employee_range,
|
|
82
132
|
has_email=has_email,
|
|
133
|
+
email_validated=email_validated,
|
|
134
|
+
has_phone=has_phone,
|
|
135
|
+
has_mobile=has_mobile,
|
|
136
|
+
has_linkedin=has_linkedin,
|
|
137
|
+
min_connections=min_connections,
|
|
83
138
|
jobstart_date=jobstart_date,
|
|
139
|
+
inclusion_query_id=inclusion_query_id,
|
|
140
|
+
exclusion_query_id=exclusion_query_id,
|
|
84
141
|
max_records=max_records,
|
|
142
|
+
max_companies=max_companies,
|
|
85
143
|
offset=offset,
|
|
144
|
+
results_by_company=results_by_company,
|
|
145
|
+
include_search_contacts=include_search_contacts,
|
|
146
|
+
consensus=consensus,
|
|
86
147
|
)
|
|
87
148
|
emit(get_client(ctx).contacts.search(build_request(ContactsSearchParams, kwargs)), fmt=fmt)
|
|
88
149
|
|
|
@@ -98,18 +159,43 @@ def count_command(
|
|
|
98
159
|
negate_department: list[str] | None = typer.Option(None, help="Exclude departments (repeatable)."),
|
|
99
160
|
title: list[str] | None = typer.Option(None, help="Filter by job title (repeatable)."),
|
|
100
161
|
negate_title: list[str] | None = typer.Option(None, help="Exclude job titles (repeatable)."),
|
|
162
|
+
name: str | None = typer.Option(None, help=NAME_HELP),
|
|
163
|
+
summary: str | None = typer.Option(None, help=SUMMARY_HELP),
|
|
164
|
+
negate_summary: str | None = typer.Option(None, help=NEGATE_SUMMARY_HELP),
|
|
165
|
+
skills: list[str] | None = typer.Option(None, help=SKILLS_HELP),
|
|
101
166
|
domain: list[str] | None = typer.Option(None, help="Filter by company domain (repeatable)."),
|
|
102
167
|
person_country: list[str] | None = typer.Option(None, help="Filter by contact country (repeatable)."),
|
|
103
168
|
negate_person_country: list[str] | None = typer.Option(None, help="Exclude contact countries (repeatable)."),
|
|
169
|
+
person_state: list[str] | None = typer.Option(None, help=PERSON_STATE_HELP),
|
|
170
|
+
persona_id: list[int] | None = typer.Option(None, help=PERSONA_ID_HELP),
|
|
104
171
|
filter_industry: list[str] | None = typer.Option(None, help="Filter by company industry (repeatable)."),
|
|
105
172
|
negate_filter_industry: list[str] | None = typer.Option(None, help="Exclude company industries (repeatable)."),
|
|
106
173
|
filter_country: list[str] | None = typer.Option(None, help="Filter by company country (repeatable)."),
|
|
107
174
|
negate_filter_country: list[str] | None = typer.Option(None, help="Exclude company countries (repeatable)."),
|
|
175
|
+
filter_state: list[str] | None = typer.Option(None, help=FILTER_STATE_HELP),
|
|
176
|
+
negate_filter_state: list[str] | None = typer.Option(None, help=NEGATE_FILTER_STATE_HELP),
|
|
108
177
|
employee_range: str | None = typer.Option(None, help="Company employee range, e.g. 50-200."),
|
|
109
178
|
has_email: bool | None = typer.Option(
|
|
110
179
|
None, "--has-email/--no-has-email", help="Only contacts with (or without) an email address."
|
|
111
180
|
),
|
|
181
|
+
email_validated: bool | None = typer.Option(
|
|
182
|
+
None, "--email-validated/--no-email-validated", help=EMAIL_VALIDATED_HELP
|
|
183
|
+
),
|
|
184
|
+
has_phone: bool | None = typer.Option(None, "--has-phone/--no-has-phone", help=HAS_PHONE_HELP),
|
|
185
|
+
has_mobile: bool | None = typer.Option(None, "--has-mobile/--no-has-mobile", help=HAS_MOBILE_HELP),
|
|
186
|
+
has_linkedin: bool | None = typer.Option(None, "--has-linkedin/--no-has-linkedin", help=HAS_LINKEDIN_HELP),
|
|
187
|
+
min_connections: int | None = typer.Option(None, help=MIN_CONNECTIONS_HELP),
|
|
112
188
|
jobstart_date: str | None = typer.Option(None, "--jobstart-date", help=JOBSTART_DATE_HELP),
|
|
189
|
+
inclusion_query_id: list[str] | None = typer.Option(None, help=INCLUSION_QUERY_ID_HELP),
|
|
190
|
+
exclusion_query_id: list[str] | None = typer.Option(None, help=EXCLUSION_QUERY_ID_HELP),
|
|
191
|
+
max_records: int | None = typer.Option(None, help="Maximum number of contacts to count (20-10000)."),
|
|
192
|
+
max_companies: int | None = typer.Option(None, help=MAX_COMPANIES_HELP),
|
|
193
|
+
offset: int | None = typer.Option(None, help="Number of records to skip for pagination."),
|
|
194
|
+
results_by_company: int | None = typer.Option(None, help=RESULTS_BY_COMPANY_HELP),
|
|
195
|
+
include_search_contacts: bool | None = typer.Option(
|
|
196
|
+
None, "--include-search-contacts/--no-include-search-contacts", help=INCLUDE_SEARCH_CONTACTS_HELP
|
|
197
|
+
),
|
|
198
|
+
consensus: int | None = typer.Option(None, help=CONSENSUS_HELP),
|
|
113
199
|
fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
114
200
|
param: list[str] | None = typer.Option(None, "--param", help=PARAM_HELP),
|
|
115
201
|
) -> None:
|
|
@@ -125,16 +211,37 @@ def count_command(
|
|
|
125
211
|
negate_department=negate_department,
|
|
126
212
|
title=title,
|
|
127
213
|
negate_title=negate_title,
|
|
214
|
+
name=name,
|
|
215
|
+
summary=summary,
|
|
216
|
+
negate_summary=negate_summary,
|
|
217
|
+
skills=skills,
|
|
128
218
|
domain=domain,
|
|
129
219
|
person_country=person_country,
|
|
130
220
|
negate_person_country=negate_person_country,
|
|
221
|
+
person_state=person_state,
|
|
222
|
+
persona_id=persona_id,
|
|
131
223
|
filter_industry=filter_industry,
|
|
132
224
|
negate_filter_industry=negate_filter_industry,
|
|
133
225
|
filter_country=filter_country,
|
|
134
226
|
negate_filter_country=negate_filter_country,
|
|
227
|
+
filter_state=filter_state,
|
|
228
|
+
negate_filter_state=negate_filter_state,
|
|
135
229
|
employee_range=employee_range,
|
|
136
230
|
has_email=has_email,
|
|
231
|
+
email_validated=email_validated,
|
|
232
|
+
has_phone=has_phone,
|
|
233
|
+
has_mobile=has_mobile,
|
|
234
|
+
has_linkedin=has_linkedin,
|
|
235
|
+
min_connections=min_connections,
|
|
137
236
|
jobstart_date=jobstart_date,
|
|
237
|
+
inclusion_query_id=inclusion_query_id,
|
|
238
|
+
exclusion_query_id=exclusion_query_id,
|
|
239
|
+
max_records=max_records,
|
|
240
|
+
max_companies=max_companies,
|
|
241
|
+
offset=offset,
|
|
242
|
+
results_by_company=results_by_company,
|
|
243
|
+
include_search_contacts=include_search_contacts,
|
|
244
|
+
consensus=consensus,
|
|
138
245
|
)
|
|
139
246
|
emit(get_client(ctx).contacts.count(build_request(ContactsCountParams, kwargs)), fmt=fmt)
|
|
140
247
|
|
|
@@ -220,19 +327,37 @@ def discover_command(
|
|
|
220
327
|
negate_department: list[str] | None = typer.Option(None, help="Exclude departments (repeatable)."),
|
|
221
328
|
title: list[str] | None = typer.Option(None, help="Filter by job title (repeatable)."),
|
|
222
329
|
negate_title: list[str] | None = typer.Option(None, help="Exclude job titles (repeatable)."),
|
|
330
|
+
name: str | None = typer.Option(None, help=NAME_HELP),
|
|
331
|
+
summary: str | None = typer.Option(None, help=SUMMARY_HELP),
|
|
332
|
+
negate_summary: str | None = typer.Option(None, help=NEGATE_SUMMARY_HELP),
|
|
333
|
+
skills: list[str] | None = typer.Option(None, help=SKILLS_HELP),
|
|
223
334
|
domain: list[str] | None = typer.Option(None, help="Filter by company domain (repeatable)."),
|
|
224
335
|
person_country: list[str] | None = typer.Option(None, help="Filter by contact country (repeatable)."),
|
|
225
336
|
negate_person_country: list[str] | None = typer.Option(None, help="Exclude contact countries (repeatable)."),
|
|
337
|
+
person_state: list[str] | None = typer.Option(None, help=PERSON_STATE_HELP),
|
|
338
|
+
persona_id: list[int] | None = typer.Option(None, help=PERSONA_ID_HELP),
|
|
226
339
|
filter_industry: list[str] | None = typer.Option(None, help="Filter by company industry (repeatable)."),
|
|
227
340
|
negate_filter_industry: list[str] | None = typer.Option(None, help="Exclude company industries (repeatable)."),
|
|
228
341
|
filter_country: list[str] | None = typer.Option(None, help="Filter by company country (repeatable)."),
|
|
229
342
|
negate_filter_country: list[str] | None = typer.Option(None, help="Exclude company countries (repeatable)."),
|
|
343
|
+
filter_state: list[str] | None = typer.Option(None, help=FILTER_STATE_HELP),
|
|
344
|
+
negate_filter_state: list[str] | None = typer.Option(None, help=NEGATE_FILTER_STATE_HELP),
|
|
230
345
|
employee_range: str | None = typer.Option(None, help="Company employee range, e.g. 50-200."),
|
|
231
346
|
has_email: bool | None = typer.Option(
|
|
232
347
|
None, "--has-email/--no-has-email", help="Only contacts with (or without) an email address."
|
|
233
348
|
),
|
|
349
|
+
email_validated: bool | None = typer.Option(
|
|
350
|
+
None, "--email-validated/--no-email-validated", help=EMAIL_VALIDATED_HELP
|
|
351
|
+
),
|
|
352
|
+
has_phone: bool | None = typer.Option(None, "--has-phone/--no-has-phone", help=HAS_PHONE_HELP),
|
|
353
|
+
has_mobile: bool | None = typer.Option(None, "--has-mobile/--no-has-mobile", help=HAS_MOBILE_HELP),
|
|
354
|
+
has_linkedin: bool | None = typer.Option(None, "--has-linkedin/--no-has-linkedin", help=HAS_LINKEDIN_HELP),
|
|
355
|
+
min_connections: int | None = typer.Option(None, help=MIN_CONNECTIONS_HELP),
|
|
234
356
|
jobstart_date: str | None = typer.Option(None, "--jobstart-date", help=JOBSTART_DATE_HELP),
|
|
357
|
+
inclusion_query_id: list[str] | None = typer.Option(None, help=INCLUSION_QUERY_ID_HELP),
|
|
358
|
+
exclusion_query_id: list[str] | None = typer.Option(None, help=EXCLUSION_QUERY_ID_HELP),
|
|
235
359
|
max_records: int | None = typer.Option(None, help="Maximum number of contacts to return."),
|
|
360
|
+
max_companies: int | None = typer.Option(None, help=MAX_COMPANIES_HELP),
|
|
236
361
|
offset: int | None = typer.Option(None, help="Number of records to skip for pagination."),
|
|
237
362
|
results_by_company: int | None = typer.Option(
|
|
238
363
|
None, "--results-by-company", help="Maximum contacts returned per company."
|
|
@@ -258,17 +383,33 @@ def discover_command(
|
|
|
258
383
|
negate_department=negate_department,
|
|
259
384
|
title=title,
|
|
260
385
|
negate_title=negate_title,
|
|
386
|
+
name=name,
|
|
387
|
+
summary=summary,
|
|
388
|
+
negate_summary=negate_summary,
|
|
389
|
+
skills=skills,
|
|
261
390
|
domain=domain,
|
|
262
391
|
person_country=person_country,
|
|
263
392
|
negate_person_country=negate_person_country,
|
|
393
|
+
person_state=person_state,
|
|
394
|
+
persona_id=persona_id,
|
|
264
395
|
filter_industry=filter_industry,
|
|
265
396
|
negate_filter_industry=negate_filter_industry,
|
|
266
397
|
filter_country=filter_country,
|
|
267
398
|
negate_filter_country=negate_filter_country,
|
|
399
|
+
filter_state=filter_state,
|
|
400
|
+
negate_filter_state=negate_filter_state,
|
|
268
401
|
employee_range=employee_range,
|
|
269
402
|
has_email=has_email,
|
|
403
|
+
email_validated=email_validated,
|
|
404
|
+
has_phone=has_phone,
|
|
405
|
+
has_mobile=has_mobile,
|
|
406
|
+
has_linkedin=has_linkedin,
|
|
407
|
+
min_connections=min_connections,
|
|
270
408
|
jobstart_date=jobstart_date,
|
|
409
|
+
inclusion_query_id=inclusion_query_id,
|
|
410
|
+
exclusion_query_id=exclusion_query_id,
|
|
271
411
|
max_records=max_records,
|
|
412
|
+
max_companies=max_companies,
|
|
272
413
|
offset=offset,
|
|
273
414
|
results_by_company=results_by_company,
|
|
274
415
|
include_search_contacts=include_search_contacts,
|
|
@@ -283,6 +424,12 @@ def generate_command(
|
|
|
283
424
|
ctx: typer.Context,
|
|
284
425
|
icp_text: str = typer.Option(..., "--icp-text", help="ICP description used to generate contacts."),
|
|
285
426
|
domain: list[str] = typer.Option(..., "--domain", help="Target company domain (repeatable)."),
|
|
427
|
+
full_domain: list[str] | None = typer.Option(
|
|
428
|
+
None, "--full-domain", help="Domain to send as full_domains to the generation job (repeatable)."
|
|
429
|
+
),
|
|
430
|
+
partial_domain: list[str] | None = typer.Option(
|
|
431
|
+
None, "--partial-domain", help="Domain to send as partial_domains to the generation job (repeatable)."
|
|
432
|
+
),
|
|
286
433
|
context_mode: str | None = typer.Option(None, "--context-mode", help="Context mode for generation."),
|
|
287
434
|
integration_id: str | None = typer.Option(None, "--integration-id", help="Integration ID to use for generation."),
|
|
288
435
|
search_provider_id: str | None = typer.Option(
|
|
@@ -310,6 +457,8 @@ def generate_command(
|
|
|
310
457
|
None,
|
|
311
458
|
icp_text=icp_text,
|
|
312
459
|
domains=domain,
|
|
460
|
+
full_domains=full_domain,
|
|
461
|
+
partial_domains=partial_domain,
|
|
313
462
|
context_mode=context_mode,
|
|
314
463
|
integration_id=integration_id,
|
|
315
464
|
search_provider_id=search_provider_id,
|
|
@@ -15,6 +15,19 @@ LIST_VALUE_SEPARATOR = ","
|
|
|
15
15
|
|
|
16
16
|
FORMAT_HELP = "Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data)."
|
|
17
17
|
PARAM_HELP = "Extra API parameter as KEY=VALUE (comma-separates into a list); see docs.discolike.com"
|
|
18
|
+
SUBDOMAIN_HELP = "Limit results to this subdomain, up to 20, each at least 3 characters (repeatable)."
|
|
19
|
+
NEGATE_SUBDOMAIN_HELP = "Exclude this subdomain, up to 20, each at least 3 characters (repeatable)."
|
|
20
|
+
START_DATE_HELP = "Minimum company start date (YYYY-MM-DD) or range (YYYY-MM-DD,YYYY-MM-DD)."
|
|
21
|
+
REDIRECT_HELP = "Include or exclude domains that redirect to another domain."
|
|
22
|
+
SOCIAL_HELP = (
|
|
23
|
+
"Filter by social platform presence (repeatable): facebook, instagram, linkedin, pinterest, threads, tiktok, "
|
|
24
|
+
"twitter, x, yelp, youtube, googleplay, applestore, amazon, vk, bluesky, xing."
|
|
25
|
+
)
|
|
26
|
+
NEGATE_SOCIAL_HELP = "Exclude companies with this social profile (repeatable); same values as --social."
|
|
27
|
+
LANGUAGE_HELP = "Filter by site language as an ISO 639-1 code, e.g. en, de, pb, zt (repeatable)."
|
|
28
|
+
NEGATE_LANGUAGE_HELP = "Exclude this site language (repeatable); same values as --language."
|
|
29
|
+
EXCLUDE_LEADGEN_HELP = "Exclude suspected lead generation sites (on by default)."
|
|
30
|
+
COUNT_EXCLUDE_LEADGEN_HELP = "Exclude suspected lead generation sites (off by default for count)."
|
|
18
31
|
|
|
19
32
|
|
|
20
33
|
def _parse_param(raw: str) -> tuple[str, str | list[str]]:
|
|
@@ -45,8 +58,15 @@ def discover_command(
|
|
|
45
58
|
negate_country: list[str] | None = typer.Option(None, help="Negate the --country filter (repeatable)."),
|
|
46
59
|
state: list[str] | None = typer.Option(None, help="State or region filter (repeatable)."),
|
|
47
60
|
negate_state: list[str] | None = typer.Option(None, help="Negate the --state filter (repeatable)."),
|
|
61
|
+
subdomain: list[str] | None = typer.Option(None, help=SUBDOMAIN_HELP),
|
|
62
|
+
negate_subdomain: list[str] | None = typer.Option(None, help=NEGATE_SUBDOMAIN_HELP),
|
|
63
|
+
language: list[str] | None = typer.Option(None, help=LANGUAGE_HELP),
|
|
64
|
+
negate_language: list[str] | None = typer.Option(None, help=NEGATE_LANGUAGE_HELP),
|
|
65
|
+
social: list[str] | None = typer.Option(None, help=SOCIAL_HELP),
|
|
66
|
+
negate_social: list[str] | None = typer.Option(None, help=NEGATE_SOCIAL_HELP),
|
|
48
67
|
employee_range: str | None = typer.Option(None, help="Employee count range filter."),
|
|
49
68
|
revenue_range: str | None = typer.Option(None, help="Revenue range filter."),
|
|
69
|
+
start_date: str | None = typer.Option(None, help=START_DATE_HELP),
|
|
50
70
|
business_model: list[str] | None = typer.Option(None, help="Business model filter (repeatable)."),
|
|
51
71
|
negate_business_model: list[str] | None = typer.Option(
|
|
52
72
|
None, help="Negate the --business-model filter (repeatable)."
|
|
@@ -55,7 +75,38 @@ def discover_command(
|
|
|
55
75
|
negate_tech_stack: list[str] | None = typer.Option(None, help="Negate the --tech-stack filter (repeatable)."),
|
|
56
76
|
min_digital_footprint: int | None = typer.Option(None, help="Minimum digital footprint score."),
|
|
57
77
|
max_digital_footprint: int | None = typer.Option(None, help="Maximum digital footprint score."),
|
|
78
|
+
min_similarity: int | None = typer.Option(None, help="Minimum similarity score to include (0-99)."),
|
|
79
|
+
variance: str | None = typer.Option(
|
|
80
|
+
None, help="Result diversity control: LOW, MID_LOW, MEDIUM, MID_HIGH, HIGH, UNRESTRICTED."
|
|
81
|
+
),
|
|
82
|
+
consensus: int | None = typer.Option(
|
|
83
|
+
None, help="Number of top results for the consensus search vector (1-20); higher reduces specificity."
|
|
84
|
+
),
|
|
85
|
+
redirect: bool | None = typer.Option(None, "--redirect/--no-redirect", help=REDIRECT_HELP),
|
|
86
|
+
exclude_leadgen: bool | None = typer.Option(
|
|
87
|
+
None, "--exclude-leadgen/--no-exclude-leadgen", help=EXCLUDE_LEADGEN_HELP
|
|
88
|
+
),
|
|
89
|
+
retrieval: bool | None = typer.Option(
|
|
90
|
+
None, "--retrieval/--no-retrieval", help="Enable page data retrieval using the Extract API."
|
|
91
|
+
),
|
|
92
|
+
enhanced: bool | None = typer.Option(
|
|
93
|
+
None, "--enhanced/--no-enhanced", help="Enable AI-powered result enhancement for improved relevance."
|
|
94
|
+
),
|
|
95
|
+
include_search_domains: bool | None = typer.Option(
|
|
96
|
+
None,
|
|
97
|
+
"--include-search-domains/--no-include-search-domains",
|
|
98
|
+
help="Include the input domains in results (excluded by default).",
|
|
99
|
+
),
|
|
100
|
+
auto_icp_text: bool | None = typer.Option(
|
|
101
|
+
None, "--auto-icp-text/--no-auto-icp-text", help="Auto-generate ICP text from the provided domain(s)."
|
|
102
|
+
),
|
|
103
|
+
auto_phrase_match: bool | None = typer.Option(
|
|
104
|
+
None, "--auto-phrase-match/--no-auto-phrase-match", help="Auto-generate phrase matches from ICP text."
|
|
105
|
+
),
|
|
58
106
|
exclude_domain: list[str] | None = typer.Option(None, help="Domain to exclude from results (repeatable)."),
|
|
107
|
+
inclusion_query_id: list[str] | None = typer.Option(
|
|
108
|
+
None, help="Saved query ID whose domains are included (repeatable); requires the STARTER plan."
|
|
109
|
+
),
|
|
59
110
|
exclusion_query_id: list[str] | None = typer.Option(
|
|
60
111
|
None, help="Saved query ID whose results are excluded (repeatable)."
|
|
61
112
|
),
|
|
@@ -81,15 +132,33 @@ def discover_command(
|
|
|
81
132
|
negate_country=negate_country,
|
|
82
133
|
state=state,
|
|
83
134
|
negate_state=negate_state,
|
|
135
|
+
subdomain=subdomain,
|
|
136
|
+
negate_subdomain=negate_subdomain,
|
|
137
|
+
language=language,
|
|
138
|
+
negate_language=negate_language,
|
|
139
|
+
social=social,
|
|
140
|
+
negate_social=negate_social,
|
|
84
141
|
employee_range=employee_range,
|
|
85
142
|
revenue_range=revenue_range,
|
|
143
|
+
start_date=start_date,
|
|
86
144
|
business_model=business_model,
|
|
87
145
|
negate_business_model=negate_business_model,
|
|
88
146
|
tech_stack=tech_stack,
|
|
89
147
|
negate_tech_stack=negate_tech_stack,
|
|
90
148
|
min_digital_footprint=min_digital_footprint,
|
|
91
149
|
max_digital_footprint=max_digital_footprint,
|
|
150
|
+
min_similarity=min_similarity,
|
|
151
|
+
variance=variance,
|
|
152
|
+
consensus=consensus,
|
|
153
|
+
redirect=redirect,
|
|
154
|
+
exclude_leadgen=exclude_leadgen,
|
|
155
|
+
retrieval=retrieval,
|
|
156
|
+
enhanced=enhanced,
|
|
157
|
+
include_search_domains=include_search_domains,
|
|
158
|
+
auto_icp_text=auto_icp_text,
|
|
159
|
+
auto_phrase_match=auto_phrase_match,
|
|
92
160
|
exclude_domain=exclude_domain,
|
|
161
|
+
inclusion_query_id=inclusion_query_id,
|
|
93
162
|
exclusion_query_id=exclusion_query_id,
|
|
94
163
|
max_records=max_records,
|
|
95
164
|
offset=offset,
|
|
@@ -109,8 +178,15 @@ def count_command(
|
|
|
109
178
|
negate_country: list[str] | None = typer.Option(None, help="Negate the --country filter (repeatable)."),
|
|
110
179
|
state: list[str] | None = typer.Option(None, help="State or region filter (repeatable)."),
|
|
111
180
|
negate_state: list[str] | None = typer.Option(None, help="Negate the --state filter (repeatable)."),
|
|
181
|
+
subdomain: list[str] | None = typer.Option(None, help=SUBDOMAIN_HELP),
|
|
182
|
+
negate_subdomain: list[str] | None = typer.Option(None, help=NEGATE_SUBDOMAIN_HELP),
|
|
183
|
+
language: list[str] | None = typer.Option(None, help=LANGUAGE_HELP),
|
|
184
|
+
negate_language: list[str] | None = typer.Option(None, help=NEGATE_LANGUAGE_HELP),
|
|
185
|
+
social: list[str] | None = typer.Option(None, help=SOCIAL_HELP),
|
|
186
|
+
negate_social: list[str] | None = typer.Option(None, help=NEGATE_SOCIAL_HELP),
|
|
112
187
|
employee_range: str | None = typer.Option(None, help="Employee count range filter."),
|
|
113
188
|
revenue_range: str | None = typer.Option(None, help="Revenue range filter."),
|
|
189
|
+
start_date: str | None = typer.Option(None, help=START_DATE_HELP),
|
|
114
190
|
business_model: list[str] | None = typer.Option(None, help="Business model filter (repeatable)."),
|
|
115
191
|
negate_business_model: list[str] | None = typer.Option(
|
|
116
192
|
None, help="Negate the --business-model filter (repeatable)."
|
|
@@ -119,6 +195,10 @@ def count_command(
|
|
|
119
195
|
negate_tech_stack: list[str] | None = typer.Option(None, help="Negate the --tech-stack filter (repeatable)."),
|
|
120
196
|
min_digital_footprint: int | None = typer.Option(None, help="Minimum digital footprint score."),
|
|
121
197
|
max_digital_footprint: int | None = typer.Option(None, help="Maximum digital footprint score."),
|
|
198
|
+
redirect: bool | None = typer.Option(None, "--redirect/--no-redirect", help=REDIRECT_HELP),
|
|
199
|
+
exclude_leadgen: bool | None = typer.Option(
|
|
200
|
+
None, "--exclude-leadgen/--no-exclude-leadgen", help=COUNT_EXCLUDE_LEADGEN_HELP
|
|
201
|
+
),
|
|
122
202
|
fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
123
203
|
param: list[str] | None = typer.Option(None, "--param", help=PARAM_HELP),
|
|
124
204
|
) -> None:
|
|
@@ -137,14 +217,23 @@ def count_command(
|
|
|
137
217
|
negate_country=negate_country,
|
|
138
218
|
state=state,
|
|
139
219
|
negate_state=negate_state,
|
|
220
|
+
subdomain=subdomain,
|
|
221
|
+
negate_subdomain=negate_subdomain,
|
|
222
|
+
language=language,
|
|
223
|
+
negate_language=negate_language,
|
|
224
|
+
social=social,
|
|
225
|
+
negate_social=negate_social,
|
|
140
226
|
employee_range=employee_range,
|
|
141
227
|
revenue_range=revenue_range,
|
|
228
|
+
start_date=start_date,
|
|
142
229
|
business_model=business_model,
|
|
143
230
|
negate_business_model=negate_business_model,
|
|
144
231
|
tech_stack=tech_stack,
|
|
145
232
|
negate_tech_stack=negate_tech_stack,
|
|
146
233
|
min_digital_footprint=min_digital_footprint,
|
|
147
234
|
max_digital_footprint=max_digital_footprint,
|
|
235
|
+
redirect=redirect,
|
|
236
|
+
exclude_leadgen=exclude_leadgen,
|
|
148
237
|
),
|
|
149
238
|
)
|
|
150
239
|
emit(get_client(ctx).count(request), fmt=fmt)
|
|
@@ -19,6 +19,7 @@ DEFAULT_WAIT_TIMEOUT_SECONDS = 900.0
|
|
|
19
19
|
FORMAT_HELP = "Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data)."
|
|
20
20
|
WAIT_HELP = "Block until the job finishes, streaming progress to stderr."
|
|
21
21
|
TIMEOUT_HELP = "Max seconds to wait with --wait."
|
|
22
|
+
QUERY_ID_HELP = "Saved query ID whose domains are included alongside the file/--domain ones (repeatable)."
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@handle_errors
|
|
@@ -76,6 +77,7 @@ def append_command(
|
|
|
76
77
|
file: pathlib.Path = typer.Argument(..., help="CSV of domains to enrich."),
|
|
77
78
|
dataset: list[str] | None = typer.Option(None, "--dataset", help="Dataset to append (repeatable)."),
|
|
78
79
|
domain_column: str | None = typer.Option(None, "--domain-column", help="Column in the CSV that holds domains."),
|
|
80
|
+
query_id: list[str] | None = typer.Option(None, "--query-id", help=QUERY_ID_HELP),
|
|
79
81
|
csv: bool | None = typer.Option(
|
|
80
82
|
None, "--csv/--no-csv", help="Request the enriched rows as CSV (written via --output)."
|
|
81
83
|
),
|
|
@@ -85,7 +87,9 @@ def append_command(
|
|
|
85
87
|
"""Enrich a CSV of domains with DiscoLike datasets."""
|
|
86
88
|
from discolike_cli.main import get_client
|
|
87
89
|
|
|
88
|
-
request = build_request(
|
|
90
|
+
request = build_request(
|
|
91
|
+
AppendParams, _merge_params(None, dataset=dataset, domain_column=domain_column, query_id=query_id, csv=csv)
|
|
92
|
+
)
|
|
89
93
|
result = get_client(ctx).append(request, file=file)
|
|
90
94
|
if isinstance(result, bytes):
|
|
91
95
|
if output is None:
|
|
@@ -102,6 +106,7 @@ def segment_command(
|
|
|
102
106
|
domain: list[str] | None = typer.Option(None, "--domain", help="Domain to segment (repeatable)."),
|
|
103
107
|
file: pathlib.Path | None = typer.Option(None, "--file", help="CSV of domains to segment (instead of --domain)."),
|
|
104
108
|
domain_column: str | None = typer.Option(None, "--domain-column", help="Column in --file that holds domains."),
|
|
109
|
+
query_id: list[str] | None = typer.Option(None, "--query-id", help=QUERY_ID_HELP),
|
|
105
110
|
max_segments: int | None = typer.Option(None, "--max-segments", help="Maximum number of segments to produce."),
|
|
106
111
|
wait: bool = typer.Option(False, "--wait", help=WAIT_HELP),
|
|
107
112
|
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout", help=TIMEOUT_HELP),
|
|
@@ -116,11 +121,14 @@ def segment_command(
|
|
|
116
121
|
client = get_client(ctx)
|
|
117
122
|
if file is not None:
|
|
118
123
|
request = build_request(
|
|
119
|
-
SegmentFileParams,
|
|
124
|
+
SegmentFileParams,
|
|
125
|
+
_merge_params(None, domain_column=domain_column, query_id=query_id, max_segments=max_segments),
|
|
120
126
|
)
|
|
121
127
|
job = client.segment_file(request, file=file)
|
|
122
128
|
else:
|
|
123
129
|
assert domain is not None
|
|
124
|
-
request = build_request(
|
|
130
|
+
request = build_request(
|
|
131
|
+
SegmentParams, _merge_params(None, domains=",".join(domain), query_id=query_id, max_segments=max_segments)
|
|
132
|
+
)
|
|
125
133
|
job = client.segment(request)
|
|
126
134
|
run_job(job, wait=wait, timeout=timeout, fmt=fmt)
|
|
@@ -18,6 +18,7 @@ DEFAULT_NAME_COLUMN = "name"
|
|
|
18
18
|
FORMAT_HELP = "Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data)."
|
|
19
19
|
WAIT_HELP = "Block until the job finishes, streaming progress to stderr."
|
|
20
20
|
TIMEOUT_HELP = "Max seconds to wait with --wait."
|
|
21
|
+
MIN_MATCH_CONFIDENCE_HELP = "Minimum match_confidence (inclusive, 50-100) a match must have to be returned."
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@handle_errors
|
|
@@ -29,6 +30,7 @@ def match_command(
|
|
|
29
30
|
state: str | None = typer.Option(None, help="State or region to disambiguate the match."),
|
|
30
31
|
country: str | None = typer.Option(None, help="Country to disambiguate the match."),
|
|
31
32
|
zip_code: str | None = typer.Option(None, help="ZIP or postal code to disambiguate the match."),
|
|
33
|
+
min_match_confidence: int | None = typer.Option(None, help=MIN_MATCH_CONFIDENCE_HELP),
|
|
32
34
|
strict: bool | None = typer.Option(None, "--strict/--no-strict", help="Toggle strict matching."),
|
|
33
35
|
local_mode: bool | None = typer.Option(None, "--local-mode/--no-local-mode", help="Toggle local matching mode."),
|
|
34
36
|
file: pathlib.Path | None = typer.Option(
|
|
@@ -37,6 +39,15 @@ def match_command(
|
|
|
37
39
|
name_column: str = typer.Option(
|
|
38
40
|
DEFAULT_NAME_COLUMN, "--name-column", help="Column in --file that holds company names."
|
|
39
41
|
),
|
|
42
|
+
phone_column: str | None = typer.Option(None, "--phone-column", help="Column in --file that holds phone numbers."),
|
|
43
|
+
city_column: str | None = typer.Option(None, "--city-column", help="Column in --file that holds cities."),
|
|
44
|
+
state_column: str | None = typer.Option(None, "--state-column", help="Column in --file that holds states."),
|
|
45
|
+
country_column: str | None = typer.Option(
|
|
46
|
+
None, "--country-column", help="Column in --file that holds country codes."
|
|
47
|
+
),
|
|
48
|
+
zip_code_column: str | None = typer.Option(
|
|
49
|
+
None, "--zip-code-column", help="Column in --file that holds zip codes."
|
|
50
|
+
),
|
|
40
51
|
wait: bool = typer.Option(False, "--wait", help=WAIT_HELP),
|
|
41
52
|
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout", help=TIMEOUT_HELP),
|
|
42
53
|
fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
@@ -59,6 +70,7 @@ def match_command(
|
|
|
59
70
|
state=state,
|
|
60
71
|
country=country,
|
|
61
72
|
zip_code=zip_code,
|
|
73
|
+
min_match_confidence=min_match_confidence,
|
|
62
74
|
strict=strict,
|
|
63
75
|
local_mode=local_mode,
|
|
64
76
|
),
|
|
@@ -68,6 +80,18 @@ def match_command(
|
|
|
68
80
|
|
|
69
81
|
assert file is not None
|
|
70
82
|
request = build_request(
|
|
71
|
-
MatchBulkParams,
|
|
83
|
+
MatchBulkParams,
|
|
84
|
+
_merge_params(
|
|
85
|
+
None,
|
|
86
|
+
name_column=name_column,
|
|
87
|
+
phone_column=phone_column,
|
|
88
|
+
city_column=city_column,
|
|
89
|
+
state_column=state_column,
|
|
90
|
+
country_column=country_column,
|
|
91
|
+
zip_code_column=zip_code_column,
|
|
92
|
+
min_match_confidence=min_match_confidence,
|
|
93
|
+
strict=strict,
|
|
94
|
+
local_mode=local_mode,
|
|
95
|
+
),
|
|
72
96
|
)
|
|
73
97
|
run_job(client.match.bulk(request, file=file), wait=wait, timeout=timeout, fmt=fmt)
|
|
@@ -144,3 +144,27 @@ def test_extract_hits_extract_endpoint_with_url(install_build_client: Callable[[
|
|
|
144
144
|
assert request.url.path == "/v1/extract"
|
|
145
145
|
assert request.url.params.get("url") == "https://acme.com/about"
|
|
146
146
|
assert json.loads(result.stdout) == {"text": "hello", "language": "en"}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_extract_domain_option_replaces_url(install_build_client: Callable[[Handler], None]) -> None:
|
|
150
|
+
captured: dict[str, httpx2.Request] = {}
|
|
151
|
+
|
|
152
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
153
|
+
captured["request"] = request
|
|
154
|
+
return httpx2.Response(200, json={"text": "hello", "language": "en"})
|
|
155
|
+
|
|
156
|
+
install_build_client(handler)
|
|
157
|
+
result = runner.invoke(app, ["extract", "--domain", "acme.com"])
|
|
158
|
+
assert result.exit_code == 0, result.output
|
|
159
|
+
params = captured["request"].url.params
|
|
160
|
+
assert params.get("domain") == "acme.com"
|
|
161
|
+
assert "url" not in params
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_extract_requires_exactly_one_of_url_or_domain(install_build_client: Callable[[Handler], None]) -> None:
|
|
165
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
166
|
+
return httpx2.Response(200, json={})
|
|
167
|
+
|
|
168
|
+
install_build_client(handler)
|
|
169
|
+
assert runner.invoke(app, ["extract"]).exit_code == 2
|
|
170
|
+
assert runner.invoke(app, ["extract", "https://acme.com", "--domain", "acme.com"]).exit_code == 2
|
|
@@ -76,6 +76,15 @@ def test_contacts_search_sends_options_and_param_escape_hatch(install_build_clie
|
|
|
76
76
|
]
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
def test_contacts_search_icp_text_is_removed(install_build_client: Callable[[Handler], None]) -> None:
|
|
80
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
81
|
+
return httpx2.Response(200, json=[])
|
|
82
|
+
|
|
83
|
+
install_build_client(handler)
|
|
84
|
+
result = runner.invoke(app, ["contacts", "search", "--icp-text", "X"])
|
|
85
|
+
assert result.exit_code == 2
|
|
86
|
+
|
|
87
|
+
|
|
79
88
|
def test_contacts_search_forwards_negate_options(install_build_client: Callable[[Handler], None]) -> None:
|
|
80
89
|
captured: dict[str, httpx2.QueryParams] = {}
|
|
81
90
|
|
|
@@ -113,15 +122,6 @@ def test_contacts_search_forwards_negate_options(install_build_client: Callable[
|
|
|
113
122
|
assert params.get_list("negate_filter_country") == ["RU"]
|
|
114
123
|
|
|
115
124
|
|
|
116
|
-
def test_contacts_search_icp_text_is_removed(install_build_client: Callable[[Handler], None]) -> None:
|
|
117
|
-
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
118
|
-
return httpx2.Response(200, json=[])
|
|
119
|
-
|
|
120
|
-
install_build_client(handler)
|
|
121
|
-
result = runner.invoke(app, ["contacts", "search", "--icp-text", "X"])
|
|
122
|
-
assert result.exit_code == 2
|
|
123
|
-
|
|
124
|
-
|
|
125
125
|
def test_contacts_search_unknown_param_passes_through(install_build_client: Callable[[Handler], None]) -> None:
|
|
126
126
|
captured: dict[str, httpx2.QueryParams] = {}
|
|
127
127
|
|
|
@@ -346,3 +346,156 @@ def test_contacts_generate_without_wait_prints_task_hint(install_build_client: C
|
|
|
346
346
|
payload = json.loads(result.stdout)
|
|
347
347
|
assert payload["task_id"] == "dg-1"
|
|
348
348
|
assert payload["task_family"] == "discogen"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
NEW_CONTACT_FILTER_ARGS = [
|
|
352
|
+
"--name",
|
|
353
|
+
"Jane",
|
|
354
|
+
"--summary",
|
|
355
|
+
"scaled a sales org",
|
|
356
|
+
"--negate-summary",
|
|
357
|
+
"intern",
|
|
358
|
+
"--skills",
|
|
359
|
+
"SQL",
|
|
360
|
+
"--person-state",
|
|
361
|
+
"CA",
|
|
362
|
+
"--persona-id",
|
|
363
|
+
"7",
|
|
364
|
+
"--filter-state",
|
|
365
|
+
"TX",
|
|
366
|
+
"--negate-filter-state",
|
|
367
|
+
"NY",
|
|
368
|
+
"--email-validated",
|
|
369
|
+
"--has-phone",
|
|
370
|
+
"--no-has-mobile",
|
|
371
|
+
"--has-linkedin",
|
|
372
|
+
"--min-connections",
|
|
373
|
+
"500",
|
|
374
|
+
"--inclusion-query-id",
|
|
375
|
+
"q1",
|
|
376
|
+
"--exclusion-query-id",
|
|
377
|
+
"q2",
|
|
378
|
+
"--max-companies",
|
|
379
|
+
"10",
|
|
380
|
+
"--results-by-company",
|
|
381
|
+
"2",
|
|
382
|
+
"--include-search-contacts",
|
|
383
|
+
"--consensus",
|
|
384
|
+
"3",
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _assert_new_contact_filters(params: httpx2.QueryParams) -> None:
|
|
389
|
+
assert params.get("name") == "Jane"
|
|
390
|
+
assert params.get("summary") == "scaled a sales org"
|
|
391
|
+
assert params.get("negate_summary") == "intern"
|
|
392
|
+
assert params.get_list("skills") == ["SQL"]
|
|
393
|
+
assert params.get_list("person_state") == ["CA"]
|
|
394
|
+
assert params.get_list("persona_id") == ["7"]
|
|
395
|
+
assert params.get_list("filter_state") == ["TX"]
|
|
396
|
+
assert params.get_list("negate_filter_state") == ["NY"]
|
|
397
|
+
assert params.get("email_validated") == "true"
|
|
398
|
+
assert params.get("has_phone") == "true"
|
|
399
|
+
assert params.get("has_mobile") == "false"
|
|
400
|
+
assert params.get("has_linkedin") == "true"
|
|
401
|
+
assert params.get("min_connections") == "500"
|
|
402
|
+
assert params.get_list("inclusion_query_id") == ["q1"]
|
|
403
|
+
assert params.get_list("exclusion_query_id") == ["q2"]
|
|
404
|
+
assert params.get("max_companies") == "10"
|
|
405
|
+
assert params.get("results_by_company") == "2"
|
|
406
|
+
assert params.get("include_search_contacts") == "true"
|
|
407
|
+
assert params.get("consensus") == "3"
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_contacts_search_forwards_every_new_flag(install_build_client: Callable[[Handler], None]) -> None:
|
|
411
|
+
captured: dict[str, httpx2.QueryParams] = {}
|
|
412
|
+
|
|
413
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
414
|
+
captured["params"] = request.url.params
|
|
415
|
+
return httpx2.Response(200, json=[])
|
|
416
|
+
|
|
417
|
+
install_build_client(handler)
|
|
418
|
+
result = runner.invoke(app, ["contacts", "search", *NEW_CONTACT_FILTER_ARGS])
|
|
419
|
+
assert result.exit_code == 0, result.output
|
|
420
|
+
_assert_new_contact_filters(captured["params"])
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_contacts_count_forwards_every_new_flag(install_build_client: Callable[[Handler], None]) -> None:
|
|
424
|
+
captured: dict[str, httpx2.QueryParams] = {}
|
|
425
|
+
|
|
426
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
427
|
+
captured["params"] = request.url.params
|
|
428
|
+
return httpx2.Response(200, json={"count": 1})
|
|
429
|
+
|
|
430
|
+
install_build_client(handler)
|
|
431
|
+
result = runner.invoke(app, ["contacts", "count", *NEW_CONTACT_FILTER_ARGS, "--max-records", "20", "--offset", "5"])
|
|
432
|
+
assert result.exit_code == 0, result.output
|
|
433
|
+
params = captured["params"]
|
|
434
|
+
_assert_new_contact_filters(params)
|
|
435
|
+
assert params.get("max_records") == "20"
|
|
436
|
+
assert params.get("offset") == "5"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_contacts_discover_forwards_every_new_flag(install_build_client: Callable[[Handler], None]) -> None:
|
|
440
|
+
captured: dict[str, object] = {}
|
|
441
|
+
|
|
442
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
443
|
+
captured["body"] = json.loads(request.content)
|
|
444
|
+
return httpx2.Response(200, json={"results": {}, "total_contacts": 0})
|
|
445
|
+
|
|
446
|
+
install_build_client(handler)
|
|
447
|
+
result = runner.invoke(app, ["contacts", "discover", *NEW_CONTACT_FILTER_ARGS])
|
|
448
|
+
assert result.exit_code == 0, result.output
|
|
449
|
+
assert captured["body"] == {
|
|
450
|
+
"name": "Jane",
|
|
451
|
+
"summary": "scaled a sales org",
|
|
452
|
+
"negate_summary": "intern",
|
|
453
|
+
"skills": ["SQL"],
|
|
454
|
+
"person_state": ["CA"],
|
|
455
|
+
"persona_id": [7],
|
|
456
|
+
"filter_state": ["TX"],
|
|
457
|
+
"negate_filter_state": ["NY"],
|
|
458
|
+
"email_validated": True,
|
|
459
|
+
"has_phone": True,
|
|
460
|
+
"has_mobile": False,
|
|
461
|
+
"has_linkedin": True,
|
|
462
|
+
"min_connections": 500,
|
|
463
|
+
"inclusion_query_id": ["q1"],
|
|
464
|
+
"exclusion_query_id": ["q2"],
|
|
465
|
+
"max_companies": 10,
|
|
466
|
+
"results_by_company": 2,
|
|
467
|
+
"include_search_contacts": True,
|
|
468
|
+
"consensus": 3,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def test_contacts_generate_forwards_full_and_partial_domains(install_build_client: Callable[[Handler], None]) -> None:
|
|
473
|
+
captured: dict[str, object] = {}
|
|
474
|
+
|
|
475
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
476
|
+
captured["body"] = json.loads(request.content)
|
|
477
|
+
return httpx2.Response(200, json={"task_id": "dg-9"})
|
|
478
|
+
|
|
479
|
+
install_build_client(handler)
|
|
480
|
+
result = runner.invoke(
|
|
481
|
+
app,
|
|
482
|
+
[
|
|
483
|
+
"contacts",
|
|
484
|
+
"generate",
|
|
485
|
+
"--icp-text",
|
|
486
|
+
"VPs of Marketing",
|
|
487
|
+
"--domain",
|
|
488
|
+
"acme.com",
|
|
489
|
+
"--full-domain",
|
|
490
|
+
"done.com",
|
|
491
|
+
"--partial-domain",
|
|
492
|
+
"half.com",
|
|
493
|
+
],
|
|
494
|
+
)
|
|
495
|
+
assert result.exit_code == 0, result.output
|
|
496
|
+
assert captured["body"] == {
|
|
497
|
+
"icp_text": "VPs of Marketing",
|
|
498
|
+
"domains": ["acme.com"],
|
|
499
|
+
"full_domains": ["done.com"],
|
|
500
|
+
"partial_domains": ["half.com"],
|
|
501
|
+
}
|
|
@@ -118,6 +118,120 @@ def test_discover_icp_text_option_removed(install_build_client: Callable[[Handle
|
|
|
118
118
|
assert result.exit_code == 2
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
def test_discover_forwards_every_new_flag(install_build_client: Callable[[Handler], None]) -> None:
|
|
122
|
+
captured: dict[str, httpx2.QueryParams] = {}
|
|
123
|
+
|
|
124
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
125
|
+
captured["params"] = request.url.params
|
|
126
|
+
return _discover_ok(request)
|
|
127
|
+
|
|
128
|
+
install_build_client(handler)
|
|
129
|
+
result = runner.invoke(
|
|
130
|
+
app,
|
|
131
|
+
[
|
|
132
|
+
"discover",
|
|
133
|
+
"--subdomain",
|
|
134
|
+
"shop",
|
|
135
|
+
"--negate-subdomain",
|
|
136
|
+
"blog",
|
|
137
|
+
"--language",
|
|
138
|
+
"en",
|
|
139
|
+
"--negate-language",
|
|
140
|
+
"de",
|
|
141
|
+
"--social",
|
|
142
|
+
"linkedin",
|
|
143
|
+
"--negate-social",
|
|
144
|
+
"tiktok",
|
|
145
|
+
"--start-date",
|
|
146
|
+
"2020-01-01",
|
|
147
|
+
"--min-similarity",
|
|
148
|
+
"40",
|
|
149
|
+
"--variance",
|
|
150
|
+
"MEDIUM",
|
|
151
|
+
"--consensus",
|
|
152
|
+
"3",
|
|
153
|
+
"--redirect",
|
|
154
|
+
"--no-exclude-leadgen",
|
|
155
|
+
"--retrieval",
|
|
156
|
+
"--enhanced",
|
|
157
|
+
"--include-search-domains",
|
|
158
|
+
"--auto-icp-text",
|
|
159
|
+
"--auto-phrase-match",
|
|
160
|
+
"--inclusion-query-id",
|
|
161
|
+
"q1",
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
assert result.exit_code == 0, result.output
|
|
165
|
+
params = captured["params"]
|
|
166
|
+
assert params.get_list("subdomain") == ["shop"]
|
|
167
|
+
assert params.get_list("negate_subdomain") == ["blog"]
|
|
168
|
+
assert params.get_list("language") == ["en"]
|
|
169
|
+
assert params.get_list("negate_language") == ["de"]
|
|
170
|
+
assert params.get_list("social") == ["linkedin"]
|
|
171
|
+
assert params.get_list("negate_social") == ["tiktok"]
|
|
172
|
+
assert params.get("start_date") == "2020-01-01"
|
|
173
|
+
assert params.get("min_similarity") == "40"
|
|
174
|
+
assert params.get("variance") == "MEDIUM"
|
|
175
|
+
assert params.get("consensus") == "3"
|
|
176
|
+
assert params.get("redirect") == "true"
|
|
177
|
+
assert params.get("exclude_leadgen") == "false"
|
|
178
|
+
assert params.get("retrieval") == "true"
|
|
179
|
+
assert params.get("enhanced") == "true"
|
|
180
|
+
assert params.get("include_search_domains") == "true"
|
|
181
|
+
assert params.get("auto_icp_text") == "true"
|
|
182
|
+
assert params.get("auto_phrase_match") == "true"
|
|
183
|
+
assert params.get_list("inclusion_query_id") == ["q1"]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_discover_rejects_invalid_variance(install_build_client: Callable[[Handler], None]) -> None:
|
|
187
|
+
install_build_client(_discover_ok)
|
|
188
|
+
result = runner.invoke(app, ["discover", "--variance", "BOGUS"])
|
|
189
|
+
assert result.exit_code == 2
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_count_forwards_every_new_flag(install_build_client: Callable[[Handler], None]) -> None:
|
|
193
|
+
captured: dict[str, httpx2.QueryParams] = {}
|
|
194
|
+
|
|
195
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
196
|
+
captured["params"] = request.url.params
|
|
197
|
+
return _count_ok(request)
|
|
198
|
+
|
|
199
|
+
install_build_client(handler)
|
|
200
|
+
result = runner.invoke(
|
|
201
|
+
app,
|
|
202
|
+
[
|
|
203
|
+
"count",
|
|
204
|
+
"--subdomain",
|
|
205
|
+
"shop",
|
|
206
|
+
"--negate-subdomain",
|
|
207
|
+
"blog",
|
|
208
|
+
"--language",
|
|
209
|
+
"en",
|
|
210
|
+
"--negate-language",
|
|
211
|
+
"de",
|
|
212
|
+
"--social",
|
|
213
|
+
"linkedin",
|
|
214
|
+
"--negate-social",
|
|
215
|
+
"tiktok",
|
|
216
|
+
"--start-date",
|
|
217
|
+
"2020-01-01,2021-01-01",
|
|
218
|
+
"--no-redirect",
|
|
219
|
+
"--exclude-leadgen",
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
assert result.exit_code == 0, result.output
|
|
223
|
+
params = captured["params"]
|
|
224
|
+
assert params.get_list("subdomain") == ["shop"]
|
|
225
|
+
assert params.get_list("negate_subdomain") == ["blog"]
|
|
226
|
+
assert params.get_list("language") == ["en"]
|
|
227
|
+
assert params.get_list("negate_language") == ["de"]
|
|
228
|
+
assert params.get_list("social") == ["linkedin"]
|
|
229
|
+
assert params.get_list("negate_social") == ["tiktok"]
|
|
230
|
+
assert params.get("start_date") == "2020-01-01,2021-01-01"
|
|
231
|
+
assert params.get("redirect") == "false"
|
|
232
|
+
assert params.get("exclude_leadgen") == "true"
|
|
233
|
+
|
|
234
|
+
|
|
121
235
|
def test_count_sends_shared_filter_subset(install_build_client: Callable[[Handler], None]) -> None:
|
|
122
236
|
captured: dict[str, httpx2.QueryParams] = {}
|
|
123
237
|
|
|
@@ -234,3 +234,40 @@ def test_segment_neither_domain_nor_file_exits_2(install_build_client: Callable[
|
|
|
234
234
|
install_build_client(handler)
|
|
235
235
|
result = runner.invoke(app, ["segment"])
|
|
236
236
|
assert result.exit_code == 2
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_append_forwards_query_id(tmp_path, install_build_client: Callable[[Handler], None]) -> None:
|
|
240
|
+
domains_file = tmp_path / "domains.csv"
|
|
241
|
+
domains_file.write_text("domain\nacme.com\n")
|
|
242
|
+
captured: dict[str, httpx2.Request] = {}
|
|
243
|
+
|
|
244
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
245
|
+
captured["request"] = request
|
|
246
|
+
return httpx2.Response(200, json=[{"domain": "acme.com"}])
|
|
247
|
+
|
|
248
|
+
install_build_client(handler)
|
|
249
|
+
result = runner.invoke(
|
|
250
|
+
app, ["append", str(domains_file), "--dataset", "bizdata", "--query-id", "q1", "--query-id", "q2"]
|
|
251
|
+
)
|
|
252
|
+
assert result.exit_code == 0, result.output
|
|
253
|
+
assert captured["request"].url.params.get_list("query_id") == ["q1", "q2"]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_segment_forwards_query_id_for_domains_and_file(
|
|
257
|
+
tmp_path, install_build_client: Callable[[Handler], None]
|
|
258
|
+
) -> None:
|
|
259
|
+
domains_file = tmp_path / "domains.csv"
|
|
260
|
+
domains_file.write_text("domain\nacme.com\n")
|
|
261
|
+
captured: list[httpx2.Request] = []
|
|
262
|
+
|
|
263
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
264
|
+
captured.append(request)
|
|
265
|
+
return httpx2.Response(200, json={"task_id": "sg-1"})
|
|
266
|
+
|
|
267
|
+
install_build_client(handler)
|
|
268
|
+
result = runner.invoke(app, ["segment", "--domain", "acme.com", "--query-id", "q1"])
|
|
269
|
+
assert result.exit_code == 0, result.output
|
|
270
|
+
result = runner.invoke(app, ["segment", "--file", str(domains_file), "--query-id", "q2"])
|
|
271
|
+
assert result.exit_code == 0, result.output
|
|
272
|
+
assert captured[0].url.params.get_list("query_id") == ["q1"]
|
|
273
|
+
assert captured[1].url.params.get_list("query_id") == ["q2"]
|
|
@@ -217,3 +217,58 @@ def test_match_bulk_local_mode_sent_when_passed(tmp_path, install_build_client:
|
|
|
217
217
|
result = runner.invoke(app, ["match", "--file", str(names_file), "--local-mode"])
|
|
218
218
|
assert result.exit_code == 0, result.output
|
|
219
219
|
assert captured["request"].url.params.get("local_mode") == "true"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_match_single_forwards_min_match_confidence(install_build_client: Callable[[Handler], None]) -> None:
|
|
223
|
+
captured: dict[str, httpx2.Request] = {}
|
|
224
|
+
|
|
225
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
226
|
+
captured["request"] = request
|
|
227
|
+
return httpx2.Response(200, json={"domain": "acme.com"})
|
|
228
|
+
|
|
229
|
+
install_build_client(handler)
|
|
230
|
+
result = runner.invoke(app, ["match", "Acme", "--min-match-confidence", "80"])
|
|
231
|
+
assert result.exit_code == 0, result.output
|
|
232
|
+
assert captured["request"].url.params.get("min_match_confidence") == "80"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_match_bulk_forwards_column_flags_and_confidence(
|
|
236
|
+
tmp_path, install_build_client: Callable[[Handler], None]
|
|
237
|
+
) -> None:
|
|
238
|
+
names_file = tmp_path / "names.csv"
|
|
239
|
+
names_file.write_text("name,phone,city,state,country,zip\nAcme,555,Austin,TX,US,78701\n")
|
|
240
|
+
captured: dict[str, httpx2.Request] = {}
|
|
241
|
+
|
|
242
|
+
def handler(request: httpx2.Request) -> httpx2.Response:
|
|
243
|
+
captured["request"] = request
|
|
244
|
+
return httpx2.Response(200, json={"task_id": "bm-6"})
|
|
245
|
+
|
|
246
|
+
install_build_client(handler)
|
|
247
|
+
result = runner.invoke(
|
|
248
|
+
app,
|
|
249
|
+
[
|
|
250
|
+
"match",
|
|
251
|
+
"--file",
|
|
252
|
+
str(names_file),
|
|
253
|
+
"--phone-column",
|
|
254
|
+
"phone",
|
|
255
|
+
"--city-column",
|
|
256
|
+
"city",
|
|
257
|
+
"--state-column",
|
|
258
|
+
"state",
|
|
259
|
+
"--country-column",
|
|
260
|
+
"country",
|
|
261
|
+
"--zip-code-column",
|
|
262
|
+
"zip",
|
|
263
|
+
"--min-match-confidence",
|
|
264
|
+
"75",
|
|
265
|
+
],
|
|
266
|
+
)
|
|
267
|
+
assert result.exit_code == 0, result.output
|
|
268
|
+
params = captured["request"].url.params
|
|
269
|
+
assert params.get("phone_column") == "phone"
|
|
270
|
+
assert params.get("city_column") == "city"
|
|
271
|
+
assert params.get("state_column") == "state"
|
|
272
|
+
assert params.get("country_column") == "country"
|
|
273
|
+
assert params.get("zip_code_column") == "zip"
|
|
274
|
+
assert params.get("min_match_confidence") == "75"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Every SDK request field must be reachable as a first-class CLI flag.
|
|
2
|
+
|
|
3
|
+
Reads the CLI source with ``ast`` rather than invoking typer, so a new field on
|
|
4
|
+
a request model fails here until the matching command forwards it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import pathlib
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import discolike.requests as requests_module
|
|
16
|
+
|
|
17
|
+
CLI_SOURCE_DIR = pathlib.Path(__file__).resolve().parents[1] / "src" / "discolike_cli"
|
|
18
|
+
REQUEST_BUILDER = "build_request"
|
|
19
|
+
# icp_text was dropped from discover and contacts in 0.1.1 in favor of icp_prompt; see the *_icp_text_*_removed tests.
|
|
20
|
+
DELIBERATELY_OMITTED: dict[str, frozenset[str]] = {
|
|
21
|
+
"DiscoverParams": frozenset({"icp_text"}),
|
|
22
|
+
"ContactsSearchParams": frozenset({"icp_text"}),
|
|
23
|
+
"ContactsCountParams": frozenset({"icp_text"}),
|
|
24
|
+
"ContactFilters": frozenset({"icp_text"}),
|
|
25
|
+
}
|
|
26
|
+
DICT_ONLY_FIELDS: dict[str, frozenset[str]] = {
|
|
27
|
+
"ContactGenerateRequest": frozenset({"initial_contact_counts"}),
|
|
28
|
+
"DiscoGenProcessRequest": frozenset({"previous_discogen_data"}),
|
|
29
|
+
"DiscoGenPersonaProcessRequest": frozenset({"previous_discogen_data"}),
|
|
30
|
+
"SaveResultsRequest": frozenset({"query_params"}),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _keyword_names(call: ast.Call) -> set[str]:
|
|
35
|
+
return {keyword.arg for keyword in call.keywords if keyword.arg is not None}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _dict_keys(node: ast.Dict, assignments: dict[str, ast.Call]) -> set[str]:
|
|
39
|
+
keys: set[str] = set()
|
|
40
|
+
for key, value in zip(node.keys, node.values, strict=True):
|
|
41
|
+
if key is None:
|
|
42
|
+
keys |= _forwarded_fields(argument=value, assignments=assignments)
|
|
43
|
+
elif isinstance(key, ast.Constant) and isinstance(key.value, str):
|
|
44
|
+
keys.add(key.value)
|
|
45
|
+
return keys
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _forwarded_fields(argument: ast.expr, assignments: dict[str, ast.Call]) -> set[str]:
|
|
49
|
+
if isinstance(argument, ast.Name) and argument.id in assignments:
|
|
50
|
+
argument = assignments[argument.id]
|
|
51
|
+
if isinstance(argument, ast.Call):
|
|
52
|
+
return _keyword_names(argument)
|
|
53
|
+
if isinstance(argument, ast.Dict):
|
|
54
|
+
return _dict_keys(node=argument, assignments=assignments)
|
|
55
|
+
return set()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _build_request_sites() -> Iterator[tuple[str, str, set[str]]]:
|
|
59
|
+
for source_file in sorted(CLI_SOURCE_DIR.glob("*.py")):
|
|
60
|
+
tree = ast.parse(source_file.read_text())
|
|
61
|
+
for function in (node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)):
|
|
62
|
+
assignments = {
|
|
63
|
+
node.targets[0].id: node.value
|
|
64
|
+
for node in ast.walk(function)
|
|
65
|
+
if isinstance(node, ast.Assign)
|
|
66
|
+
and len(node.targets) == 1
|
|
67
|
+
and isinstance(node.targets[0], ast.Name)
|
|
68
|
+
and isinstance(node.value, ast.Call)
|
|
69
|
+
}
|
|
70
|
+
for call in (node for node in ast.walk(function) if isinstance(node, ast.Call)):
|
|
71
|
+
callee = call.func.id if isinstance(call.func, ast.Name) else getattr(call.func, "attr", "")
|
|
72
|
+
if callee != REQUEST_BUILDER or not call.args or not isinstance(call.args[0], ast.Name):
|
|
73
|
+
continue
|
|
74
|
+
forwarded: set[str] = set()
|
|
75
|
+
for argument in call.args[1:]:
|
|
76
|
+
forwarded |= _forwarded_fields(argument=argument, assignments=assignments)
|
|
77
|
+
yield f"{source_file.stem}.{function.name}", call.args[0].id, forwarded
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
SITES = list(_build_request_sites())
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize(("site", "model_name", "forwarded"), SITES, ids=[f"{s}->{m}" for s, m, _ in SITES])
|
|
84
|
+
def test_cli_forwards_every_sdk_field(site: str, model_name: str, forwarded: set[str]) -> None:
|
|
85
|
+
model = getattr(requests_module, model_name)
|
|
86
|
+
excluded = DICT_ONLY_FIELDS.get(model_name, frozenset()) | DELIBERATELY_OMITTED.get(model_name, frozenset())
|
|
87
|
+
expected = set(model.model_fields) - excluded
|
|
88
|
+
missing = sorted(expected - forwarded)
|
|
89
|
+
assert not missing, f"{site} does not forward {model_name} fields: {', '.join(missing)}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_every_request_model_has_a_cli_command() -> None:
|
|
93
|
+
used = {model_name for _, model_name, _ in SITES}
|
|
94
|
+
declared = {
|
|
95
|
+
name
|
|
96
|
+
for name in dir(requests_module)
|
|
97
|
+
if name.endswith(("Params", "Request")) and hasattr(getattr(requests_module, name), "model_fields")
|
|
98
|
+
}
|
|
99
|
+
assert not sorted(declared - used), f"request models with no CLI command: {sorted(declared - used)}"
|
|
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
|