blitz-api-py 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/CHANGELOG.md +19 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/PKG-INFO +104 -7
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/README.md +103 -6
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_client_async.py +23 -2
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_client_sync.py +23 -2
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_rate_limit_async.py +6 -4
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_rate_limit_sync.py +6 -4
- blitz_api_py-0.5.0/src/blitz_api/_version.py +1 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/utils.py +21 -1
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/utils.py +21 -1
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/__init__.py +4 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/utils.py +20 -0
- blitz_api_py-0.3.0/src/blitz_api/_version.py +0 -1
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/.gitignore +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/LICENSE +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/pyproject.toml +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/__init__.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_base_client.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_client.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_compat.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_constants.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_exceptions.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_pagination_async.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_pagination_base.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_pagination_sync.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_rate_limit.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/py.typed +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/__init__.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/__init__.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/account.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/enrichment.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/search.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/__init__.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/account.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/enrichment.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/search.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/_models.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/account.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/enrichment.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/enums.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/filters.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/search.py +0 -0
- {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/shared.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/api-blitz/blitz-api-py/compare/v0.4.0...v0.5.0) (2026-06-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* scope client-side rate limiting per endpoint ([#13](https://github.com/api-blitz/blitz-api-py/issues/13)) ([de5308b](https://github.com/api-blitz/blitz-api-py/commit/de5308b6995edc68dd1e43a8554296b7de095df2))
|
|
9
|
+
|
|
10
|
+
## [0.4.0](https://github.com/api-blitz/blitz-api-py/compare/v0.3.0...v0.4.0) (2026-06-17)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add company department distribution endpoint and response models ([b64a6d9](https://github.com/api-blitz/blitz-api-py/commit/b64a6d9bada069206c77ee11c9f86fae01ef6baa))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* port JS SDK README upgrade (badges, billing note, TOC, example) ([#11](https://github.com/api-blitz/blitz-api-py/issues/11)) ([2276513](https://github.com/api-blitz/blitz-api-py/commit/2276513cdae4c23d4a1b50f59366e7156bf81019))
|
|
21
|
+
|
|
3
22
|
## [0.3.0](https://github.com/api-blitz/blitz-api-py/compare/v0.2.0...v0.3.0) (2026-06-04)
|
|
4
23
|
|
|
5
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blitz-api-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Typed Python SDK for the Blitz API — B2B data, search, and enrichment.
|
|
5
5
|
Project-URL: Homepage, https://blitz-api.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.blitz-api.ai
|
|
@@ -31,6 +31,11 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
|
|
32
32
|
# blitz-api-py
|
|
33
33
|
|
|
34
|
+
[](https://pypi.org/project/blitz-api-py/)
|
|
35
|
+
[](https://pypi.org/project/blitz-api-py/)
|
|
36
|
+
[](https://github.com/api-blitz/blitz-api-py/actions/workflows/ci.yml)
|
|
37
|
+
[](./LICENSE)
|
|
38
|
+
|
|
34
39
|
The typed Python SDK for the [Blitz API](https://blitz-api.ai) — B2B data, search,
|
|
35
40
|
and enrichment.
|
|
36
41
|
|
|
@@ -41,14 +46,35 @@ and enrichment.
|
|
|
41
46
|
- **Resilient** — built-in client-side rate limiting, retries with backoff on
|
|
42
47
|
`429`/`5xx`, and a typed exception hierarchy.
|
|
43
48
|
- **Forward-compatible** — new fields the API adds never break deserialization.
|
|
49
|
+
- **1:1 with the API** — request filters and response fields are snake_case,
|
|
50
|
+
matching [docs.blitz-api.ai](https://docs.blitz-api.ai).
|
|
44
51
|
|
|
45
52
|
> Create and manage API keys at [app.blitz-api.ai](https://app.blitz-api.ai).
|
|
46
53
|
|
|
54
|
+
> **Billing.** Blitz bills **per result**. A bare `for person in client.search.people(...)`
|
|
55
|
+
> loop streams every match up to the server-side limit (people: 50k results), which can be
|
|
56
|
+
> a lot of credits. Bound spend with **`max_items`** (a client-side total cap on
|
|
57
|
+
> `.collect()` / `.auto_paging_iter()`, never sent on the wire) — details in
|
|
58
|
+
> [Pagination](#pagination).
|
|
59
|
+
|
|
60
|
+
## Contents
|
|
61
|
+
|
|
62
|
+
- [Installation](#installation)
|
|
63
|
+
- [Quickstart](#quickstart)
|
|
64
|
+
- [Example: find, enrich, collect](#example-find-enrich-collect)
|
|
65
|
+
- [Authentication](#authentication)
|
|
66
|
+
- [Endpoints](#endpoints)
|
|
67
|
+
- [Pagination](#pagination)
|
|
68
|
+
- [Configuration](#configuration)
|
|
69
|
+
- [Error handling](#error-handling)
|
|
70
|
+
- [Forward compatibility](#forward-compatibility)
|
|
71
|
+
- [Development](#development)
|
|
72
|
+
|
|
47
73
|
## Installation
|
|
48
74
|
|
|
49
75
|
```bash
|
|
50
76
|
pip install blitz-api-py
|
|
51
|
-
# or: uv add blitz-api-py
|
|
77
|
+
# or: poetry add blitz-api-py / uv add blitz-api-py
|
|
52
78
|
```
|
|
53
79
|
|
|
54
80
|
Requires Python 3.10+.
|
|
@@ -98,6 +124,61 @@ async def main() -> None:
|
|
|
98
124
|
asyncio.run(main())
|
|
99
125
|
```
|
|
100
126
|
|
|
127
|
+
## Example: find, enrich, collect
|
|
128
|
+
|
|
129
|
+
A complete flow — find people, enrich each one's verified work email, collect the
|
|
130
|
+
contacts. `max_items` caps the total fetched so the run can't surprise you with credits.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from blitz_api import BlitzAPI
|
|
134
|
+
from blitz_api.types import Industry, JobLevel
|
|
135
|
+
|
|
136
|
+
client = BlitzAPI() # reads BLITZ_API_KEY
|
|
137
|
+
|
|
138
|
+
# 1. Find up to 25 VPs at software companies (typed filters, 1:1 with the API).
|
|
139
|
+
leads = client.search.people(
|
|
140
|
+
company={"industry": {"include": [Industry.SOFTWARE_DEVELOPMENT]}},
|
|
141
|
+
people={"job_level": [JobLevel.VP]},
|
|
142
|
+
max_results=25,
|
|
143
|
+
).collect(max_items=25) # client-side total cap — bounds credit spend
|
|
144
|
+
|
|
145
|
+
# 2. Enrich each lead's verified work email from their LinkedIn profile URL.
|
|
146
|
+
contacts: list[dict[str, str | None]] = []
|
|
147
|
+
for person in leads:
|
|
148
|
+
if not person.linkedin_url:
|
|
149
|
+
continue
|
|
150
|
+
result = client.enrichment.email(person_linkedin_url=person.linkedin_url)
|
|
151
|
+
if result.found:
|
|
152
|
+
contacts.append({"name": person.full_name, "email": result.email})
|
|
153
|
+
|
|
154
|
+
print(f"Collected {len(contacts)} contacts")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
What comes back is typed and snake_case. A `Person` from the search above (fields are a
|
|
158
|
+
**superset** — only what the profile has is populated, and unknown fields the API adds
|
|
159
|
+
later are preserved):
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
Person(
|
|
163
|
+
full_name="Jordan Lee",
|
|
164
|
+
headline="VP of Engineering at Acme",
|
|
165
|
+
linkedin_url="https://www.linkedin.com/in/example-person",
|
|
166
|
+
location=Location(city="San Francisco", state_code="CA", country_code="US", continent="North America"),
|
|
167
|
+
experiences=[Experience(job_title="VP of Engineering", company_name="Acme", job_is_current=True)],
|
|
168
|
+
# first_name, last_name, skills, education, certifications, … also present
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
And `enrichment.email(...)` returns:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
EmailEnrichmentResponse(
|
|
176
|
+
found=True,
|
|
177
|
+
email="jordan@acme.com",
|
|
178
|
+
all_emails=[EmailMatch(email="jordan@acme.com", email_domain="acme.com")],
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
101
182
|
## Authentication
|
|
102
183
|
|
|
103
184
|
Pass the key explicitly or via the `BLITZ_API_KEY` environment variable:
|
|
@@ -119,7 +200,7 @@ All methods are grouped into four namespaces:
|
|
|
119
200
|
| `client.account` | `key_info()` |
|
|
120
201
|
| `client.search` | `people()`, `companies()`, `employee_finder()`, `waterfall_icp()` |
|
|
121
202
|
| `client.enrichment` | `email()`, `phone()`, `email_to_person()`, `phone_to_person()`, `company()`, `domain_to_linkedin()`, `linkedin_to_domain()` |
|
|
122
|
-
| `client.utils` | `current_date()`, `company_employment_distribution()` |
|
|
203
|
+
| `client.utils` | `current_date()`, `company_employment_distribution()`, `company_department_distribution()` |
|
|
123
204
|
|
|
124
205
|
Every method returns a typed Pydantic model (see `blitz_api.types`). Enum-backed
|
|
125
206
|
filter fields (e.g. `Industry`, `JobLevel`, `Continent`) accept either an enum
|
|
@@ -131,6 +212,13 @@ The search methods return an **auto-paginating page**: iterate it and the SDK fe
|
|
|
131
212
|
each subsequent page for you. `search.people`/`search.companies` are cursor-based;
|
|
132
213
|
`search.employee_finder` is page-based — both behave identically here.
|
|
133
214
|
|
|
215
|
+
> **`max_results` is the page size, not a total.** It's results per page, and the API
|
|
216
|
+
> **bills 1 credit per result returned**. A bare `for person in client.search.people(...)`
|
|
217
|
+
> loop streams *every* match up to the server-side limit (people: 50k results / 1k pages;
|
|
218
|
+
> employee finder: 10k), which can be a lot of credits. Bound it with **`max_items`** on
|
|
219
|
+
> `.collect()` / `.auto_paging_iter()` (a client-side total cap — never sent on the wire),
|
|
220
|
+
> `break` out of the loop, or drive pages manually.
|
|
221
|
+
|
|
134
222
|
```python
|
|
135
223
|
# Iterate every matching person across all pages — no cursor handling needed.
|
|
136
224
|
for person in client.search.people(people={"job_level": ["VP"]}):
|
|
@@ -170,10 +258,13 @@ client = BlitzAPI(
|
|
|
170
258
|
```
|
|
171
259
|
|
|
172
260
|
The client-side rate limiter is a sliding window — at most `rate_limit_rps` requests
|
|
173
|
-
in any rolling second —
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
261
|
+
in any rolling second — applied **per endpoint**: each endpoint (e.g. `.email` vs
|
|
262
|
+
`.phone`) is throttled independently, mirroring the API's own limit, which is also per
|
|
263
|
+
endpoint (5 req/s by default; check yours via
|
|
264
|
+
`client.account.key_info().max_requests_per_seconds`). A single client instance therefore
|
|
265
|
+
stays under the limit on every endpoint, so a burst on one never blocks another. Across
|
|
266
|
+
multiple processes — which share an endpoint's budget — you may still hit `429`; the retry
|
|
267
|
+
path handles that.
|
|
177
268
|
|
|
178
269
|
Every method also accepts a per-call `timeout` (seconds or an `httpx.Timeout`) when one
|
|
179
270
|
endpoint needs longer than the client default:
|
|
@@ -210,6 +301,12 @@ except BlitzError:
|
|
|
210
301
|
but a **read timeout is not** — the server may already have processed (and billed) the
|
|
211
302
|
request, so it surfaces as `APITimeoutError` rather than risking a double charge.
|
|
212
303
|
|
|
304
|
+
## Forward compatibility
|
|
305
|
+
|
|
306
|
+
Response models subclass a base configured with `extra="allow"`, so a field the API adds
|
|
307
|
+
before this SDK models it is still present on the parsed object (via attribute access or
|
|
308
|
+
`.model_extra`). Known fields stay precisely typed.
|
|
309
|
+
|
|
213
310
|
## Development
|
|
214
311
|
|
|
215
312
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, the test/type/lint
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# blitz-api-py
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/blitz-api-py/)
|
|
4
|
+
[](https://pypi.org/project/blitz-api-py/)
|
|
5
|
+
[](https://github.com/api-blitz/blitz-api-py/actions/workflows/ci.yml)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
3
8
|
The typed Python SDK for the [Blitz API](https://blitz-api.ai) — B2B data, search,
|
|
4
9
|
and enrichment.
|
|
5
10
|
|
|
@@ -10,14 +15,35 @@ and enrichment.
|
|
|
10
15
|
- **Resilient** — built-in client-side rate limiting, retries with backoff on
|
|
11
16
|
`429`/`5xx`, and a typed exception hierarchy.
|
|
12
17
|
- **Forward-compatible** — new fields the API adds never break deserialization.
|
|
18
|
+
- **1:1 with the API** — request filters and response fields are snake_case,
|
|
19
|
+
matching [docs.blitz-api.ai](https://docs.blitz-api.ai).
|
|
13
20
|
|
|
14
21
|
> Create and manage API keys at [app.blitz-api.ai](https://app.blitz-api.ai).
|
|
15
22
|
|
|
23
|
+
> **Billing.** Blitz bills **per result**. A bare `for person in client.search.people(...)`
|
|
24
|
+
> loop streams every match up to the server-side limit (people: 50k results), which can be
|
|
25
|
+
> a lot of credits. Bound spend with **`max_items`** (a client-side total cap on
|
|
26
|
+
> `.collect()` / `.auto_paging_iter()`, never sent on the wire) — details in
|
|
27
|
+
> [Pagination](#pagination).
|
|
28
|
+
|
|
29
|
+
## Contents
|
|
30
|
+
|
|
31
|
+
- [Installation](#installation)
|
|
32
|
+
- [Quickstart](#quickstart)
|
|
33
|
+
- [Example: find, enrich, collect](#example-find-enrich-collect)
|
|
34
|
+
- [Authentication](#authentication)
|
|
35
|
+
- [Endpoints](#endpoints)
|
|
36
|
+
- [Pagination](#pagination)
|
|
37
|
+
- [Configuration](#configuration)
|
|
38
|
+
- [Error handling](#error-handling)
|
|
39
|
+
- [Forward compatibility](#forward-compatibility)
|
|
40
|
+
- [Development](#development)
|
|
41
|
+
|
|
16
42
|
## Installation
|
|
17
43
|
|
|
18
44
|
```bash
|
|
19
45
|
pip install blitz-api-py
|
|
20
|
-
# or: uv add blitz-api-py
|
|
46
|
+
# or: poetry add blitz-api-py / uv add blitz-api-py
|
|
21
47
|
```
|
|
22
48
|
|
|
23
49
|
Requires Python 3.10+.
|
|
@@ -67,6 +93,61 @@ async def main() -> None:
|
|
|
67
93
|
asyncio.run(main())
|
|
68
94
|
```
|
|
69
95
|
|
|
96
|
+
## Example: find, enrich, collect
|
|
97
|
+
|
|
98
|
+
A complete flow — find people, enrich each one's verified work email, collect the
|
|
99
|
+
contacts. `max_items` caps the total fetched so the run can't surprise you with credits.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from blitz_api import BlitzAPI
|
|
103
|
+
from blitz_api.types import Industry, JobLevel
|
|
104
|
+
|
|
105
|
+
client = BlitzAPI() # reads BLITZ_API_KEY
|
|
106
|
+
|
|
107
|
+
# 1. Find up to 25 VPs at software companies (typed filters, 1:1 with the API).
|
|
108
|
+
leads = client.search.people(
|
|
109
|
+
company={"industry": {"include": [Industry.SOFTWARE_DEVELOPMENT]}},
|
|
110
|
+
people={"job_level": [JobLevel.VP]},
|
|
111
|
+
max_results=25,
|
|
112
|
+
).collect(max_items=25) # client-side total cap — bounds credit spend
|
|
113
|
+
|
|
114
|
+
# 2. Enrich each lead's verified work email from their LinkedIn profile URL.
|
|
115
|
+
contacts: list[dict[str, str | None]] = []
|
|
116
|
+
for person in leads:
|
|
117
|
+
if not person.linkedin_url:
|
|
118
|
+
continue
|
|
119
|
+
result = client.enrichment.email(person_linkedin_url=person.linkedin_url)
|
|
120
|
+
if result.found:
|
|
121
|
+
contacts.append({"name": person.full_name, "email": result.email})
|
|
122
|
+
|
|
123
|
+
print(f"Collected {len(contacts)} contacts")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
What comes back is typed and snake_case. A `Person` from the search above (fields are a
|
|
127
|
+
**superset** — only what the profile has is populated, and unknown fields the API adds
|
|
128
|
+
later are preserved):
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
Person(
|
|
132
|
+
full_name="Jordan Lee",
|
|
133
|
+
headline="VP of Engineering at Acme",
|
|
134
|
+
linkedin_url="https://www.linkedin.com/in/example-person",
|
|
135
|
+
location=Location(city="San Francisco", state_code="CA", country_code="US", continent="North America"),
|
|
136
|
+
experiences=[Experience(job_title="VP of Engineering", company_name="Acme", job_is_current=True)],
|
|
137
|
+
# first_name, last_name, skills, education, certifications, … also present
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
And `enrichment.email(...)` returns:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
EmailEnrichmentResponse(
|
|
145
|
+
found=True,
|
|
146
|
+
email="jordan@acme.com",
|
|
147
|
+
all_emails=[EmailMatch(email="jordan@acme.com", email_domain="acme.com")],
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
70
151
|
## Authentication
|
|
71
152
|
|
|
72
153
|
Pass the key explicitly or via the `BLITZ_API_KEY` environment variable:
|
|
@@ -88,7 +169,7 @@ All methods are grouped into four namespaces:
|
|
|
88
169
|
| `client.account` | `key_info()` |
|
|
89
170
|
| `client.search` | `people()`, `companies()`, `employee_finder()`, `waterfall_icp()` |
|
|
90
171
|
| `client.enrichment` | `email()`, `phone()`, `email_to_person()`, `phone_to_person()`, `company()`, `domain_to_linkedin()`, `linkedin_to_domain()` |
|
|
91
|
-
| `client.utils` | `current_date()`, `company_employment_distribution()` |
|
|
172
|
+
| `client.utils` | `current_date()`, `company_employment_distribution()`, `company_department_distribution()` |
|
|
92
173
|
|
|
93
174
|
Every method returns a typed Pydantic model (see `blitz_api.types`). Enum-backed
|
|
94
175
|
filter fields (e.g. `Industry`, `JobLevel`, `Continent`) accept either an enum
|
|
@@ -100,6 +181,13 @@ The search methods return an **auto-paginating page**: iterate it and the SDK fe
|
|
|
100
181
|
each subsequent page for you. `search.people`/`search.companies` are cursor-based;
|
|
101
182
|
`search.employee_finder` is page-based — both behave identically here.
|
|
102
183
|
|
|
184
|
+
> **`max_results` is the page size, not a total.** It's results per page, and the API
|
|
185
|
+
> **bills 1 credit per result returned**. A bare `for person in client.search.people(...)`
|
|
186
|
+
> loop streams *every* match up to the server-side limit (people: 50k results / 1k pages;
|
|
187
|
+
> employee finder: 10k), which can be a lot of credits. Bound it with **`max_items`** on
|
|
188
|
+
> `.collect()` / `.auto_paging_iter()` (a client-side total cap — never sent on the wire),
|
|
189
|
+
> `break` out of the loop, or drive pages manually.
|
|
190
|
+
|
|
103
191
|
```python
|
|
104
192
|
# Iterate every matching person across all pages — no cursor handling needed.
|
|
105
193
|
for person in client.search.people(people={"job_level": ["VP"]}):
|
|
@@ -139,10 +227,13 @@ client = BlitzAPI(
|
|
|
139
227
|
```
|
|
140
228
|
|
|
141
229
|
The client-side rate limiter is a sliding window — at most `rate_limit_rps` requests
|
|
142
|
-
in any rolling second —
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
230
|
+
in any rolling second — applied **per endpoint**: each endpoint (e.g. `.email` vs
|
|
231
|
+
`.phone`) is throttled independently, mirroring the API's own limit, which is also per
|
|
232
|
+
endpoint (5 req/s by default; check yours via
|
|
233
|
+
`client.account.key_info().max_requests_per_seconds`). A single client instance therefore
|
|
234
|
+
stays under the limit on every endpoint, so a burst on one never blocks another. Across
|
|
235
|
+
multiple processes — which share an endpoint's budget — you may still hit `429`; the retry
|
|
236
|
+
path handles that.
|
|
146
237
|
|
|
147
238
|
Every method also accepts a per-call `timeout` (seconds or an `httpx.Timeout`) when one
|
|
148
239
|
endpoint needs longer than the client default:
|
|
@@ -179,6 +270,12 @@ except BlitzError:
|
|
|
179
270
|
but a **read timeout is not** — the server may already have processed (and billed) the
|
|
180
271
|
request, so it surfaces as `APITimeoutError` rather than risking a double charge.
|
|
181
272
|
|
|
273
|
+
## Forward compatibility
|
|
274
|
+
|
|
275
|
+
Response models subclass a base configured with `extra="allow"`, so a field the API adds
|
|
276
|
+
before this SDK models it is still present on the parsed object (via attribute access or
|
|
277
|
+
`.model_extra`). Known fields stay precisely typed.
|
|
278
|
+
|
|
182
279
|
## Development
|
|
183
280
|
|
|
184
281
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, the test/type/lint
|
|
@@ -50,13 +50,34 @@ class AsyncBlitzAPI(BaseClient):
|
|
|
50
50
|
)
|
|
51
51
|
self._http_client = http_client or httpx.AsyncClient(timeout=timeout)
|
|
52
52
|
self._owns_http_client = http_client is None
|
|
53
|
-
self.
|
|
53
|
+
self._rate_limit_rps = rate_limit_rps
|
|
54
|
+
# One limiter per endpoint path so each endpoint's rate limit is tracked
|
|
55
|
+
# independently (e.g. ``.email`` and ``.phone`` do not share a budget). Built
|
|
56
|
+
# lazily in ``_limiter_for`` on first use of each path.
|
|
57
|
+
self._rate_limiters: dict[str, AsyncRateLimiter] = {}
|
|
54
58
|
if sleep is None:
|
|
55
59
|
import asyncio
|
|
56
60
|
|
|
57
61
|
sleep = asyncio.sleep
|
|
58
62
|
self._sleep = sleep
|
|
59
63
|
|
|
64
|
+
def _limiter_for(self, path: str) -> AsyncRateLimiter:
|
|
65
|
+
"""Return the per-endpoint limiter for ``path``, creating it on first use.
|
|
66
|
+
|
|
67
|
+
Each endpoint path gets its own sliding window so its rate limit is tracked
|
|
68
|
+
independently of every other endpoint.
|
|
69
|
+
"""
|
|
70
|
+
limiter = self._rate_limiters.get(path)
|
|
71
|
+
if limiter is None:
|
|
72
|
+
# ``setdefault`` keeps concurrent first-callers on the same instance; any
|
|
73
|
+
# extra limiter built in a race is harmlessly discarded. Thread the client's
|
|
74
|
+
# ``sleep`` through so a custom/fake sleep injected for tests also drives the
|
|
75
|
+
# limiter's throttle wait, not just the retry backoff.
|
|
76
|
+
limiter = self._rate_limiters.setdefault(
|
|
77
|
+
path, AsyncRateLimiter(self._rate_limit_rps, sleep=self._sleep)
|
|
78
|
+
)
|
|
79
|
+
return limiter
|
|
80
|
+
|
|
60
81
|
async def _request(
|
|
61
82
|
self,
|
|
62
83
|
method: str,
|
|
@@ -72,7 +93,7 @@ class AsyncBlitzAPI(BaseClient):
|
|
|
72
93
|
|
|
73
94
|
attempt = 0
|
|
74
95
|
while True:
|
|
75
|
-
await self.
|
|
96
|
+
await self._limiter_for(path).acquire()
|
|
76
97
|
try:
|
|
77
98
|
if timeout is None:
|
|
78
99
|
response = await self._http_client.request(
|
|
@@ -52,13 +52,34 @@ class BlitzAPI(BaseClient):
|
|
|
52
52
|
)
|
|
53
53
|
self._http_client = http_client or httpx.Client(timeout=timeout)
|
|
54
54
|
self._owns_http_client = http_client is None
|
|
55
|
-
self.
|
|
55
|
+
self._rate_limit_rps = rate_limit_rps
|
|
56
|
+
# One limiter per endpoint path so each endpoint's rate limit is tracked
|
|
57
|
+
# independently (e.g. ``.email`` and ``.phone`` do not share a budget). Built
|
|
58
|
+
# lazily in ``_limiter_for`` on first use of each path.
|
|
59
|
+
self._rate_limiters: dict[str, RateLimiter] = {}
|
|
56
60
|
if sleep is None:
|
|
57
61
|
import time
|
|
58
62
|
|
|
59
63
|
sleep = time.sleep
|
|
60
64
|
self._sleep = sleep
|
|
61
65
|
|
|
66
|
+
def _limiter_for(self, path: str) -> RateLimiter:
|
|
67
|
+
"""Return the per-endpoint limiter for ``path``, creating it on first use.
|
|
68
|
+
|
|
69
|
+
Each endpoint path gets its own sliding window so its rate limit is tracked
|
|
70
|
+
independently of every other endpoint.
|
|
71
|
+
"""
|
|
72
|
+
limiter = self._rate_limiters.get(path)
|
|
73
|
+
if limiter is None:
|
|
74
|
+
# ``setdefault`` keeps concurrent first-callers on the same instance; any
|
|
75
|
+
# extra limiter built in a race is harmlessly discarded. Thread the client's
|
|
76
|
+
# ``sleep`` through so a custom/fake sleep injected for tests also drives the
|
|
77
|
+
# limiter's throttle wait, not just the retry backoff.
|
|
78
|
+
limiter = self._rate_limiters.setdefault(
|
|
79
|
+
path, RateLimiter(self._rate_limit_rps, sleep=self._sleep)
|
|
80
|
+
)
|
|
81
|
+
return limiter
|
|
82
|
+
|
|
62
83
|
def _request(
|
|
63
84
|
self,
|
|
64
85
|
method: str,
|
|
@@ -74,7 +95,7 @@ class BlitzAPI(BaseClient):
|
|
|
74
95
|
|
|
75
96
|
attempt = 0
|
|
76
97
|
while True:
|
|
77
|
-
self.
|
|
98
|
+
self._limiter_for(path).acquire()
|
|
78
99
|
try:
|
|
79
100
|
if timeout is None:
|
|
80
101
|
response = self._http_client.request(
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Client-side sliding-window rate limiter (async source; sync twin generated).
|
|
2
2
|
|
|
3
|
-
The API enforces a per-
|
|
4
|
-
outgoing requests *before* they are sent
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The API enforces a per-endpoint request rate (5 req/s by default). This limiter throttles
|
|
4
|
+
outgoing requests *before* they are sent. The client holds one limiter **per endpoint
|
|
5
|
+
path** (see ``_client_async.py``), so each endpoint is throttled to ``rps`` independently,
|
|
6
|
+
mirroring the server's per-endpoint budget; a single client instance therefore stays under
|
|
7
|
+
the limit on every endpoint on its own. The server-side 429 retry path is the backstop for
|
|
8
|
+
bursts across processes, which share the same per-endpoint budget.
|
|
7
9
|
|
|
8
10
|
The algorithm is a sliding window: at most ``rps`` requests may begin in any rolling
|
|
9
11
|
one-second window. This matches the Blitz docs ("max 5 requests per 1000 ms") and the
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
# Do not edit by hand — edit the async source and run `python scripts/gen_sync.py`.
|
|
3
3
|
"""Client-side sliding-window rate limiter (async source; sync twin generated).
|
|
4
4
|
|
|
5
|
-
The API enforces a per-
|
|
6
|
-
outgoing requests *before* they are sent
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
The API enforces a per-endpoint request rate (5 req/s by default). This limiter throttles
|
|
6
|
+
outgoing requests *before* they are sent. The client holds one limiter **per endpoint
|
|
7
|
+
path** (see ``_client_async.py``), so each endpoint is throttled to ``rps`` independently,
|
|
8
|
+
mirroring the server's per-endpoint budget; a single client instance therefore stays under
|
|
9
|
+
the limit on every endpoint on its own. The server-side 429 retry path is the backstop for
|
|
10
|
+
bursts across processes, which share the same per-endpoint budget.
|
|
9
11
|
|
|
10
12
|
The algorithm is a sliding window: at most ``rps`` requests may begin in any rolling
|
|
11
13
|
one-second window. This matches the Blitz docs ("max 5 requests per 1000 ms") and the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0" # x-release-please-version
|
|
@@ -5,13 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from ..._compat import TimeoutParam
|
|
8
|
-
from ...types.utils import
|
|
8
|
+
from ...types.utils import (
|
|
9
|
+
CompanyDepartmentDistributionResponse,
|
|
10
|
+
CompanyEmploymentDistributionResponse,
|
|
11
|
+
CurrentDateResponse,
|
|
12
|
+
)
|
|
9
13
|
|
|
10
14
|
if TYPE_CHECKING:
|
|
11
15
|
from ..._client import AsyncBlitzAPI
|
|
12
16
|
|
|
13
17
|
_CURRENT_DATE = "/v2/utils/current-date"
|
|
14
18
|
_EMPLOYMENT_DISTRIBUTION = "/v2/utils/company-employment-distribution"
|
|
19
|
+
_DEPARTMENT_DISTRIBUTION = "/v2/utils/company-department-distribution"
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
class AsyncUtilsResource:
|
|
@@ -41,3 +46,18 @@ class AsyncUtilsResource:
|
|
|
41
46
|
cast_to=CompanyEmploymentDistributionResponse,
|
|
42
47
|
timeout=timeout,
|
|
43
48
|
)
|
|
49
|
+
|
|
50
|
+
async def company_department_distribution(
|
|
51
|
+
self, *, company_linkedin_url: str, timeout: TimeoutParam = None
|
|
52
|
+
) -> CompanyDepartmentDistributionResponse:
|
|
53
|
+
"""Get a company's employee count broken down by department.
|
|
54
|
+
|
|
55
|
+
Employees with no classified department are counted under ``"Other"``.
|
|
56
|
+
"""
|
|
57
|
+
return await self._client._request(
|
|
58
|
+
"POST",
|
|
59
|
+
_DEPARTMENT_DISTRIBUTION,
|
|
60
|
+
body={"company_linkedin_url": company_linkedin_url},
|
|
61
|
+
cast_to=CompanyDepartmentDistributionResponse,
|
|
62
|
+
timeout=timeout,
|
|
63
|
+
)
|
|
@@ -7,13 +7,18 @@ from __future__ import annotations
|
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
9
|
from ..._compat import TimeoutParam
|
|
10
|
-
from ...types.utils import
|
|
10
|
+
from ...types.utils import (
|
|
11
|
+
CompanyDepartmentDistributionResponse,
|
|
12
|
+
CompanyEmploymentDistributionResponse,
|
|
13
|
+
CurrentDateResponse,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
if TYPE_CHECKING:
|
|
13
17
|
from ..._client import BlitzAPI
|
|
14
18
|
|
|
15
19
|
_CURRENT_DATE = "/v2/utils/current-date"
|
|
16
20
|
_EMPLOYMENT_DISTRIBUTION = "/v2/utils/company-employment-distribution"
|
|
21
|
+
_DEPARTMENT_DISTRIBUTION = "/v2/utils/company-department-distribution"
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class UtilsResource:
|
|
@@ -43,3 +48,18 @@ class UtilsResource:
|
|
|
43
48
|
cast_to=CompanyEmploymentDistributionResponse,
|
|
44
49
|
timeout=timeout,
|
|
45
50
|
)
|
|
51
|
+
|
|
52
|
+
def company_department_distribution(
|
|
53
|
+
self, *, company_linkedin_url: str, timeout: TimeoutParam = None
|
|
54
|
+
) -> CompanyDepartmentDistributionResponse:
|
|
55
|
+
"""Get a company's employee count broken down by department.
|
|
56
|
+
|
|
57
|
+
Employees with no classified department are counted under ``"Other"``.
|
|
58
|
+
"""
|
|
59
|
+
return self._client._request(
|
|
60
|
+
"POST",
|
|
61
|
+
_DEPARTMENT_DISTRIBUTION,
|
|
62
|
+
body={"company_linkedin_url": company_linkedin_url},
|
|
63
|
+
cast_to=CompanyDepartmentDistributionResponse,
|
|
64
|
+
timeout=timeout,
|
|
65
|
+
)
|
|
@@ -53,8 +53,10 @@ from .shared import (
|
|
|
53
53
|
Person,
|
|
54
54
|
)
|
|
55
55
|
from .utils import (
|
|
56
|
+
CompanyDepartmentDistributionResponse,
|
|
56
57
|
CompanyEmploymentDistributionResponse,
|
|
57
58
|
CurrentDateResponse,
|
|
59
|
+
DepartmentDistributionItem,
|
|
58
60
|
EmploymentDistributionItem,
|
|
59
61
|
)
|
|
60
62
|
|
|
@@ -105,4 +107,6 @@ __all__ = [
|
|
|
105
107
|
"CurrentDateResponse",
|
|
106
108
|
"EmploymentDistributionItem",
|
|
107
109
|
"CompanyEmploymentDistributionResponse",
|
|
110
|
+
"DepartmentDistributionItem",
|
|
111
|
+
"CompanyDepartmentDistributionResponse",
|
|
108
112
|
]
|
|
@@ -8,6 +8,8 @@ __all__ = [
|
|
|
8
8
|
"CurrentDateResponse",
|
|
9
9
|
"EmploymentDistributionItem",
|
|
10
10
|
"CompanyEmploymentDistributionResponse",
|
|
11
|
+
"DepartmentDistributionItem",
|
|
12
|
+
"CompanyDepartmentDistributionResponse",
|
|
11
13
|
]
|
|
12
14
|
|
|
13
15
|
|
|
@@ -33,3 +35,21 @@ class CompanyEmploymentDistributionResponse(BlitzModel):
|
|
|
33
35
|
company_linkedin_url: str | None = None
|
|
34
36
|
total_employees: int | None = None
|
|
35
37
|
distribution: list[EmploymentDistributionItem] = []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DepartmentDistributionItem(BlitzModel):
|
|
41
|
+
"""Employee count for a single department (Blitz job function).
|
|
42
|
+
|
|
43
|
+
Employees with no classified department are counted under ``"Other"``.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
department: str | None = None
|
|
47
|
+
count: int | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CompanyDepartmentDistributionResponse(BlitzModel):
|
|
51
|
+
"""Result of ``utils.company_department_distribution``."""
|
|
52
|
+
|
|
53
|
+
company_linkedin_url: str | None = None
|
|
54
|
+
total_employees: int | None = None
|
|
55
|
+
distribution: list[DepartmentDistributionItem] = []
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0" # x-release-please-version
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|