Skip to content

feat: sync the SDK to the 2026-09-28 → 2026-10-15 API releases - #33

Merged
ibadus merged 6 commits into
mainfrom
nagoya/sdk-is_recent_hire-update
Oct 15, 2026
Merged

ibadus merged 6 commits into
mainfrom
nagoya/sdk-is_recent_hire-update

Conversation

@ibadus

@ibadus ibadus commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Brings the SDK level with the live spec (/openapi) and GET /changelog/. The last sync was 2026-09-22 and nine changelog entries had landed since. I re-audited every request and response key on all 20 JSON routes against a fresh spec: zero request differences.

Added (non-breaking)

  • is_recent_hire, include_active_jobs, include_past_jobs are keys of the people object on search.people, and keys of each cascade tier on search.waterfall_icp (so one cascade can try recent hires first and fall back to everyone). On company.tam_by_people only is_recent_hire is a key of people: the past-job flags are search.people-only, so they live on a new SearchPeopleFilter(PeopleFilter) and TamPeopleFilter does not offer them. search.people takes SearchPeopleFilter | PeopleFilter so an existing PeopleFilter variable keeps type-checking. search.employee_finder and both enrichment.company_distribution_by_* methods take is_recent_hire as a top-level argument. The API rejects unknown fields at any depth with a 422 (since 2026-09-28), so placement is load-bearing; tests pin it from both sides. Unset flags are omitted so server defaults apply, False is sent, and both include_*_jobs False is a 422 (documented, not pre-validated).
  • company.mx.provider on the shared CompanyFilter (MxFilter, MxProviderFilter), backed by a generated 66-value MxProvider enum (gen_enums.py --fetch; every existing enum byte-identical). MxProvider.ANY means every provider.
  • people.education.start_year / end_year via PeopleEducationFilter, which extends KeywordFilter.
  • mx on enrichment.company (company.mx) and enrichment.email (top level and each all_emails[] item), as list[MxRecord] | None. The changelog documents only the company one; the email one is in the spec only. Deliberately not a BlitzList: null ("no MX data yet") and [] ("takes no mail") are different answers, so coercing would turn "unknown" into "has no mail server".
  • found on both distribution responses; the country distribution now counts people, not jobs.
  • [brackets] exact-match documented on job.title and the jobs-side company HQ city / state. No schema change, and a test pins that brackets pass through verbatim.

Documented, not changed

  • Location and HQ.postcode / HQ.street are SDK-only keys the spec doesn't list. They are deliberate and kept; HQ.postcode / street were weighed for removal as a feat!: and kept, so this stays a minor release. docs/CONTEXT.md §7 and §10 now record why and list the expected SDK-only keys, so the next audit doesn't "fix" them.
  • No change needed for the 2026-09-28 422 body reshape: _make_status_error reads only message / error.

Test plan

  • ruff check / ruff format --check
  • mypy and pyright (strict)
  • pytest — 217 passed (186 before), sync and async
  • gen_enums.py --check and gen_sync.py --check
  • gen_enums.py --fetch against the live spec produces no extra diff
  • Full request + response audit of all 20 JSON routes against the republished spec and changelog: zero request differences; the only response differences are the documented deliberate extras
  • mypy and pyright both reject search.companies(is_recent_hire=True) and a flag passed as a top-level argument to search.people

Follow-up

  • blitz-api-js needs the same changes (a sdk-is_recent_hire-update workspace exists for it).

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no blocking issues remain.

Summary

Adds hiring filters, mail-provider filters, education-year filters, distribution found fields, and typed MX records. Since the last review, the changes add MX response fields and preserve the difference between unknown mail data and an empty record list.

  • People searches can choose which jobs count toward a match.
  • Company filters can match the mail provider used by a domain.
  • Enrichment results include MX records and a clear found status.
  • People searches can filter education by attendance and graduation years.
  • Job titles and company locations document bracketed exact matches.
  • The SDK's API audit notes explain which response fields it keeps.

Reviews (5) · Last reviewed commit: "feat: return the MX records on enrichmen..." · Reviewed by Greptile

Audited every request and response key on all 20 JSON routes against the live
spec (`/openapi`, plus `GET /changelog/` for the narrative); the SDK had last
synced on 2026-09-22 and was missing nine changelog entries.

- `is_recent_hire` is a top-level argument on `search.people`,
  `company.tam_by_people`, `search.employee_finder`, `search.waterfall_icp` and
  both `enrichment.company_distribution_by_*` methods. It is deliberately not a
  key of `PeopleFilter`/`TamPeopleFilter`: since 2026-09-28 the API rejects
  unknown fields at any depth with a 422.
- `include_active_jobs` / `include_past_jobs` on `search.people` and
  `search.waterfall_icp`. Unset flags are omitted so the server defaults apply;
  `False` is sent.
- `company.mx.provider` on the shared `CompanyFilter` (`MxFilter`,
  `MxProviderFilter`) with a generated 66-value `MxProvider` enum, mapped in
  `gen_enums.py` by its owning property `provider`. `MxProvider.ANY` is a
  sentinel meaning every provider, documented on `MxProviderFilter`.
- `people.education.start_year` / `end_year` via `PeopleEducationFilter`, which
  extends `KeywordFilter`.
- `found` on both distribution responses (`false`, at no cost, when the company
  is unknown or has no current employees); the country distribution now counts
  people, not jobs.
