bls-api-client 1.0.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.
- bls_api_client-1.0.0/.github/workflows/ci.yml +24 -0
- bls_api_client-1.0.0/.github/workflows/release.yml +20 -0
- bls_api_client-1.0.0/.python-version +1 -0
- bls_api_client-1.0.0/LICENSE +21 -0
- bls_api_client-1.0.0/PKG-INFO +236 -0
- bls_api_client-1.0.0/README.md +205 -0
- bls_api_client-1.0.0/pyproject.toml +53 -0
- bls_api_client-1.0.0/src/blsapi/__about__.py +3 -0
- bls_api_client-1.0.0/src/blsapi/__init__.py +44 -0
- bls_api_client-1.0.0/src/blsapi/_core.py +58 -0
- bls_api_client-1.0.0/src/blsapi/_endpoints.py +33 -0
- bls_api_client-1.0.0/src/blsapi/_retry.py +46 -0
- bls_api_client-1.0.0/src/blsapi/aclient.py +165 -0
- bls_api_client-1.0.0/src/blsapi/batching.py +59 -0
- bls_api_client-1.0.0/src/blsapi/client.py +169 -0
- bls_api_client-1.0.0/src/blsapi/config.py +53 -0
- bls_api_client-1.0.0/src/blsapi/enums.py +23 -0
- bls_api_client-1.0.0/src/blsapi/exceptions.py +31 -0
- bls_api_client-1.0.0/src/blsapi/frames.py +138 -0
- bls_api_client-1.0.0/src/blsapi/models.py +141 -0
- bls_api_client-1.0.0/src/blsapi/py.typed +0 -0
- bls_api_client-1.0.0/uv.lock +265 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v6
|
|
16
|
+
- name: Build sdist and wheel
|
|
17
|
+
run: uv build
|
|
18
|
+
- name: Check distribution metadata
|
|
19
|
+
run: uvx twine check dist/*
|
|
20
|
+
- name: Upload distributions
|
|
21
|
+
uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/*
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v6
|
|
17
|
+
- name: Build sdist and wheel
|
|
18
|
+
run: uv build
|
|
19
|
+
- name: Publish to PyPI
|
|
20
|
+
run: uv publish
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 covertcast
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bls-api-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A sync + async client for the BLS Public Data API
|
|
5
|
+
Project-URL: Homepage, https://github.com/covertcast/blsapi
|
|
6
|
+
Project-URL: Repository, https://github.com/covertcast/blsapi
|
|
7
|
+
Project-URL: Issues, https://github.com/covertcast/blsapi/issues
|
|
8
|
+
Author-email: covertcast <covertcast@proton.me>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api-client,bls,bureau-of-labor-statistics,economics,labor-statistics,polars
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: anyio>=4.0
|
|
27
|
+
Requires-Dist: httpx2>=2.4.0
|
|
28
|
+
Requires-Dist: polars>=1.42.0
|
|
29
|
+
Requires-Dist: pydantic>=2.13.4
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# blsapi
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/blsapi/)
|
|
35
|
+
[](https://pypi.org/project/blsapi/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
A **sync + async** client inspired by `blsR` for the [U.S. Bureau of Labor Statistics
|
|
39
|
+
(BLS) Public Data API](https://www.bls.gov/developers/).
|
|
40
|
+
|
|
41
|
+
- **Sync and async** clients (`BLSClient`, `AsyncBLSClient`).
|
|
42
|
+
- **Polars output** returns easy to use and convert Polars Dataframes.
|
|
43
|
+
- **Automatic batching** over the BLS per-request limits (series count and year span).
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
Requires Python 3.11+:
|
|
48
|
+
```bash
|
|
49
|
+
pip install bls-api-client
|
|
50
|
+
# or
|
|
51
|
+
uv add bls-api-client
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import blsapi
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from blsapi import BLSClient
|
|
62
|
+
|
|
63
|
+
with BLSClient() as client:
|
|
64
|
+
df = client.get_series("LNS14000000", start_year=2023, end_year=2024)
|
|
65
|
+
|
|
66
|
+
print(df)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The result is a tidy/long DataFrame. Exporting elsewhere is
|
|
70
|
+
simple:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
df.write_parquet("data.parquet")
|
|
74
|
+
df.write_csv("data.csv")
|
|
75
|
+
df.to_pandas()
|
|
76
|
+
df.to_arrow()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Async
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import anyio
|
|
83
|
+
from blsapi import AsyncBLSClient
|
|
84
|
+
|
|
85
|
+
async def main():
|
|
86
|
+
async with AsyncBLSClient() as client:
|
|
87
|
+
df = await client.get_series("LNS14000000", start_year=2023, end_year=2024)
|
|
88
|
+
print(df)
|
|
89
|
+
|
|
90
|
+
anyio.run(main)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
Everything has a sensible default, so `blsapi` works out of the box.
|
|
96
|
+
|
|
97
|
+
### API key
|
|
98
|
+
|
|
99
|
+
A registration key is optional but raises your rate limits substantially. The key is resolved with this precedence:
|
|
100
|
+
|
|
101
|
+
1. The explicit `api_key=` argument, if given.
|
|
102
|
+
2. Otherwise the `BLS_API_KEY` environment variable.
|
|
103
|
+
3. Otherwise no key (unauthenticated tier).
|
|
104
|
+
|
|
105
|
+
### Example:
|
|
106
|
+
```python
|
|
107
|
+
# Explicit (highest precedence)
|
|
108
|
+
client = BLSClient(api_key="your_32_char_key")
|
|
109
|
+
|
|
110
|
+
# Or rely on the environment
|
|
111
|
+
# export BLS_API_KEY=your_32_char_key (macOS/Linux)
|
|
112
|
+
# $env:BLS_API_KEY = "your_32_char_key" (PowerShell)
|
|
113
|
+
client = BLSClient()
|
|
114
|
+
|
|
115
|
+
client.has_key # returns True if a key is active
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Other options
|
|
119
|
+
|
|
120
|
+
All are keyword-only on both clients:
|
|
121
|
+
|
|
122
|
+
| Argument | Default | Purpose |
|
|
123
|
+
| ------------- | -------------------------------- | -------------------------------------------------------------- |
|
|
124
|
+
| `api_key` | `None` -> `BLS_API_KEY` | Registration key. |
|
|
125
|
+
| `base_url` | `https://api.bls.gov/publicAPI/v2/` | API base URL. |
|
|
126
|
+
| `timeout` | `5/30/10/5s` (connect/read/write/pool) | `httpx2.Timeout`. |
|
|
127
|
+
| `max_retries` | `3` | Application-level retries for transient failures. |
|
|
128
|
+
| `auto_batch` | `True` | Transparently split over the tier limits. |
|
|
129
|
+
| `user_agent` | `blsapi/<version>` | Sent as `User-Agent`. |
|
|
130
|
+
| `client` | `None` | Inject your own `httpx2.Client`/`AsyncClient`.|
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import httpx2
|
|
134
|
+
from blsapi import BLSClient
|
|
135
|
+
|
|
136
|
+
client = BLSClient(
|
|
137
|
+
api_key="…",
|
|
138
|
+
max_retries=5,
|
|
139
|
+
timeout=httpx2.Timeout(connect=5.0, read=60.0, write=10.0, pool=5.0),
|
|
140
|
+
user_agent="my-app/1.0 (you@example.com)",
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Usage
|
|
145
|
+
|
|
146
|
+
### Multiple series and named aliases
|
|
147
|
+
|
|
148
|
+
Pass a list of ids, or a `{label: id}` mapping to relabel the output's `series_id` column:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
df = client.get_series(
|
|
152
|
+
{"unemployment": "LNS14000000", "cpi": "CUUR0000SA0"},
|
|
153
|
+
start_year=2020,
|
|
154
|
+
end_year=2024,
|
|
155
|
+
)
|
|
156
|
+
df["series_id"].unique().to_list() # -> ["cpi", "unemployment"]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Wide format
|
|
160
|
+
|
|
161
|
+
Reshape the long frame to one value column per series, indexed by date:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from blsapi import pivot_wide
|
|
165
|
+
|
|
166
|
+
wide = pivot_wide(df) # columns: date, unemployment, cpi
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
(Annual-average rows have no real date and are dropped by `pivot_wide`.)
|
|
170
|
+
|
|
171
|
+
### Calculations and catalog
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
df = client.get_series(
|
|
175
|
+
"LNS14000000", start_year=2023, end_year=2024, calculations=True
|
|
176
|
+
)
|
|
177
|
+
# adds net_change_{1,3,6,12}m and pct_change_{1,3,6,12}m columns
|
|
178
|
+
|
|
179
|
+
resp = client.get_series_raw("LNS14000000", catalog=True) # -> BLSResponse with catalog metadata
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Surveys and popular series
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
client.list_surveys() # -> DataFrame of all surveys
|
|
186
|
+
client.get_survey("CU") # -> dict of metadata for one survey
|
|
187
|
+
client.get_popular() # -> list of popular series ids
|
|
188
|
+
client.get_popular("LN") # -> popular ids within a survey
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Quota planning
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
client.query_cost(["LNS14000000", "CUUR0000SA0"], 2000, 2024) # -> number of API calls used
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Error handling
|
|
198
|
+
|
|
199
|
+
All errors derive from `BLSError`, so you can catch broadly or narrowly:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from blsapi import BLSError, BLSValidationError, BLSAPIError, BLSHTTPError
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
df = client.get_series("BAD_ID", start_year=2024, end_year=2023)
|
|
206
|
+
except BLSValidationError:
|
|
207
|
+
... # bad input caught locally, before any network call
|
|
208
|
+
except BLSAPIError as e:
|
|
209
|
+
... # BLS returned a non-success status; inspect e.status and e.messages
|
|
210
|
+
except BLSHTTPError:
|
|
211
|
+
... # transport/HTTP failure that survived the retry loop
|
|
212
|
+
except BLSError:
|
|
213
|
+
... # anything else from this library
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Tiers & limits
|
|
217
|
+
|
|
218
|
+
| | Series per request | Years per request | Queries per day |
|
|
219
|
+
| --- | --- | --- | --- |
|
|
220
|
+
| **No key** | 25 | 10 | 25 |
|
|
221
|
+
| **With key** | 50 | 20 | 500 |
|
|
222
|
+
|
|
223
|
+
With `auto_batch=True` (the default), requests that exceed the per-request limits are split
|
|
224
|
+
into multiple calls automatically and stitched back together. Register for a free key at
|
|
225
|
+
<https://data.bls.gov/registrationEngine/>.
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
uv sync # install dependencies
|
|
231
|
+
uv build # build the sdist + wheel
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# blsapi
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/blsapi/)
|
|
4
|
+
[](https://pypi.org/project/blsapi/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A **sync + async** client inspired by `blsR` for the [U.S. Bureau of Labor Statistics
|
|
8
|
+
(BLS) Public Data API](https://www.bls.gov/developers/).
|
|
9
|
+
|
|
10
|
+
- **Sync and async** clients (`BLSClient`, `AsyncBLSClient`).
|
|
11
|
+
- **Polars output** returns easy to use and convert Polars Dataframes.
|
|
12
|
+
- **Automatic batching** over the BLS per-request limits (series count and year span).
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Requires Python 3.11+:
|
|
17
|
+
```bash
|
|
18
|
+
pip install bls-api-client
|
|
19
|
+
# or
|
|
20
|
+
uv add bls-api-client
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import blsapi
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from blsapi import BLSClient
|
|
31
|
+
|
|
32
|
+
with BLSClient() as client:
|
|
33
|
+
df = client.get_series("LNS14000000", start_year=2023, end_year=2024)
|
|
34
|
+
|
|
35
|
+
print(df)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The result is a tidy/long DataFrame. Exporting elsewhere is
|
|
39
|
+
simple:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
df.write_parquet("data.parquet")
|
|
43
|
+
df.write_csv("data.csv")
|
|
44
|
+
df.to_pandas()
|
|
45
|
+
df.to_arrow()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Async
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import anyio
|
|
52
|
+
from blsapi import AsyncBLSClient
|
|
53
|
+
|
|
54
|
+
async def main():
|
|
55
|
+
async with AsyncBLSClient() as client:
|
|
56
|
+
df = await client.get_series("LNS14000000", start_year=2023, end_year=2024)
|
|
57
|
+
print(df)
|
|
58
|
+
|
|
59
|
+
anyio.run(main)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Everything has a sensible default, so `blsapi` works out of the box.
|
|
65
|
+
|
|
66
|
+
### API key
|
|
67
|
+
|
|
68
|
+
A registration key is optional but raises your rate limits substantially. The key is resolved with this precedence:
|
|
69
|
+
|
|
70
|
+
1. The explicit `api_key=` argument, if given.
|
|
71
|
+
2. Otherwise the `BLS_API_KEY` environment variable.
|
|
72
|
+
3. Otherwise no key (unauthenticated tier).
|
|
73
|
+
|
|
74
|
+
### Example:
|
|
75
|
+
```python
|
|
76
|
+
# Explicit (highest precedence)
|
|
77
|
+
client = BLSClient(api_key="your_32_char_key")
|
|
78
|
+
|
|
79
|
+
# Or rely on the environment
|
|
80
|
+
# export BLS_API_KEY=your_32_char_key (macOS/Linux)
|
|
81
|
+
# $env:BLS_API_KEY = "your_32_char_key" (PowerShell)
|
|
82
|
+
client = BLSClient()
|
|
83
|
+
|
|
84
|
+
client.has_key # returns True if a key is active
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Other options
|
|
88
|
+
|
|
89
|
+
All are keyword-only on both clients:
|
|
90
|
+
|
|
91
|
+
| Argument | Default | Purpose |
|
|
92
|
+
| ------------- | -------------------------------- | -------------------------------------------------------------- |
|
|
93
|
+
| `api_key` | `None` -> `BLS_API_KEY` | Registration key. |
|
|
94
|
+
| `base_url` | `https://api.bls.gov/publicAPI/v2/` | API base URL. |
|
|
95
|
+
| `timeout` | `5/30/10/5s` (connect/read/write/pool) | `httpx2.Timeout`. |
|
|
96
|
+
| `max_retries` | `3` | Application-level retries for transient failures. |
|
|
97
|
+
| `auto_batch` | `True` | Transparently split over the tier limits. |
|
|
98
|
+
| `user_agent` | `blsapi/<version>` | Sent as `User-Agent`. |
|
|
99
|
+
| `client` | `None` | Inject your own `httpx2.Client`/`AsyncClient`.|
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import httpx2
|
|
103
|
+
from blsapi import BLSClient
|
|
104
|
+
|
|
105
|
+
client = BLSClient(
|
|
106
|
+
api_key="…",
|
|
107
|
+
max_retries=5,
|
|
108
|
+
timeout=httpx2.Timeout(connect=5.0, read=60.0, write=10.0, pool=5.0),
|
|
109
|
+
user_agent="my-app/1.0 (you@example.com)",
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Usage
|
|
114
|
+
|
|
115
|
+
### Multiple series and named aliases
|
|
116
|
+
|
|
117
|
+
Pass a list of ids, or a `{label: id}` mapping to relabel the output's `series_id` column:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
df = client.get_series(
|
|
121
|
+
{"unemployment": "LNS14000000", "cpi": "CUUR0000SA0"},
|
|
122
|
+
start_year=2020,
|
|
123
|
+
end_year=2024,
|
|
124
|
+
)
|
|
125
|
+
df["series_id"].unique().to_list() # -> ["cpi", "unemployment"]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Wide format
|
|
129
|
+
|
|
130
|
+
Reshape the long frame to one value column per series, indexed by date:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from blsapi import pivot_wide
|
|
134
|
+
|
|
135
|
+
wide = pivot_wide(df) # columns: date, unemployment, cpi
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
(Annual-average rows have no real date and are dropped by `pivot_wide`.)
|
|
139
|
+
|
|
140
|
+
### Calculations and catalog
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
df = client.get_series(
|
|
144
|
+
"LNS14000000", start_year=2023, end_year=2024, calculations=True
|
|
145
|
+
)
|
|
146
|
+
# adds net_change_{1,3,6,12}m and pct_change_{1,3,6,12}m columns
|
|
147
|
+
|
|
148
|
+
resp = client.get_series_raw("LNS14000000", catalog=True) # -> BLSResponse with catalog metadata
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Surveys and popular series
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
client.list_surveys() # -> DataFrame of all surveys
|
|
155
|
+
client.get_survey("CU") # -> dict of metadata for one survey
|
|
156
|
+
client.get_popular() # -> list of popular series ids
|
|
157
|
+
client.get_popular("LN") # -> popular ids within a survey
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Quota planning
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
client.query_cost(["LNS14000000", "CUUR0000SA0"], 2000, 2024) # -> number of API calls used
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Error handling
|
|
167
|
+
|
|
168
|
+
All errors derive from `BLSError`, so you can catch broadly or narrowly:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from blsapi import BLSError, BLSValidationError, BLSAPIError, BLSHTTPError
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
df = client.get_series("BAD_ID", start_year=2024, end_year=2023)
|
|
175
|
+
except BLSValidationError:
|
|
176
|
+
... # bad input caught locally, before any network call
|
|
177
|
+
except BLSAPIError as e:
|
|
178
|
+
... # BLS returned a non-success status; inspect e.status and e.messages
|
|
179
|
+
except BLSHTTPError:
|
|
180
|
+
... # transport/HTTP failure that survived the retry loop
|
|
181
|
+
except BLSError:
|
|
182
|
+
... # anything else from this library
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Tiers & limits
|
|
186
|
+
|
|
187
|
+
| | Series per request | Years per request | Queries per day |
|
|
188
|
+
| --- | --- | --- | --- |
|
|
189
|
+
| **No key** | 25 | 10 | 25 |
|
|
190
|
+
| **With key** | 50 | 20 | 500 |
|
|
191
|
+
|
|
192
|
+
With `auto_batch=True` (the default), requests that exceed the per-request limits are split
|
|
193
|
+
into multiple calls automatically and stitched back together. Register for a free key at
|
|
194
|
+
<https://data.bls.gov/registrationEngine/>.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
uv sync # install dependencies
|
|
200
|
+
uv build # build the sdist + wheel
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bls-api-client"
|
|
3
|
+
description = "A sync + async client for the BLS Public Data API"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "covertcast", email = "covertcast@proton.me" }]
|
|
9
|
+
keywords = [
|
|
10
|
+
"bls",
|
|
11
|
+
"bureau-of-labor-statistics",
|
|
12
|
+
"economics",
|
|
13
|
+
"labor-statistics",
|
|
14
|
+
"polars",
|
|
15
|
+
"api-client",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Programming Language :: Python :: 3.14",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
dynamic = ["version"]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"httpx2>=2.4.0",
|
|
35
|
+
"polars>=1.42.0",
|
|
36
|
+
"pydantic>=2.13.4",
|
|
37
|
+
"anyio>=4.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/covertcast/blsapi"
|
|
42
|
+
Repository = "https://github.com/covertcast/blsapi"
|
|
43
|
+
Issues = "https://github.com/covertcast/blsapi/issues"
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["hatchling"]
|
|
47
|
+
build-backend = "hatchling.build"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version]
|
|
50
|
+
path = "src/blsapi/__about__.py"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["src/blsapi"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""blsapi - a sync + async client for the BLS Public Data API.
|
|
2
|
+
|
|
3
|
+
Quick start:
|
|
4
|
+
|
|
5
|
+
from blsapi import BLSClient
|
|
6
|
+
with BLSClient() as client: # reads BLS_API_KEY from env if set
|
|
7
|
+
df = client.get_series("LNS14000000", start_year=2023, end_year=2024)
|
|
8
|
+
|
|
9
|
+
# async
|
|
10
|
+
from blsapi import AsyncBLSClient
|
|
11
|
+
async with AsyncBLSClient() as client:
|
|
12
|
+
df = await client.get_series("LNS14000000", start_year=2023, end_year=2024)
|
|
13
|
+
|
|
14
|
+
The result is a Polars DataFrame. Exporting it to other formats is simple:
|
|
15
|
+
df.write_parquet(...), df.write_csv(...), df.to_pandas(), df.to_arrow()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .__about__ import __version__
|
|
19
|
+
from .aclient import AsyncBLSClient
|
|
20
|
+
from .client import BLSClient
|
|
21
|
+
from .enums import PeriodType
|
|
22
|
+
from .exceptions import (
|
|
23
|
+
BLSAPIError,
|
|
24
|
+
BLSError,
|
|
25
|
+
BLSHTTPError,
|
|
26
|
+
BLSValidationError,
|
|
27
|
+
)
|
|
28
|
+
from .frames import period_to_date, pivot_wide, series_to_frame
|
|
29
|
+
from .models import BLSResponse
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AsyncBLSClient",
|
|
33
|
+
"BLSClient",
|
|
34
|
+
"BLSError",
|
|
35
|
+
"BLSValidationError",
|
|
36
|
+
"BLSAPIError",
|
|
37
|
+
"BLSHTTPError",
|
|
38
|
+
"BLSResponse",
|
|
39
|
+
"PeriodType",
|
|
40
|
+
"series_to_frame",
|
|
41
|
+
"pivot_wide",
|
|
42
|
+
"period_to_date",
|
|
43
|
+
"__version__",
|
|
44
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Utilities for both the sync and async clients."""
|
|
2
|
+
|
|
3
|
+
from .models import BLSResponse, SeriesRequest, SeriesResult
|
|
4
|
+
from ._endpoints import RequestSpec, data_url
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from . import config
|
|
7
|
+
from .batching import chunk_series, year_windows
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_data_request(req: SeriesRequest, api_key: str | None, base_url: str) -> RequestSpec:
|
|
11
|
+
"""Turn a validated SeriesRequest into a RequestSpec."""
|
|
12
|
+
return RequestSpec(
|
|
13
|
+
method="POST",
|
|
14
|
+
url=data_url(base_url),
|
|
15
|
+
json=req.to_payload(api_key),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_data_response(payload: dict) -> BLSResponse:
|
|
20
|
+
"""Validate the payload. Raises typed errors on failure."""
|
|
21
|
+
return BLSResponse.model_validate(payload)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def plan_data_requests(
|
|
25
|
+
req: SeriesRequest, api_key: str | None, base_url: str, auto_batch: bool
|
|
26
|
+
) -> list[RequestSpec]:
|
|
27
|
+
"""One RequestSpec per API call, splitting over the tier limits when auto_batch is enabled."""
|
|
28
|
+
if not auto_batch:
|
|
29
|
+
return [build_data_request(req, api_key, base_url)]
|
|
30
|
+
|
|
31
|
+
series_limit, year_limit, _ = config.limits_for(api_key)
|
|
32
|
+
if req.start_year is not None and req.end_year is not None:
|
|
33
|
+
windows = list(year_windows(req.start_year, req.end_year, year_limit))
|
|
34
|
+
else:
|
|
35
|
+
windows = [(req.start_year, req.end_year)] # no year filter produces a single window
|
|
36
|
+
|
|
37
|
+
# every combination of series-chunk and year-window is one request.
|
|
38
|
+
return [
|
|
39
|
+
build_data_request(
|
|
40
|
+
req.model_copy(update={"series_ids": ids, "start_year": lo, "end_year": hi}),
|
|
41
|
+
api_key,
|
|
42
|
+
base_url,
|
|
43
|
+
)
|
|
44
|
+
for ids in chunk_series(req.series_ids, series_limit)
|
|
45
|
+
for lo, hi in windows
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def merge_series(series_lists: Iterable[list[SeriesResult]]) -> list[SeriesResult]:
|
|
50
|
+
"""Combine SeriesResults from several responses, concatenating .data by series_id."""
|
|
51
|
+
merged: dict[str, SeriesResult] = {}
|
|
52
|
+
for series in series_lists:
|
|
53
|
+
for s in series:
|
|
54
|
+
if s.series_id in merged:
|
|
55
|
+
merged[s.series_id].data.extend(s.data) # same series, another year-window
|
|
56
|
+
else:
|
|
57
|
+
merged[s.series_id] = s # first sighting
|
|
58
|
+
return list(merged.values())
|