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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
- CLI: `--params-file PATH` on `discover`, `count`, `contacts discover|search|count` — a JSON object of API parameter names (an app form copied over), validated locally with the SDK request model before the call. Precedence: file < `--param` < flags.
- CLI: `discover --exclude-domains-file PATH` merges a file into the inline `exclude_domain` list (100 max, checked locally).
- CLI: `discolike bulk companies|estimate|contacts` — the volume pipeline as plain CLI calls. `companies` loops `discover` at up to 10,000 per page, saves each page as an exclusion list (`<run-name>-round-N`) for the next page, appends to `--out` as pages land and resumes from that CSV on rerun (warns at the 250,000-domain exclusion capacity). `estimate` sums the free `contacts count` over 1,000-domain slices and reports the upper bound at `--per-company`. `contacts` slices the domain list at `10000 / per-company` per `contacts discover` call (`results_by_company`, no `offset`), flattens to one row per contact, checkpoints finished slices in `<out>.checkpoint` (stamped with the domains, `--per-company` and filters it was written for, so a rerun with different inputs is refused instead of silently skipped) and skips them on rerun. All three validate the request locally before the first billable call, keep one call in flight under `--rate-limit`, retry on 429/5xx, and print a JSON summary on stdout.
- SDK: `ContactGenerateRequest.find_emails` (default `False`) asks the platform to run the email finder over every named, email-less row before the ContaGen job completes, filling `email` and `email_status` on those rows. Found addresses bill under the finder's rules; the rest of the job stays unbilled.
- CLI: `discolike contacts generate --find-emails` sets the same flag, so a generate run can return name + title + email triples without chaining `email find-batch` afterwards.
>>>>>>> dffdd27 (feat: find_emails on ContactGenerateRequest and --find-emails on contacts generate)
- SDK: OAuth refreshes now resend the RFC 8707 `resource` the token was issued for. `OAuthCredential` gains an optional `resource` field, filled in by `exchange_code` and persisted to the config file; credentials stored by earlier releases load with `resource=None` and refresh as before until the next `discolike auth login`. Without it, an authorization server configured with a default resource could re-bind a refreshed REST token to another audience.
- SDK (behavior change, no code change): company `address.state` now comes back from the API as the subdivision name ("California", "Tokyo") instead of the ISO code ("CA", "13"). `CompanyAddress.state` is still `str | None` and needs no migration, but anything joining or grouping on that value as a code has to resolve it. The contact's own `state` is unchanged and stays a code.
- SDK: state filters accept a code or a name, resolved server-side against the countries you selected. Discover/count still take one `country` value, but that value may be a region alias (`EU`, `APAC`, `DACH`) and the state resolves against every member. Contacts state filters accept multiple countries and drop a value they cannot resolve rather than erroring.
Expand Down
6 changes: 6 additions & 0 deletions packages/discolike-cli/src/discolike_cli/contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,11 @@ def generate_command(
max_company_records: int | None = typer.Option(
None, "--max-company-records", help="Maximum company records to process."
),
find_emails: bool = typer.Option(
False,
"--find-emails",
help="Run the email finder over named, email-less rows before the job completes (found addresses bill).",
),
wait: bool = typer.Option(False, "--wait", help=WAIT_HELP),
timeout: float = typer.Option(DEFAULT_WAIT_TIMEOUT_SECONDS, "--timeout", help=TIMEOUT_HELP),
fmt: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
Expand All @@ -483,6 +488,7 @@ def generate_command(
search_context_size=search_context_size,
max_contacts_per_domain=max_contacts_per_domain,
max_company_records=max_company_records,
find_emails=find_emails or None,
),
)
run_job(get_client(ctx).contacts.generate(request), wait=wait, timeout=timeout, fmt=fmt)
16 changes: 16 additions & 0 deletions packages/discolike-cli/tests/test_contacts_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,19 @@ def handler(request: httpx2.Request) -> httpx2.Response:
"full_domains": ["done.com"],
"partial_domains": ["half.com"],
}


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

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

install_build_client(handler)
result = runner.invoke(
app,
["contacts", "generate", "--icp-text", "VPs of Marketing", "--domain", "acme.com", "--find-emails"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {"icp_text": "VPs of Marketing", "domains": ["acme.com"], "find_emails": True}
7 changes: 7 additions & 0 deletions packages/discolike/src/discolike/_generated/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,13 @@ class ContactGenerateRequest(DiscolikeRequest):
full_domains: Annotated[list[str] | None, Field(title="Full Domains")] = None
partial_domains: Annotated[list[str] | None, Field(title="Partial Domains")] = None
initial_contact_counts: Annotated[dict[str, int] | None, Field(title="Initial Contact Counts")] = None
find_emails: Annotated[
bool | None,
Field(
description="After discovery, run every contact that has a name but no email through the email finder (same as the app's Verify step) and fill `email` / `email_status` on the row before the task completes. Found addresses are billed under the finder's own rules; nothing else is.",
title="Find Emails",
),
] = False


class DiscoGenProcessRequest(DiscolikeRequest):
Expand Down
Loading