discolike-cli 0.6.0__tar.gz → 0.6.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.
Files changed (48) hide show
  1. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/PKG-INFO +3 -2
  2. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/pyproject.toml +3 -2
  3. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/_output.py +6 -0
  4. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/bulk.py +5 -3
  5. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/discover.py +10 -5
  6. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/providers.py +51 -6
  7. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_bulk_cli.py +5 -2
  8. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_discogen_cli.py +4 -1
  9. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_discover.py +5 -2
  10. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_file_inputs_cli.py +4 -4
  11. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_output.py +14 -0
  12. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_providers_cli.py +65 -0
  13. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/.gitignore +0 -0
  14. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/LICENSE +0 -0
  15. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/README.md +0 -0
  16. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/__init__.py +0 -0
  17. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/_help.py +0 -0
  18. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/_inputs.py +0 -0
  19. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/_loopback.py +0 -0
  20. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/account.py +0 -0
  21. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/auth.py +0 -0
  22. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/company.py +0 -0
  23. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/contacts.py +0 -0
  24. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/discogen.py +0 -0
  25. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/email.py +0 -0
  26. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/enrich.py +0 -0
  27. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/main.py +0 -0
  28. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/match.py +0 -0
  29. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/prospecting.py +0 -0
  30. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/py.typed +0 -0
  31. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/queries.py +0 -0
  32. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/src/discolike_cli/signup.py +0 -0
  33. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/conftest.py +0 -0
  34. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_account.py +0 -0
  35. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_auth.py +0 -0
  36. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_auth_oauth.py +0 -0
  37. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_company.py +0 -0
  38. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_contacts_cli.py +0 -0
  39. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_email_cli.py +0 -0
  40. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_enrich_cli.py +0 -0
  41. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_entrypoint.py +0 -0
  42. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_help.py +0 -0
  43. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_inputs.py +0 -0
  44. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_match_cli.py +0 -0
  45. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_prospecting_cli.py +0 -0
  46. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_queries_cli.py +0 -0
  47. {discolike_cli-0.6.0 → discolike_cli-0.6.2}/tests/test_sdk_parity.py +0 -0
  48. {discolike_cli-0.6.0 → discolike_cli-0.6.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.6.0
3
+ Version: 0.6.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
@@ -21,10 +21,11 @@ Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
24
25
  Classifier: Topic :: Utilities
25
26
  Classifier: Typing :: Typed
26
27
  Requires-Python: >=3.11
27
- Requires-Dist: discolike==0.6.0
28
+ Requires-Dist: discolike==0.6.2
28
29
  Requires-Dist: rich>=13.0
29
30
  Requires-Dist: typer<0.28,>=0.26.1
30
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.6.0"
7
+ version = "0.6.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.11"
13
13
  authors = [{ name = "DiscoLike", email = "support@discolike.com" }]
14
14
  dependencies = [
15
- "discolike==0.6.0",
15
+ "discolike==0.6.2",
16
16
  "typer>=0.26.1,<0.28",
17
17
  "rich>=13.0",
18
18
  ]
@@ -25,6 +25,7 @@ classifiers = [
25
25
  "Programming Language :: Python :: 3.12",
26
26
  "Programming Language :: Python :: 3.13",
27
27
  "Programming Language :: Python :: 3.14",
28
+ "Programming Language :: Python :: 3.15",
28
29
  "Programming Language :: Python :: 3 :: Only",
29
30
  "Operating System :: OS Independent",
30
31
  "Environment :: Console",
@@ -20,6 +20,7 @@ from discolike._config import NO_CREDENTIAL_MESSAGE
20
20
  from discolike._exceptions import APIConnectionError
21
21
  from discolike._exceptions import AuthenticationError
22
22
  from discolike._exceptions import DiscolikeError
23
+ from discolike._exceptions import InsufficientBalanceError
23
24
  from discolike._exceptions import JobFailedError
24
25
  from discolike._exceptions import JobTimeoutError
25
26
  from discolike._exceptions import NotFoundError
@@ -38,6 +39,7 @@ EXIT_CODES: dict[type, int] = {
38
39
  ValidationError: 2,
39
40
  AuthenticationError: 3,
40
41
  PlanAccessError: 3,
42
+ InsufficientBalanceError: 3,
41
43
  RateLimitError: 4,
42
44
  APIConnectionError: 5,
43
45
  NotFoundError: 6,
@@ -51,6 +53,7 @@ ERROR_CODES: dict[type, str] = {
51
53
  ValidationError: "validation_error",
52
54
  AuthenticationError: "auth_invalid",
53
55
  PlanAccessError: "plan_access",
56
+ InsufficientBalanceError: "insufficient_balance",
54
57
  RateLimitError: "rate_limited",
55
58
  APIConnectionError: "network_error",
56
59
  NotFoundError: "not_found",
@@ -150,6 +153,9 @@ def fail(exc: DiscolikeError) -> typer.Exit:
150
153
  }
151
154
  if isinstance(exc, RateLimitError) and exc.retry_after is not None:
152
155
  payload["retry_after"] = exc.retry_after
156
+ if isinstance(exc, InsufficientBalanceError):
157
+ payload["max_affordable_records"] = exc.max_affordable_records
158
+ payload["remaining_usd"] = exc.remaining_usd
153
159
  print(json.dumps(payload), file=sys.stderr)
154
160
  return typer.Exit(code=exit_code)
155
161
 
@@ -40,6 +40,8 @@ from discolike_cli._output import handle_errors
40
40
  from discolike_cli.discover import DOMAINS_FILE_HELP
41
41
  from discolike_cli.discover import PARAM_HELP
42
42
  from discolike_cli.discover import PARAMS_FILE_HELP
43
+ from discolike_cli.discover import RELEVANCE_HELP
44
+ from discolike_cli.discover import VARIANCE_HELP
43
45
  from discolike_cli.discover import _merge_params
44
46
 
45
47
  T = TypeVar("T")
@@ -157,9 +159,8 @@ def companies_command(
157
159
  state: list[str] | None = typer.Option(None, help="State or region filter (repeatable)."),
158
160
  employee_range: str | None = typer.Option(None, help="Employee count range filter."),
159
161
  revenue_range: str | None = typer.Option(None, help="Revenue range filter."),
160
- variance: str | None = typer.Option(
161
- None, help="Result diversity: LOW, MID_LOW, MEDIUM, MID_HIGH, HIGH, UNRESTRICTED."
162
- ),
162
+ relevance: str | None = typer.Option(None, help=RELEVANCE_HELP),
163
+ variance: str | None = typer.Option(None, help=VARIANCE_HELP),
163
164
  exclusion_query_id: list[str] | None = typer.Option(None, help=EXCLUSION_QUERY_ID_HELP),
164
165
  param: list[str] | None = typer.Option(None, "--param", help=PARAM_HELP),
165
166
  params_file: pathlib.Path | None = typer.Option(None, help=PARAMS_FILE_HELP),
@@ -186,6 +187,7 @@ def companies_command(
186
187
  state=state,
187
188
  employee_range=employee_range,
188
189
  revenue_range=revenue_range,
190
+ relevance=relevance,
189
191
  variance=variance,
190
192
  ),
191
193
  COMPANIES_MANAGED,
@@ -43,6 +43,11 @@ LANGUAGE_HELP = "Filter by site language as an ISO 639-1 code, e.g. en, de, pb,
43
43
  NEGATE_LANGUAGE_HELP = "Exclude this site language (repeatable); same values as --language."
44
44
  EXCLUDE_LEADGEN_HELP = "Exclude suspected lead generation sites (on by default)."
45
45
  COUNT_EXCLUDE_LEADGEN_HELP = "Exclude suspected lead generation sites (off by default for count)."
46
+ RELEVANCE_HELP = (
47
+ "Where the list stops, by estimated ICP fit: MAX_RELEVANCE (tightest), BALANCED, MAX_COVERAGE (longest). "
48
+ "Omit for no automatic stop."
49
+ )
50
+ VARIANCE_HELP = "Deprecated, mapped onto --relevance by the API; use --relevance."
46
51
 
47
52
 
48
53
  def _parse_param(raw: str) -> tuple[str, str | list[str]]:
@@ -104,11 +109,10 @@ def discover_command(
104
109
  min_digital_footprint: int | None = typer.Option(None, help="Minimum digital footprint score."),
105
110
  max_digital_footprint: int | None = typer.Option(None, help="Maximum digital footprint score."),
106
111
  min_similarity: int | None = typer.Option(None, help="Minimum similarity score to include (0-99)."),
107
- variance: str | None = typer.Option(
108
- None, help="Result diversity control: LOW, MID_LOW, MEDIUM, MID_HIGH, HIGH, UNRESTRICTED."
109
- ),
112
+ relevance: str | None = typer.Option(None, help=RELEVANCE_HELP),
113
+ variance: str | None = typer.Option(None, help=VARIANCE_HELP),
110
114
  consensus: int | None = typer.Option(
111
- None, help="Number of top results for the consensus search vector (1-20); higher reduces specificity."
115
+ None, help="Deprecated, no-op. Discover applies consensus itself to single-domain lookalike searches."
112
116
  ),
113
117
  redirect: bool | None = typer.Option(None, "--redirect/--no-redirect", help=REDIRECT_HELP),
114
118
  exclude_leadgen: bool | None = typer.Option(
@@ -118,7 +122,7 @@ def discover_command(
118
122
  None, "--retrieval/--no-retrieval", help="Enable page data retrieval using the Extract API."
119
123
  ),
120
124
  enhanced: bool | None = typer.Option(
121
- None, "--enhanced/--no-enhanced", help="Enable AI-powered result enhancement for improved relevance."
125
+ None, "--enhanced/--no-enhanced", help="Deprecated, no-op. Discover no longer adds AI-suggested domains."
122
126
  ),
123
127
  include_search_domains: bool | None = typer.Option(
124
128
  None,
@@ -186,6 +190,7 @@ def discover_command(
186
190
  min_digital_footprint=min_digital_footprint,
187
191
  max_digital_footprint=max_digital_footprint,
188
192
  min_similarity=min_similarity,
193
+ relevance=relevance,
189
194
  variance=variance,
190
195
  consensus=consensus,
191
196
  redirect=redirect,
@@ -1,5 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from urllib.parse import parse_qsl
4
+ from urllib.parse import urlencode
5
+
3
6
  import typer
4
7
 
5
8
  from discolike.requests import LLMProviderCreateRequest
@@ -15,6 +18,23 @@ FORMAT_HELP = "Output format: json or table (table auto-selected on a TTY; falls
15
18
  search_providers_app = typer.Typer(help="Manage BYOK web search provider integrations (Tavily, Serper, etc.).")
16
19
  llm_providers_app = typer.Typer(help="Manage BYOK LLM provider integrations (OpenAI, Anthropic, custom endpoints).")
17
20
 
21
+ LLM_PROVIDER_HELP = "LLM provider key, e.g. openai, anthropic, bedrock, snowflake, watsonx, or custom."
22
+ REGION_HELP = (
23
+ "Provider region: an AWS region for bedrock and bedrock_mantle, a watsonx region (us-south, eu-de, ...) "
24
+ "for watsonx. `discogen models` lists them under provider_regions."
25
+ )
26
+ ACCOUNT_HELP = "Snowflake account identifier, e.g. orgname-accountname (snowflake only)."
27
+ PROJECT_HELP = "watsonx project ID (watsonx only)."
28
+
29
+
30
+ def _with_model_settings(model_name: str, *, region: str | None, account: str | None, project: str | None) -> str:
31
+ """The API reads provider settings from model_name after "#", e.g. "<model>#region=us-east-1"."""
32
+ flags = {key: value for key, value in (("region", region), ("account", account), ("project", project)) if value}
33
+ if not flags:
34
+ return model_name
35
+ base, _, existing = model_name.partition("#")
36
+ return f"{base}#{urlencode(dict(parse_qsl(existing)) | flags)}"
37
+
18
38
 
19
39
  @search_providers_app.command("list")
20
40
  @handle_errors
@@ -149,10 +169,13 @@ def llm_list_command(
149
169
  def llm_create_command(
150
170
  ctx: typer.Context,
151
171
  name: str = typer.Option(..., "--name", help="User-friendly name for the integration."),
152
- provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."),
172
+ provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP),
153
173
  api_key: str = typer.Option(..., "--api-key", help="API key for the provider."),
154
174
  model_name: str = typer.Option(..., "--model-name", help="Default model name for this integration."),
155
175
  base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."),
176
+ region: str | None = typer.Option(None, "--region", help=REGION_HELP),
177
+ account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP),
178
+ project: str | None = typer.Option(None, "--project", help=PROJECT_HELP),
156
179
  ) -> None:
157
180
  """Create an LLM provider integration."""
158
181
  from discolike_cli.main import get_client
@@ -160,7 +183,12 @@ def llm_create_command(
160
183
  request = build_request(
161
184
  LLMProviderCreateRequest,
162
185
  _merge_params(
163
- None, integration_name=name, provider=provider, api_key=api_key, model_name=model_name, base_url=base_url
186
+ None,
187
+ integration_name=name,
188
+ provider=provider,
189
+ api_key=api_key,
190
+ model_name=_with_model_settings(model_name, region=region, account=account, project=project),
191
+ base_url=base_url,
164
192
  ),
165
193
  )
166
194
  emit(get_client(ctx).llm_providers.create(request))
@@ -185,10 +213,13 @@ def llm_update_command(
185
213
  ctx: typer.Context,
186
214
  integration_id: str = typer.Argument(..., help="ID of the LLM provider integration to update."),
187
215
  name: str = typer.Option(..., "--name", help="User-friendly name for the integration."),
188
- provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."),
216
+ provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP),
189
217
  model_name: str = typer.Option(..., "--model-name", help="Default model name for this integration."),
190
218
  api_key: str | None = typer.Option(None, "--api-key", help="New API key (omit to keep the stored key)."),
191
219
  base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."),
220
+ region: str | None = typer.Option(None, "--region", help=REGION_HELP),
221
+ account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP),
222
+ project: str | None = typer.Option(None, "--project", help=PROJECT_HELP),
192
223
  ) -> None:
193
224
  """Update an LLM provider integration."""
194
225
  from discolike_cli.main import get_client
@@ -196,7 +227,13 @@ def llm_update_command(
196
227
  request = build_request(
197
228
  LLMProviderUpdateRequest,
198
229
  {
199
- **_merge_params(None, integration_name=name, provider=provider, model_name=model_name, base_url=base_url),
230
+ **_merge_params(
231
+ None,
232
+ integration_name=name,
233
+ provider=provider,
234
+ model_name=_with_model_settings(model_name, region=region, account=account, project=project),
235
+ base_url=base_url,
236
+ ),
200
237
  "api_key": api_key,
201
238
  },
202
239
  )
@@ -233,10 +270,13 @@ def llm_set_default_command(
233
270
  def llm_test_connection_command(
234
271
  ctx: typer.Context,
235
272
  name: str = typer.Option("cli-test", "--name", help="Name to label the test configuration."),
236
- provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."),
273
+ provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP),
237
274
  api_key: str = typer.Option(..., "--api-key", help="API key for the provider."),
238
275
  model_name: str = typer.Option(..., "--model-name", help="Model name to test against."),
239
276
  base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."),
277
+ region: str | None = typer.Option(None, "--region", help=REGION_HELP),
278
+ account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP),
279
+ project: str | None = typer.Option(None, "--project", help=PROJECT_HELP),
240
280
  ) -> None:
241
281
  """Test a provider configuration before saving it."""
242
282
  from discolike_cli.main import get_client
@@ -244,7 +284,12 @@ def llm_test_connection_command(
244
284
  request = build_request(
245
285
  LLMProviderCreateRequest,
246
286
  _merge_params(
247
- None, integration_name=name, provider=provider, api_key=api_key, model_name=model_name, base_url=base_url
287
+ None,
288
+ integration_name=name,
289
+ provider=provider,
290
+ api_key=api_key,
291
+ model_name=_with_model_settings(model_name, region=region, account=account, project=project),
292
+ base_url=base_url,
248
293
  ),
249
294
  )
250
295
  emit(get_client(ctx).llm_providers.test_connection(request))
@@ -280,7 +280,7 @@ def test_bulk_companies_takes_params_file_and_drops_managed_keys(
280
280
  install_build_client: Callable[[Handler], None], tmp_path: Path
281
281
  ) -> None:
282
282
  form = tmp_path / "form.json"
283
- form.write_text(json.dumps({"country": ["US"], "variance": "MID_HIGH", "max_records": 5, "offset": 99}))
283
+ form.write_text(json.dumps({"country": ["US"], "relevance": "MAX_COVERAGE", "max_records": 5, "offset": 99}))
284
284
  recorder = Recorder(discover=[[]], queries_exclusion_list=[{"query_id": "q"}])
285
285
  install_build_client(recorder)
286
286
  result = runner.invoke(
@@ -292,6 +292,8 @@ def test_bulk_companies_takes_params_file_and_drops_managed_keys(
292
292
  str(form),
293
293
  "--param",
294
294
  "consensus=3",
295
+ "--relevance",
296
+ "BALANCED",
295
297
  "--variance",
296
298
  "LOW",
297
299
  "--out",
@@ -302,6 +304,7 @@ def test_bulk_companies_takes_params_file_and_drops_managed_keys(
302
304
  assert result.exit_code == 0, result.output
303
305
  params = recorder.params("/v1/discover")[0]
304
306
  assert params.get_list("country") == ["US"]
307
+ assert params.get("relevance") == "BALANCED"
305
308
  assert params.get("variance") == "LOW"
306
309
  assert params.get("consensus") == "3"
307
310
  assert params.get("max_records") == "10000"
@@ -316,7 +319,7 @@ def test_bulk_companies_validates_before_first_call(
316
319
  recorder = Recorder(discover=[[]])
317
320
  install_build_client(recorder)
318
321
  result = runner.invoke(
319
- app, ["bulk", "companies", "--variance", "BOGUS", "--out", str(tmp_path / "c.csv"), *NO_LIMIT]
322
+ app, ["bulk", "companies", "--relevance", "BOGUS", "--out", str(tmp_path / "c.csv"), *NO_LIMIT]
320
323
  )
321
324
  assert result.exit_code == 2
322
325
  assert "ValidationError" in result.output
@@ -314,7 +314,10 @@ def test_discogen_models_hits_models_endpoint(install_build_client: Callable[[Ha
314
314
  install_build_client(handler)
315
315
  result = runner.invoke(app, ["discogen", "models"])
316
316
  assert result.exit_code == 0, result.output
317
- assert json.loads(result.stdout) == {"models": {"openai": [{"name": "gpt-5.4", "supports_web_search": False}]}}
317
+ assert json.loads(result.stdout) == {
318
+ "models": {"openai": [{"name": "gpt-5.4", "supports_web_search": False}]},
319
+ "provider_regions": {},
320
+ }
318
321
 
319
322
 
320
323
  def test_discogen_status_default_family_discogen(install_build_client: Callable[[Handler], None]) -> None:
@@ -146,6 +146,8 @@ def test_discover_forwards_every_new_flag(install_build_client: Callable[[Handle
146
146
  "2020-01-01",
147
147
  "--min-similarity",
148
148
  "40",
149
+ "--relevance",
150
+ "MAX_RELEVANCE",
149
151
  "--variance",
150
152
  "MEDIUM",
151
153
  "--consensus",
@@ -171,6 +173,7 @@ def test_discover_forwards_every_new_flag(install_build_client: Callable[[Handle
171
173
  assert params.get_list("negate_social") == ["tiktok"]
172
174
  assert params.get("start_date") == "2020-01-01"
173
175
  assert params.get("min_similarity") == "40"
176
+ assert params.get("relevance") == "MAX_RELEVANCE"
174
177
  assert params.get("variance") == "MEDIUM"
175
178
  assert params.get("consensus") == "3"
176
179
  assert params.get("redirect") == "true"
@@ -183,9 +186,9 @@ def test_discover_forwards_every_new_flag(install_build_client: Callable[[Handle
183
186
  assert params.get_list("inclusion_query_id") == ["q1"]
184
187
 
185
188
 
186
- def test_discover_rejects_invalid_variance(install_build_client: Callable[[Handler], None]) -> None:
189
+ def test_discover_rejects_invalid_relevance(install_build_client: Callable[[Handler], None]) -> None:
187
190
  install_build_client(_discover_ok)
188
- result = runner.invoke(app, ["discover", "--variance", "BOGUS"])
191
+ result = runner.invoke(app, ["discover", "--relevance", "BOGUS"])
189
192
  assert result.exit_code == 2
190
193
 
191
194
 
@@ -120,17 +120,17 @@ def test_discover_params_file_under_param_under_flags(
120
120
  install_build_client: Callable[[Handler], None], tmp_path: Path
121
121
  ) -> None:
122
122
  form = tmp_path / "form.json"
123
- form.write_text(json.dumps({"country": ["US"], "variance": "LOW", "min_similarity": 10, "consensus": 3}))
123
+ form.write_text(json.dumps({"country": ["US"], "relevance": "MAX_COVERAGE", "min_similarity": 10, "consensus": 3}))
124
124
  captured: dict[str, Any] = {}
125
125
  install_build_client(_capture_json(captured, []))
126
126
  result = runner.invoke(
127
127
  app,
128
- ["discover", "--params-file", str(form), "--param", "min_similarity=50", "--variance", "HIGH"],
128
+ ["discover", "--params-file", str(form), "--param", "min_similarity=50", "--relevance", "BALANCED"],
129
129
  )
130
130
  assert result.exit_code == 0, result.output
131
131
  params = captured["params"]
132
132
  assert params.get_list("country") == ["US"]
133
- assert params.get("variance") == "HIGH"
133
+ assert params.get("relevance") == "BALANCED"
134
134
  assert params.get("min_similarity") == "50"
135
135
  assert params.get("consensus") == "3"
136
136
 
@@ -139,7 +139,7 @@ def test_discover_params_file_is_validated_locally(
139
139
  install_build_client: Callable[[Handler], None], tmp_path: Path
140
140
  ) -> None:
141
141
  form = tmp_path / "form.json"
142
- form.write_text(json.dumps({"variance": "BOGUS"}))
142
+ form.write_text(json.dumps({"relevance": "BOGUS"}))
143
143
  calls: list[str] = []
144
144
 
145
145
  def handler(request: httpx2.Request) -> httpx2.Response:
@@ -11,6 +11,7 @@ import typer
11
11
  from discolike import APIConnectionError
12
12
  from discolike import AuthenticationError
13
13
  from discolike import DiscolikeError
14
+ from discolike import InsufficientBalanceError
14
15
  from discolike import JobFailedError
15
16
  from discolike import JobTimeoutError
16
17
  from discolike import NotFoundError
@@ -154,6 +155,7 @@ def test_emit_table_flag_falls_back_to_json_when_no_scalar_columns(capsys: pytes
154
155
  (ValidationError("bad input"), 2),
155
156
  (AuthenticationError("no key"), 3),
156
157
  (RateLimitError("slow down", retry_after=3), 4),
158
+ (InsufficientBalanceError("too costly", max_affordable_records=10), 3),
157
159
  (ServerError("boom"), 1),
158
160
  ],
159
161
  )
@@ -186,6 +188,18 @@ def test_fail_rate_limit_includes_retry_after(capsys: pytest.CaptureFixture[str]
186
188
  assert payload["retry_after"] == 3.5
187
189
 
188
190
 
191
+ def test_fail_insufficient_balance_includes_max_affordable_records(capsys: pytest.CaptureFixture[str]) -> None:
192
+ exc = InsufficientBalanceError(
193
+ "too costly", status_code=403, max_affordable_records=60, remaining_usd=3.0, requested_records=250
194
+ )
195
+ result = fail(exc)
196
+ assert result.exit_code == 3
197
+ payload = json.loads(capsys.readouterr().err)
198
+ assert payload["code"] == "insufficient_balance"
199
+ assert payload["max_affordable_records"] == 60
200
+ assert payload["remaining_usd"] == 3.0
201
+
202
+
189
203
  def test_fail_rate_limit_without_retry_after_omits_key(capsys: pytest.CaptureFixture[str]) -> None:
190
204
  exc = RateLimitError("slow down", status_code=429, retry_after=None)
191
205
  fail(exc)
@@ -138,6 +138,71 @@ def test_llm_providers_create_posts_json(install_build_client: Callable[[Handler
138
138
  }
139
139
 
140
140
 
141
+ def test_llm_providers_create_appends_provider_settings_to_model_name(
142
+ install_build_client: Callable[[Handler], None],
143
+ ) -> None:
144
+ captured: dict[str, dict[str, str]] = {}
145
+
146
+ def handler(request: httpx2.Request) -> httpx2.Response:
147
+ captured["body"] = json.loads(request.content)
148
+ return httpx2.Response(200, json={"message": "created", "integration_id": "llm1"})
149
+
150
+ install_build_client(handler)
151
+ result = runner.invoke(
152
+ app,
153
+ [
154
+ "llm-providers",
155
+ "create",
156
+ "--name",
157
+ "watsonx",
158
+ "--provider",
159
+ "watsonx",
160
+ "--api-key",
161
+ "ibm-key",
162
+ "--model-name",
163
+ "watsonx/ibm/granite-3-8b-instruct",
164
+ "--region",
165
+ "eu-de",
166
+ "--project",
167
+ "11111111-2222-3333-4444-555555555555",
168
+ ],
169
+ )
170
+ assert result.exit_code == 0, result.output
171
+ assert captured["body"]["model_name"] == (
172
+ "watsonx/ibm/granite-3-8b-instruct#region=eu-de&project=11111111-2222-3333-4444-555555555555"
173
+ )
174
+
175
+
176
+ def test_llm_providers_update_flags_override_an_existing_settings_suffix(
177
+ install_build_client: Callable[[Handler], None],
178
+ ) -> None:
179
+ captured: dict[str, dict[str, str]] = {}
180
+
181
+ def handler(request: httpx2.Request) -> httpx2.Response:
182
+ captured["body"] = json.loads(request.content)
183
+ return httpx2.Response(200, json={"message": "updated"})
184
+
185
+ install_build_client(handler)
186
+ result = runner.invoke(
187
+ app,
188
+ [
189
+ "llm-providers",
190
+ "update",
191
+ "llm1",
192
+ "--name",
193
+ "watsonx",
194
+ "--provider",
195
+ "watsonx",
196
+ "--model-name",
197
+ "watsonx/ibm/granite-3-8b-instruct#region=us-south&project=p1",
198
+ "--region",
199
+ "eu-de",
200
+ ],
201
+ )
202
+ assert result.exit_code == 0, result.output
203
+ assert captured["body"]["model_name"] == "watsonx/ibm/granite-3-8b-instruct#region=eu-de&project=p1"
204
+
205
+
141
206
  def test_llm_providers_update_keeps_null_api_key(install_build_client: Callable[[Handler], None]) -> None:
142
207
  captured: dict[str, object] = {}
143
208
 
File without changes
File without changes
File without changes