Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13', '3.14']
python-version: ['3.11', '3.12', '3.13', '3.14', '3.15']
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
Expand All @@ -34,7 +34,7 @@ jobs:
matrix:
include:
- resolution: highest
python-version: '3.14'
python-version: '3.15'
- resolution: lowest-direct
python-version: '3.11'
steps:
Expand All @@ -47,17 +47,3 @@ jobs:
- run: uv run --no-sync pytest packages/discolike-cli/tests -q
- run: uv build --all-packages -o dist
- run: uv run --no-project --isolated --resolution ${{ matrix.resolution }} --find-links dist --with dist/discolike_cli-*.whl discolike --version

# 3.15 is still prerelease; non-blocking until 3.15.0 final, when it moves
# into the matrix above and gets a classifier.
test-prerelease:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: '3.15'
- run: uv sync --all-packages
- run: uv run pytest packages/discolike/tests -q
- run: uv run pytest packages/discolike-cli/tests -q
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.6.2 (2026-10-21)

- SDK/CLI: Python 3.15 is supported and tested in CI.
- SDK/CLI: an LLM integration's `model_name` can carry provider settings after `#`, as URL query parameters: `region` for `bedrock` and `bedrock_mantle` (any AWS region, optional), `account` for `snowflake` (account identifier, with a programmatic access token as `api_key`) and `region` plus `project` for `watsonx`, e.g. `us.anthropic.claude-sonnet-4-5-20250929-v1:0#region=us-east-1`. Snowflake and watsonx become usable as providers. An unknown setting or region is a 400 (`ValidationError`). `DiscogenModels.provider_regions` lists the accepted regions per provider. The CLI's `llm-providers create`, `update` and `test-connection` gain `--region`, `--account` and `--project`, which build the suffix for you, and `discogen models` output gains a `provider_regions` key. It needs an API release that knows the settings; older SDKs can already send the suffix in `model_name`.
- SDK: `BulkContactMatchQueryItem` gains `linkedin`, a LinkedIn profile URL or username matched exactly before `email`, then name and company, so a list of profile URLs alone returns the matching contacts. It needs an API release that knows the field.
- SDK (note for maintainers): `discolike/_generated/requests.py` is regenerated from the dev spec, which adds `linkedin` and moves `api_key` after `base_url` on the LLM provider request models (keyword arguments, so no caller change).

## 0.6.1 (2026-10-19)

- SDK/CLI: discover's server-side default for `min_digital_footprint` drops from 50 to 25 when the field is omitted, so lists include more real companies with a thin web presence. Pass `min_digital_footprint=50` (CLI `--min-digital-footprint 50`) to keep the old floor. No SDK change; older versions get the new default too.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ uv run pytest packages/discolike/tests -q
uv run pytest packages/discolike-cli/tests -q
```

CI runs the same checks across Python 3.11–3.14.
CI runs the same checks across Python 3.11–3.15.

## Branches

Expand Down
2 changes: 1 addition & 1 deletion examples/discover_and_enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def main() -> None:

print(f"\nRunning DiscoGen over {len(domains)} domains: {args.query!r}")
job = client.discogen.process(DiscoGenProcessRequest(query=args.query, domains=domains, web_search=True))
status = job.wait(timeout=args.timeout, on_poll=lambda s: print(f" status={s.status} progress={s.progress}%"))
status = job.wait(max_wait=args.timeout, on_poll=lambda s: print(f" status={s.status} progress={s.progress}%"))

print("\nEnriched results:")
for row in status.results or []:
Expand Down
6 changes: 3 additions & 3 deletions examples/enrich_crm_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def enrich_row(client: Discolike, *, domain: str) -> dict[str, str]:
"dl_name": profile.name or "",
"dl_employees": profile.employees or "",
"dl_revenue_range": profile.revenue_range or "",
"dl_country": address.country if address else "",
"dl_state": address.state if address else "",
"dl_city": address.city if address else "",
"dl_country": (address.country if address else None) or "",
"dl_state": (address.state if address else None) or "",
"dl_city": (address.city if address else None) or "",
"dl_industry_groups": top_keys(profile.industry_groups),
"dl_business_model": top_keys(profile.business_model),
"dl_description": profile.description or "",
Expand Down
2 changes: 1 addition & 1 deletion examples/find_emails_from_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def main() -> None:
chunk = contacts[start : start + MAX_CONTACTS_PER_BATCH]
print(f"Submitting batch of {len(chunk)} contacts ({start + len(chunk)}/{len(contacts)})...")
batch = client.email.find_batch(FindEmailBatchRequest.model_validate({"requests": chunk}))
results = batch.results(timeout=args.timeout)
results = batch.results(max_wait=args.timeout)
for item in results.results:
output = item.result
if not isinstance(output, EnumerationOutput):
Expand Down
2 changes: 1 addition & 1 deletion examples/match_crm_contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def main() -> None:
job = client.contacts.bulk_match(
BulkContactMatchRequest.model_validate({"queries": queries, "limit": 1})
)
status = job.wait(timeout=args.timeout)
status = job.wait(max_wait=args.timeout)
except Exception as exc: # leave the chunk un-checkpointed so a rerun retries it
failed_chunks += 1
print(f" ! {label} failed ({exc}); rows left pending, rerun to retry")
Expand Down
5 changes: 3 additions & 2 deletions packages/discolike-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ build-backend = "hatchling.build"

[project]
name = "discolike-cli"
version = "0.6.1"
version = "0.6.2"
description = "Official CLI for the DiscoLike API"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.11"
authors = [{ name = "DiscoLike", email = "support@discolike.com" }]
dependencies = [
"discolike==0.6.1",
"discolike==0.6.2",
"typer>=0.26.1,<0.28",
"rich>=13.0",
]
Expand All @@ -25,6 +25,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Programming Language :: Python :: 3 :: Only",
"Operating System :: OS Independent",
"Environment :: Console",
Expand Down
57 changes: 51 additions & 6 deletions packages/discolike-cli/src/discolike_cli/providers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from urllib.parse import parse_qsl
from urllib.parse import urlencode

import typer

from discolike.requests import LLMProviderCreateRequest
Expand All @@ -15,6 +18,23 @@
search_providers_app = typer.Typer(help="Manage BYOK web search provider integrations (Tavily, Serper, etc.).")
llm_providers_app = typer.Typer(help="Manage BYOK LLM provider integrations (OpenAI, Anthropic, custom endpoints).")

LLM_PROVIDER_HELP = "LLM provider key, e.g. openai, anthropic, bedrock, snowflake, watsonx, or custom."
REGION_HELP = (
"Provider region: an AWS region for bedrock and bedrock_mantle, a watsonx region (us-south, eu-de, ...) "
"for watsonx. `discogen models` lists them under provider_regions."
)
ACCOUNT_HELP = "Snowflake account identifier, e.g. orgname-accountname (snowflake only)."
PROJECT_HELP = "watsonx project ID (watsonx only)."


def _with_model_settings(model_name: str, *, region: str | None, account: str | None, project: str | None) -> str:
"""The API reads provider settings from model_name after "#", e.g. "<model>#region=us-east-1"."""
flags = {key: value for key, value in (("region", region), ("account", account), ("project", project)) if value}
if not flags:
return model_name
base, _, existing = model_name.partition("#")
return f"{base}#{urlencode(dict(parse_qsl(existing)) | flags)}"


@search_providers_app.command("list")
@handle_errors
Expand Down Expand Up @@ -149,18 +169,26 @@ def llm_list_command(
def llm_create_command(
ctx: typer.Context,
name: str = typer.Option(..., "--name", help="User-friendly name for the integration."),
provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."),
provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP),
api_key: str = typer.Option(..., "--api-key", help="API key for the provider."),
model_name: str = typer.Option(..., "--model-name", help="Default model name for this integration."),
base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."),
region: str | None = typer.Option(None, "--region", help=REGION_HELP),
account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP),
project: str | None = typer.Option(None, "--project", help=PROJECT_HELP),
) -> None:
"""Create an LLM provider integration."""
from discolike_cli.main import get_client

request = build_request(
LLMProviderCreateRequest,
_merge_params(
None, integration_name=name, provider=provider, api_key=api_key, model_name=model_name, base_url=base_url
None,
integration_name=name,
provider=provider,
api_key=api_key,
model_name=_with_model_settings(model_name, region=region, account=account, project=project),
base_url=base_url,
),
)
emit(get_client(ctx).llm_providers.create(request))
Expand All @@ -185,18 +213,27 @@ def llm_update_command(
ctx: typer.Context,
integration_id: str = typer.Argument(..., help="ID of the LLM provider integration to update."),
name: str = typer.Option(..., "--name", help="User-friendly name for the integration."),
provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."),
provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP),
model_name: str = typer.Option(..., "--model-name", help="Default model name for this integration."),
api_key: str | None = typer.Option(None, "--api-key", help="New API key (omit to keep the stored key)."),
base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."),
region: str | None = typer.Option(None, "--region", help=REGION_HELP),
account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP),
project: str | None = typer.Option(None, "--project", help=PROJECT_HELP),
) -> None:
"""Update an LLM provider integration."""
from discolike_cli.main import get_client

request = build_request(
LLMProviderUpdateRequest,
{
**_merge_params(None, integration_name=name, provider=provider, model_name=model_name, base_url=base_url),
**_merge_params(
None,
integration_name=name,
provider=provider,
model_name=_with_model_settings(model_name, region=region, account=account, project=project),
base_url=base_url,
),
"api_key": api_key,
},
)
Expand Down Expand Up @@ -233,18 +270,26 @@ def llm_set_default_command(
def llm_test_connection_command(
ctx: typer.Context,
name: str = typer.Option("cli-test", "--name", help="Name to label the test configuration."),
provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."),
provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP),
api_key: str = typer.Option(..., "--api-key", help="API key for the provider."),
model_name: str = typer.Option(..., "--model-name", help="Model name to test against."),
base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."),
region: str | None = typer.Option(None, "--region", help=REGION_HELP),
account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP),
project: str | None = typer.Option(None, "--project", help=PROJECT_HELP),
) -> None:
"""Test a provider configuration before saving it."""
from discolike_cli.main import get_client

request = build_request(
LLMProviderCreateRequest,
_merge_params(
None, integration_name=name, provider=provider, api_key=api_key, model_name=model_name, base_url=base_url
None,
integration_name=name,
provider=provider,
api_key=api_key,
model_name=_with_model_settings(model_name, region=region, account=account, project=project),
base_url=base_url,
),
)
emit(get_client(ctx).llm_providers.test_connection(request))
5 changes: 4 additions & 1 deletion packages/discolike-cli/tests/test_discogen_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,10 @@ def handler(request: httpx2.Request) -> httpx2.Response:
install_build_client(handler)
result = runner.invoke(app, ["discogen", "models"])
assert result.exit_code == 0, result.output
assert json.loads(result.stdout) == {"models": {"openai": [{"name": "gpt-5.4", "supports_web_search": False}]}}
assert json.loads(result.stdout) == {
"models": {"openai": [{"name": "gpt-5.4", "supports_web_search": False}]},
"provider_regions": {},
}


def test_discogen_status_default_family_discogen(install_build_client: Callable[[Handler], None]) -> None:
Expand Down
65 changes: 65 additions & 0 deletions packages/discolike-cli/tests/test_providers_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,71 @@ def handler(request: httpx2.Request) -> httpx2.Response:
}


def test_llm_providers_create_appends_provider_settings_to_model_name(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, dict[str, str]] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"message": "created", "integration_id": "llm1"})

install_build_client(handler)
result = runner.invoke(
app,
[
"llm-providers",
"create",
"--name",
"watsonx",
"--provider",
"watsonx",
"--api-key",
"ibm-key",
"--model-name",
"watsonx/ibm/granite-3-8b-instruct",
"--region",
"eu-de",
"--project",
"11111111-2222-3333-4444-555555555555",
],
)
assert result.exit_code == 0, result.output
assert captured["body"]["model_name"] == (
"watsonx/ibm/granite-3-8b-instruct#region=eu-de&project=11111111-2222-3333-4444-555555555555"
)


def test_llm_providers_update_flags_override_an_existing_settings_suffix(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, dict[str, str]] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"message": "updated"})

install_build_client(handler)
result = runner.invoke(
app,
[
"llm-providers",
"update",
"llm1",
"--name",
"watsonx",
"--provider",
"watsonx",
"--model-name",
"watsonx/ibm/granite-3-8b-instruct#region=us-south&project=p1",
"--region",
"eu-de",
],
)
assert result.exit_code == 0, result.output
assert captured["body"]["model_name"] == "watsonx/ibm/granite-3-8b-instruct#region=eu-de&project=p1"


def test_llm_providers_update_keeps_null_api_key(install_build_client: Callable[[Handler], None]) -> None:
captured: dict[str, object] = {}

Expand Down
3 changes: 2 additions & 1 deletion packages/discolike/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Programming Language :: Python :: 3 :: Only",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]

[project.optional-dependencies]
cli = ["discolike-cli==0.6.1"]
cli = ["discolike-cli==0.6.2"]

[project.urls]
Homepage = "https://www.discolike.com"
Expand Down
Loading
Loading