- Documented the `[brackets]` exact-match syntax on `job.title` and the
  jobs-side company HQ `city`/`state`. No schema change, so the filters stay
  `KeywordFilter`; a test pins that brackets pass through verbatim.

No change needed for the 2026-09-28 error-body reshape: `_make_status_error`
reads only `message`/`error`.

Tests 186 -> 210; mypy and pyright strict, both drift guards clean.
The re-audit against the live spec found SDK-only response keys. All of them
are deliberate, so say so where the next audit will look instead of leaving
them to be rediscovered and "fixed":

- Location is one superset model over three spec sites: a person's location
  has all six keys, Experience.job_location only city/state_code/country_code,
  and a job's location only city/country_code. The unlisted keys read None.
- HQ.postcode and HQ.street appear nowhere in the spec but are kept by
  maintainer decision. This reverses the earlier plan to drop them in the next
  major; removal would turn a silent None into an AttributeError.
- total_results on the two TAM builders comes from the shared CursorPage.

Adds the list of expected SDK-only keys to the audit playbook, and rewords the
Location and HQ docstrings, which still called the HQ fields "unverified".
Comment thread src/blitz_api/types/filters.py Outdated
Widening PeopleFilter.education from KeywordFilter to PeopleEducationFilter
(to add start_year / end_year) broke callers that reuse a typed variable:

    education: KeywordFilter = {"include": ["Stanford"]}
    people: PeopleFilter = {"education": education}

A KeywordFilter variable is not assignable to a TypedDict that adds optional
keys -- TypedDict assignability is structural, and those keys could hold any
type -- so both mypy and pyright rejected it. The field, and the TamPeopleFilter
that inherits it, is now `PeopleEducationFilter | KeywordFilter`. Dict literals
and PeopleEducationFilter variables are unaffected, and typos and wrong types
inside `education` are still rejected.

The new regression case lives in tests/test_filters.py as plain typed
variables, so mypy and pyright fail the suite if it regresses; confirmed it
fails on both before this change.

Found in review of #33.
…cade tiers

The API owner moved is_recent_hire, include_active_jobs and include_past_jobs:

- search.people: all three are now keys of `people`.
- company.tam_by_people: is_recent_hire is a key of `people`.
- search.waterfall_icp: all three are keys of each `cascade` tier, so one cascade
  can try recent hires first and fall back to everyone.
- search.employee_finder and both enrichment.company_distribution_by_* keep
  is_recent_hire as a top-level argument.

PeopleFilter (and TamPeopleFilter, which extends it) and CascadeTier gain the
keys; search.people, tam_by_people and waterfall_icp lose the top-level keyword
arguments. Since 2026-09-28 the API rejects unknown fields at any depth with a
422, so placement is load-bearing: tests pin it from both sides (the keys exist
on the filter types, and are not parameters of the methods that nest them), and
the auto-pagination test checks the flags in `people` survive onto page 2.

The flags have never been in a release, so this is not a breaking change against
4.0.0, and it supersedes the top-level placement described in the body of
200f4cc. The live spec still showed the previous placement when this landed;
docs/CONTEXT.md §12 records that the audit will flag it until republished.
Comment thread src/blitz_api/types/filters.py
The republished spec gives company.tam_by_people's `people` object is_recent_hire
but not include_active_jobs / include_past_jobs; only search.people takes those.
Because TamPeopleFilter extends PeopleFilter, putting them on PeopleFilter made
TamPeopleFilter offer two keys the endpoint rejects with a 422.

The two flags move to a new SearchPeopleFilter(PeopleFilter); PeopleFilter stays
the persona criteria both routes share (with is_recent_hire), and TamPeopleFilter
is unchanged. search.people takes `SearchPeopleFilter | PeopleFilter` rather than
the bare child, for the same reason as people.education in 5353a05: a declared
PeopleFilter variable is not assignable to a TypedDict that adds optional keys, so
narrowing the parameter would break every caller that reuses one. A call-site test
passes a PeopleFilter, a TamPeopleFilter and a SearchPeopleFilter variable to
search.people; mypy and pyright fail the suite if that regresses.

Re-audited all 20 routes against the republished spec and changelog: zero request
differences, and the only response differences are the documented deliberate extras.
Re-pulled the live spec and changelog (one new entry, 40 -> 41). The audit found
two response changes where the changelog names one:

- enrichment.company: `company.mx`, the MX records of the company's domain
  (documented in the 2026-10-15 changelog entry).
- enrichment.email: `mx` at the top level and on every `all_emails[]` item, all
  required and nullable. This one is in the spec only.

Adds MxRecord (`priority`, `host`) and `mx: list[MxRecord] | None` on Company,
EmailEnrichmentResponse and EmailMatch. Company is the shared superset model, so
`mx` reads None on search.companies and the TAM builders, whose schemas don't
list it.

Deliberately NOT a BlitzList: the API gives null ("no MX data yet" / "not
resolved") and [] ("the domain takes no mail") different meanings, and coercing
one into the other would silently turn "unknown" into "has no mail server". This
is the one exception to the rule that every nullable array is a BlitzList; it is
recorded on the alias, in CONTEXT.md section 5, and pinned by a test that keeps
null and [] distinct.

Fixtures use the spec's own example (priority 1, aspmx.l.google.com) and leave
the shared _COMPANY fixture mx-free. Re-audited all 20 routes: zero request
differences, and the only response differences are the documented deliberate
extras (now including Company.mx at the three non-enrichment company sites).
@ibadus
ibadus merged commit 260320b into main Oct 15, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant