-
Notifications
You must be signed in to change notification settings - Fork 0
Email finder: CLI command group, known_pattern, contract-checked routes, examples folder #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ad905bb
feat(cli): add the discolike email command group
354b2cb
feat(sdk): known_pattern on email.find; email routes join the contrac…
7d8e3f5
docs: add runnable examples folder
b91a895
fix(examples): cap bulk-match chunks by query count; keep failed emai…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Examples | ||
|
|
||
| Runnable, self-contained scripts showing how to use the DiscoLike Python SDK for common GTM workflows: matching a messy CRM export to DiscoLike contacts, finding verified work emails in bulk, and discovering plus AI-enriching target accounts. Each script is stdlib-plus-SDK only, has an argparse CLI, and is meant to be copied into your own pipeline and adapted. | ||
|
|
||
| | Script | What it does | | ||
| |---|---| | ||
| | [`match_crm_contacts.py`](match_crm_contacts.py) | Match a CSV of CRM contacts to DiscoLike persona IDs via `contacts.bulk_match()`, with dual domain keys (website + email domain), resumable JSONL checkpointing, and a persona_id + match_score output CSV | | ||
| | [`find_emails_from_csv.py`](find_emails_from_csv.py) | Find work emails for a CSV of people (first name, last name, domain) via `email.find_batch()` in chunks of 500; only status "found" bills | | ||
| | [`discover_and_enrich.py`](discover_and_enrich.py) | Discover companies matching an ICP with `client.discover()`, then run a DiscoGen research prompt over them with `discogen.process()` and `job.wait()` | | ||
|
|
||
| ## Running | ||
|
|
||
| ```bash | ||
| pip install discolike | ||
| export DISCOLIKE_API_KEY="dl_..." # create one at https://app.discolike.com/account/management/keys | ||
| python examples/<script>.py --help | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """Discover companies matching an ICP, then enrich them with DiscoGen. | ||
|
|
||
| Two calls end to end: | ||
|
|
||
| 1. ``client.discover(icp_text=..., country=..., max_records=...)`` finds | ||
| lookalike companies from DiscoLike's index of 80M+ business websites. | ||
| 2. ``client.discogen.process(query=..., domains=[...], web_search=True)`` | ||
| runs an AI research prompt over the discovered domains and returns one | ||
| structured answer per company. ``job.wait()`` blocks until it finishes. | ||
|
|
||
| DiscoGen runs on your own LLM provider key (BYOK) - configure one first via | ||
| ``client.llm_providers`` or in the app under Settings -> Integrations. | ||
|
|
||
| Usage: | ||
| export DISCOLIKE_API_KEY="dl_..." | ||
| python examples/discover_and_enrich.py \ | ||
| --icp "Cybersecurity for SMBs, managed IT services" \ | ||
| --country US \ | ||
| --query "What is their pricing model, and do they sell to MSPs?" | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import json | ||
| import sys | ||
|
|
||
| from discolike import Discolike | ||
|
|
||
|
|
||
| def parse_args() -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | ||
| parser.add_argument("--icp", required=True, help="ICP description, e.g. 'B2B SaaS for logistics'") | ||
| parser.add_argument("--country", action="append", help="ISO-2 country filter, repeatable (e.g. --country US)") | ||
| parser.add_argument("--max-records", type=int, default=10, help="How many companies to discover (default: 10)") | ||
| parser.add_argument("--query", required=True, help="DiscoGen research prompt to run over each company") | ||
| parser.add_argument("--timeout", type=float, default=1800.0, help="DiscoGen wait timeout in seconds") | ||
| return parser.parse_args() | ||
|
|
||
|
|
||
| def main() -> None: | ||
| args = parse_args() | ||
| client = Discolike() | ||
|
|
||
| print(f"Discovering up to {args.max_records} companies for: {args.icp!r}") | ||
| companies = client.discover(icp_text=args.icp, country=args.country, max_records=args.max_records) | ||
| if not companies: | ||
| sys.exit("No companies found for that ICP - try broadening it.") | ||
| domains = [company.domain for company in companies if company.domain] | ||
| for company in companies: | ||
| print(f" {company.domain} {company.name or ''} (similarity {company.similarity})") | ||
|
|
||
| print(f"\nRunning DiscoGen over {len(domains)} domains: {args.query!r}") | ||
| job = client.discogen.process(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}%")) | ||
|
|
||
| print("\nEnriched results:") | ||
| for row in status.results or []: | ||
| print(json.dumps(row, ensure_ascii=False, default=str)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| """Find work email addresses for a CSV of people via the email finder batch API. | ||
|
|
||
| Reads people from a CSV with ``first_name``, ``last_name``, and ``domain`` | ||
| columns (names configurable), submits them with | ||
| ``client.email.find_batch(contacts=[...])`` in chunks of up to 500, waits for | ||
| each batch with ``batch.results()``, and writes the found emails plus status | ||
| to an output CSV. | ||
|
|
||
| Billing note: only results with status "found" (an SMTP-verified address) are | ||
| billed. Catch-all domains and pattern-based guesses are returned for free. | ||
|
|
||
| Usage: | ||
| export DISCOLIKE_API_KEY="dl_..." | ||
| python examples/find_emails_from_csv.py people.csv --output emails.csv | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import csv | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| from discolike import Discolike | ||
|
|
||
| MAX_CONTACTS_PER_BATCH = 500 | ||
|
|
||
| OUTPUT_FIELDS = ["first_name", "last_name", "domain", "email", "status", "is_catch_all", "error"] | ||
|
|
||
|
|
||
| def parse_args() -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | ||
| parser.add_argument("input", type=Path, help="Input CSV of people") | ||
| parser.add_argument("--output", type=Path, default=Path("emails.csv"), help="Output CSV path") | ||
| parser.add_argument("--first-name-column", default="first_name", help="First-name column (default: first_name)") | ||
| parser.add_argument("--last-name-column", default="last_name", help="Last-name column (default: last_name)") | ||
| parser.add_argument("--domain-column", default="domain", help="Company-domain column (default: domain)") | ||
| parser.add_argument("--timeout", type=float, default=1800.0, help="Per-batch wait timeout in seconds") | ||
| return parser.parse_args() | ||
|
|
||
|
|
||
| def load_contacts(path: Path, args: argparse.Namespace) -> list[dict[str, str]]: | ||
| contacts: list[dict[str, str]] = [] | ||
| with path.open(newline="", encoding="utf-8-sig") as handle: | ||
| for row in csv.DictReader(handle): | ||
| first = row.get(args.first_name_column, "").strip() | ||
| last = row.get(args.last_name_column, "").strip() | ||
| domain = row.get(args.domain_column, "").strip().lower().removeprefix("www.") | ||
| if first and last and domain: | ||
| contacts.append({"first_name": first, "last_name": last, "domain": domain}) | ||
| return contacts | ||
|
|
||
|
|
||
| def main() -> None: | ||
| args = parse_args() | ||
| contacts = load_contacts(args.input, args) | ||
| if not contacts: | ||
| sys.exit(f"No usable rows (first name + last name + domain) found in {args.input}") | ||
|
|
||
| client = Discolike() | ||
| found = 0 | ||
| with args.output.open("w", newline="", encoding="utf-8") as handle: | ||
| writer = csv.DictWriter(handle, fieldnames=OUTPUT_FIELDS) | ||
| writer.writeheader() | ||
| for start in range(0, len(contacts), MAX_CONTACTS_PER_BATCH): | ||
| 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(contacts=chunk) | ||
| results = batch.results(timeout=args.timeout) | ||
| for item in results.results: | ||
| output = item.result | ||
| if output is None: | ||
| # Failed jobs carry no EnumerationOutput (so no identity), | ||
| # but must not vanish from the output: keep the status and | ||
| # error so the failure is visible and countable. | ||
| writer.writerow( | ||
| { | ||
| "first_name": "", | ||
| "last_name": "", | ||
| "domain": "", | ||
| "email": "", | ||
| "status": item.status or "failed", | ||
| "is_catch_all": "", | ||
| "error": item.error or "", | ||
| } | ||
| ) | ||
| continue | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| match = getattr(output, "result", None) | ||
| email = match.email if match is not None else None | ||
| if output.status == "found": | ||
| found += 1 | ||
| writer.writerow( | ||
| { | ||
| "first_name": output.first_name, | ||
| "last_name": output.last_name, | ||
| "domain": output.domain, | ||
| "email": email or "", | ||
| "status": output.status or "", | ||
| "is_catch_all": output.is_catch_all, | ||
| "error": output.error or "", | ||
| } | ||
| ) | ||
| handle.flush() | ||
|
|
||
| print(f'\nDone: {found}/{len(contacts)} verified emails (status "found") -> {args.output}') | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a terminal batch item has
result=None, this branch writes empty first-name, last-name, and domain fields instead of correlating the failure with its submitted contact, causing an anonymous CSV row that users cannot correct or retry accurately.Prompt To Fix With AI