diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e7864d..bd8312a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 (`-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 `.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. diff --git a/packages/discolike-cli/src/discolike_cli/contacts.py b/packages/discolike-cli/src/discolike_cli/contacts.py index 60396fb..be81c1e 100644 --- a/packages/discolike-cli/src/discolike_cli/contacts.py +++ b/packages/discolike-cli/src/discolike_cli/contacts.py @@ -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), @@ -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) diff --git a/packages/discolike-cli/tests/test_contacts_cli.py b/packages/discolike-cli/tests/test_contacts_cli.py index d835bb8..002ede3 100644 --- a/packages/discolike-cli/tests/test_contacts_cli.py +++ b/packages/discolike-cli/tests/test_contacts_cli.py @@ -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} diff --git a/packages/discolike/src/discolike/_generated/requests.py b/packages/discolike/src/discolike/_generated/requests.py index 6959cf1..9f3b048 100644 --- a/packages/discolike/src/discolike/_generated/requests.py +++ b/packages/discolike/src/discolike/_generated/requests.py @@ -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):