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.
Files changed (43) hide show
  1. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/CHANGELOG.md +19 -0
  2. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/PKG-INFO +104 -7
  3. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/README.md +103 -6
  4. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_client_async.py +23 -2
  5. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_client_sync.py +23 -2
  6. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_rate_limit_async.py +6 -4
  7. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_rate_limit_sync.py +6 -4
  8. blitz_api_py-0.5.0/src/blitz_api/_version.py +1 -0
  9. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/utils.py +21 -1
  10. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/utils.py +21 -1
  11. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/__init__.py +4 -0
  12. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/utils.py +20 -0
  13. blitz_api_py-0.3.0/src/blitz_api/_version.py +0 -1
  14. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/.gitignore +0 -0
  15. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/LICENSE +0 -0
  16. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/pyproject.toml +0 -0
  17. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/__init__.py +0 -0
  18. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_base_client.py +0 -0
  19. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_client.py +0 -0
  20. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_compat.py +0 -0
  21. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_constants.py +0 -0
  22. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_exceptions.py +0 -0
  23. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_pagination_async.py +0 -0
  24. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_pagination_base.py +0 -0
  25. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_pagination_sync.py +0 -0
  26. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/_rate_limit.py +0 -0
  27. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/py.typed +0 -0
  28. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/__init__.py +0 -0
  29. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/__init__.py +0 -0
  30. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/account.py +0 -0
  31. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/enrichment.py +0 -0
  32. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_async/search.py +0 -0
  33. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/__init__.py +0 -0
  34. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/account.py +0 -0
  35. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/enrichment.py +0 -0
  36. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/resources/_sync/search.py +0 -0
  37. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/_models.py +0 -0
  38. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/account.py +0 -0
  39. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/enrichment.py +0 -0
  40. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/enums.py +0 -0
  41. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/filters.py +0 -0
  42. {blitz_api_py-0.3.0 → blitz_api_py-0.5.0}/src/blitz_api/types/search.py +0 -0
  43. {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.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
+ [![PyPI version](https://img.shields.io/pypi/v/blitz-api-py.svg)](https://pypi.org/project/blitz-api-py/)
35
+ [![types: py.typed](https://img.shields.io/badge/types-py.typed-blue.svg)](https://pypi.org/project/blitz-api-py/)
36
+ [![CI](https://github.com/api-blitz/blitz-api-py/actions/workflows/ci.yml/badge.svg)](https://github.com/api-blitz/blitz-api-py/actions/workflows/ci.yml)
37
+ [![license: MIT](https://img.shields.io/pypi/l/blitz-api-py.svg)](./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 — so a single client instance stays under the API's limit (5 req/s
174
- by default; check your key's limit via
175
- `client.account.key_info().max_requests_per_seconds`). Across multiple processes you may
176
- still hit `429` the retry path handles that.
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
+ [![PyPI version](https://img.shields.io/pypi/v/blitz-api-py.svg)](https://pypi.org/project/blitz-api-py/)
4
+ [![types: py.typed](https://img.shields.io/badge/types-py.typed-blue.svg)](https://pypi.org/project/blitz-api-py/)
5
+ [![CI](https://github.com/api-blitz/blitz-api-py/actions/workflows/ci.yml/badge.svg)](https://github.com/api-blitz/blitz-api-py/actions/workflows/ci.yml)
6
+ [![license: MIT](https://img.shields.io/pypi/l/blitz-api-py.svg)](./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 — so a single client instance stays under the API's limit (5 req/s
143
- by default; check your key's limit via
144
- `client.account.key_info().max_requests_per_seconds`). Across multiple processes you may
145
- still hit `429` the retry path handles that.
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._rate_limiter = AsyncRateLimiter(rate_limit_rps)
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._rate_limiter.acquire()
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._rate_limiter = RateLimiter(rate_limit_rps)
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._rate_limiter.acquire()
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-key request rate (5 req/s by default). This limiter throttles
4
- outgoing requests *before* they are sent so a single client instance stays under the
5
- limit proactively; the server-side 429 retry path is the backstop for bursts across
6
- processes.
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-key request rate (5 req/s by default). This limiter throttles
6
- outgoing requests *before* they are sent so a single client instance stays under the
7
- limit proactively; the server-side 429 retry path is the backstop for bursts across
8
- processes.
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 CompanyEmploymentDistributionResponse, CurrentDateResponse
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 CompanyEmploymentDistributionResponse, CurrentDateResponse
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