release: 0.2.0 — httpx2 migration, typed responses, email resource - #12
Merged
Conversation
Every release so far shipped wheels with no MIT license text. The only LICENSE sat at the repo root, outside either package root, and license-files was unset, so hatchling had nothing to collect. Copy it into each published package and declare it. discolike-cli was fully annotated but shipped without py.typed, so none of that was visible to consumers' type checkers.
/queries/save-results is present in the production spec, so the route no longer needs to opt out of the contract check. Verified: 46 routes checked, only the two undeployed /email/find routes still skipped.
3.15 is still in beta, so an upstream regression in httpx, pydantic or typer would otherwise red-light every PR for something we cannot fix. Non-blocking until 3.15.0 final, when it moves into the matrix proper and earns a classifier.
find (with --known-pattern), find-batch from CSV and/or repeatable --contact, results (find or verify batches), and job - each with --wait/--no-wait polling. 14 tests.
…t check known_pattern matches the platform's POST /email/find body and is omitted when unset. The platform now exposes the email find/poll routes in its OpenAPI spec, so the openapi=False stamps are gone and check_contract.py validates the routes and params like every other resource.
match_crm_contacts.py (bulk-match a CRM CSV to personas with resumable checkpointing and website+email domain keys), find_emails_from_csv.py, and discover_and_enrich.py, referenced from the README.
…l jobs in output Rows expand to up to two queries (website + email domain), so chunking by row count could send up to 1,000 queries against the 500-per-call limit. Chunks are now packed by expanded query count, rows never split across calls. In find_emails_from_csv, terminal jobs with no result carried only item.error and vanished from the CSV; they now write a row with their status and error.
Email finder: CLI command group, known_pattern, contract-checked routes, examples folder
Every command routes its client through get_client(ctx), which reads the
global options the root callback stashes on ctx.obj — except auth login
and auth status, which took no ctx and built a client directly.
The visible damage was auth status reporting {"valid": true} against a
host it never contacted:
$ discolike --base-url http://127.0.0.1:9 auth status
{"source": "env", "api_key": "…isco", "valid": true}
while the same override on any other command correctly refused to
connect. --api-key was ignored too, so there was no way to verify a key
before auth login wrote it to disk.
auth status gains a third source value, "option", distinguishing a key
passed on the command line from one inherited via DISCOLIKE_API_KEY.
That distinction has to come from click's parameter source, since the
global --api-key is env-bound and the value alone cannot tell them apart.
The same distinction keeps auth login's prompt intact: an ambient
DISCOLIKE_API_KEY must not silently become the saved credential, or
anyone holding a production key in their environment would persist the
wrong one while trying to store another. Only an explicit flag skips the
prompt.
fix(cli): honor global --base-url and --api-key in auth commands
…d kind email.job() hardcoded kind="find", so a verify job could never be rehydrated through it — its ValidationOutput parsed into the wrong model. The union member was chosen solely from the handle's kind, so a batch reattached with the wrong kind silently mis-parsed every result. Results now prefer the kind the server reports per job (additive platform field), falling back to the handle's kind. wait() checks the result against the expected model for the job's kind and fails loudly otherwise.
Both returned a bare DiscolikeModel with every field in .extra — the only two untyped responses left in the SDK. count now returns Count, discover a ContactsDiscoverResponse mirroring the platform's response: a map of domain -> ContactsByCompany plus total_contacts/total_domains. ContactsByCompany was defined but never constructed; it now extends CompanyProfile and serves as the per-domain entry, matching the platform's DomainContactsEntry (CompanyResult + contacts + email_pattern fields).
discover, count, validate_icp, append, and segment forwarded **kwargs to typed methods on private resources — the typed signatures existed but were unreachable, so the five most-used entry points had no static checking or completion, and a misspelled keyword surfaced only at runtime (or reached the API as an unknown query parameter). The forwarders now mirror the inner signatures exactly and forward each parameter by keyword; wire behavior is unchanged since inner methods drop None values before sending.
Long-running calls (large append uploads, slow discover queries) had no way to exceed the client-wide timeout without constructing a second client. with_options returns a view sharing the parent's connection pool whose transport passes the override per request; the base client is untouched.
Transport clones from with_timeout mark themselves as views; close()/ aclose() on a view is a no-op so the parent client (and sibling views) keep working after a view is closed or exits its context manager.
Same shape as the platform-side mapping: a match over EmailKind with assert_never, so a future third kind fails type checking until decode and wait handle it, instead of silently decoding as ValidationOutput. typing-extensions promoted to an explicit dependency (assert_never on Python 3.10; it was already pulled in transitively by pydantic).
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md # packages/discolike/src/discolike/_client.py
httpx upstream is low-activity; Pydantic maintains httpx2 as its continuation and ships security fixes for it. This library sits in the critical path of user requests, so it follows the maintained fork. Breaking: httpx types are part of the public surface (http_client=, with_options(timeout=), the testkit Handler alias), so callers must swap `import httpx` for `import httpx2`. httpx2 also verifies TLS against the OS trust store via truststore rather than bundled certifi roots.
0.1.2 was written into the changelog and version files on 2026-08-19 but never tagged or published — PyPI still serves 0.1.1. Since no user ever saw a 0.1.2, its entries fold into the release that actually ships rather than shipping two versions on one day. 0.2.0 rather than 0.1.3: the unshipped section already dropped companies.metrics/history and changed four company endpoints to return lists, and the httpx2 move breaks the public client types on top of that. Also repoints the cross-package pins (discolike-cli -> discolike, the discolike[cli] extra), which referenced the 0.1.2 that never existed on the index and so could not resolve.
The CLI is not an independent consumer of the SDK — it is the SDK's command-line front end, released from this repo in lockstep and only ever tested against its own generation. A range pin invites combinations that never ran together in CI; an exact pin means the pair a user installs is the pair that was tested. Consequence: both packages ship on every release, including one whose own code did not change. Publishing them together also matters, since discolike[cli] is unsatisfiable in the window between the two uploads.
feat!: migrate to httpx2 and consolidate the unshipped 0.1.2 into 0.2.0
_decode_job_result lets the server's kind field override the handle's,
so a verify job rehydrated as the default kind="find" decodes into a
ValidationOutput. wait() then checked that result against the model for
self.kind, so the override it was meant to honor made a completed job
raise JobFailedError("email find job completed without a result").
The isinstance check conflated two things: whether a result arrived, and
whether it matched the kind the caller guessed. Only the first belongs
here — the second is what the server field is there to correct. Batch
results never had the check, which is why only the job path was affected.
Reported by Greptile on #12.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Release PR for 0.2.0 — 25 commits, everything merged into
developmentsince0.1.1.mainis behind by content only; its three extra commits are the merge commits from PRs #2, #3, and #4, so there is nothing onmainthis would revert.Why 0.2.0 and not 0.1.2
0.1.2was written into the changelog and version files on 2026-08-19 but never tagged or published — PyPI still serves0.1.1. Worse, both cross-package pins referenced that0.1.2, sopip install discolike-clifrom the index could not resolve at all. Since no user ever saw a0.1.2, its entries were folded into the release that actually ships. The minor bump is warranted on its own: this dropscompanies.metrics/history, changes four company endpoints to return lists, and swaps the HTTP client underneath the public API.Breaking changes
httpx → httpx2. The SDK now depends on
httpx2, Pydantic's maintained continuation of httpx, for timely security updates. httpx types are part of the public surface, so callers must update:import httpximport httpx2Client(http_client=httpx.Client(...))Client(http_client=httpx2.Client(...))client.with_options(timeout=httpx.Timeout(...))client.with_options(timeout=httpx2.Timeout(...))httpx2 also verifies TLS against the OS trust store via
truststorerather than bundledcertifiroots — no code change on our side, but hosts with an incomplete or custom system trust store may see verification failures thatcertifipreviously masked.Removed
companies.metrics/companies.historyand thecompany metrics/company historycommands; the underlying endpoints are deprecated with removal scheduled for 2026-10-01.Return types
companies.redirects,vendors,subsidiaries, andpublic_linksnow return alistof typed rows. These endpoints return a JSON array; the SDK was validating it into a single model and raisedValidationErroron every live call.Also in this release
Typed responses across
contacts.count,contacts.discover,extract,score,match, and the provider and model listings, replacing bare passthrough models. Newemailresource anddiscolike emailcommand group.client.with_options(timeout=...).queries.save_results. Typed signatures on the client-level forwarders. A runnableexamples/folder. Both wheels now ship the MIT license text, which was missing from every release so far.Full detail in
CHANGELOG.md.Testing
315 tests pass;
ruff checkandruff format --checkclean;tyreports only 3 pre-existingexamples/diagnostics. The suite runs entirely throughMockTransport, so the real wire path was verified separately: a live HTTPS request toapi.discolike.comcompletes handshake and round trip under httpx2's truststore-based verification.After merge
Tag
v0.2.0, then publish both packages together — they pin each other exactly, sodiscolike[cli]is unsatisfiable in the window between the two uploads.Greptile Summary
The release migrates the SDK and testkit to httpx2, expands typed response coverage, introduces email job and batch APIs with corresponding CLI commands, adds per-request timeout views, and packages the 0.2.0 release.
Confidence Score: 4/5
The PR should not merge until email wait() accepts results decoded according to the server-reported kind; the CRM CSV formula-injection concern is also worth hardening.
Server-kind decoding and handle-kind validation currently disagree, causing a completed find or verify job to raise JobFailedError when those kinds differ; the new CRM example also preserves formula-leading input cells in its spreadsheet-oriented output.
Files Needing Attention: packages/discolike/src/discolike/_email.py; examples/match_crm_contacts.py
Security Review
The new CRM CSV example writes untrusted input fields without neutralizing spreadsheet formula prefixes, creating a formula-injection risk when generated output is opened in a spreadsheet.
Important Files Changed
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "Merge pull request #11 from discolike/fe..." | Re-trigger Greptile