usps-v3 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.
- usps_v3-1.0.0/.github/workflows/publish.yml +21 -0
- usps_v3-1.0.0/.github/workflows/test.yml +30 -0
- usps_v3-1.0.0/.gitignore +19 -0
- usps_v3-1.0.0/LICENSE +21 -0
- usps_v3-1.0.0/PKG-INFO +189 -0
- usps_v3-1.0.0/README.md +157 -0
- usps_v3-1.0.0/pyproject.toml +42 -0
- usps_v3-1.0.0/src/usps_v3/__init__.py +25 -0
- usps_v3-1.0.0/src/usps_v3/addresses.py +108 -0
- usps_v3-1.0.0/src/usps_v3/auth.py +267 -0
- usps_v3-1.0.0/src/usps_v3/client.py +129 -0
- usps_v3-1.0.0/src/usps_v3/exceptions.py +40 -0
- usps_v3-1.0.0/src/usps_v3/labels.py +294 -0
- usps_v3-1.0.0/src/usps_v3/locations.py +91 -0
- usps_v3-1.0.0/src/usps_v3/prices.py +216 -0
- usps_v3-1.0.0/src/usps_v3/standards.py +81 -0
- usps_v3-1.0.0/src/usps_v3/tracking.py +68 -0
- usps_v3-1.0.0/tests/conftest.py +88 -0
- usps_v3-1.0.0/tests/test_addresses.py +113 -0
- usps_v3-1.0.0/tests/test_auth.py +183 -0
- usps_v3-1.0.0/tests/test_client.py +88 -0
- usps_v3-1.0.0/tests/test_labels.py +118 -0
- usps_v3-1.0.0/tests/test_locations.py +78 -0
- usps_v3-1.0.0/tests/test_prices.py +108 -0
- usps_v3-1.0.0/tests/test_standards.py +62 -0
- usps_v3-1.0.0/tests/test_tracking.py +77 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: pip install build twine
|
|
17
|
+
- run: python -m build
|
|
18
|
+
- run: python -m twine upload dist/*
|
|
19
|
+
env:
|
|
20
|
+
TWINE_USERNAME: __token__
|
|
21
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: pip install -e ".[dev]"
|
|
24
|
+
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: pytest -v --tb=short
|
|
27
|
+
|
|
28
|
+
- name: Lint
|
|
29
|
+
if: matrix.python-version == '3.12'
|
|
30
|
+
run: ruff check src/ tests/
|
usps_v3-1.0.0/.gitignore
ADDED
usps_v3-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 James Lambert / Revasser LLC
|
|
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.
|
usps_v3-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usps-v3
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for USPS Web Tools v3 REST API — OAuth 2.0, address validation, tracking, labels, prices
|
|
5
|
+
Project-URL: Homepage, https://revaddress.com
|
|
6
|
+
Project-URL: Documentation, https://revaddress.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/revereveal/usps-v3
|
|
8
|
+
Project-URL: Issues, https://github.com/revereveal/usps-v3/issues
|
|
9
|
+
Author-email: James Lambert <james@revasser.nyc>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: address-validation,api,labels,postal,shipping,tracking,usps
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-httpx>=0.21; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# usps-v3
|
|
34
|
+
|
|
35
|
+
Python SDK for the **USPS Web Tools v3 REST API** — the replacement for the retired XML-based Web Tools.
|
|
36
|
+
|
|
37
|
+
Direct USPS integration. OAuth 2.0. No middleman. No per-label fees.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install usps-v3
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from usps_v3 import Client
|
|
49
|
+
|
|
50
|
+
# Credentials from USPS Business Customer Gateway
|
|
51
|
+
client = Client(client_id="your-id", client_secret="your-secret")
|
|
52
|
+
# Or set USPS_CLIENT_ID and USPS_CLIENT_SECRET environment variables:
|
|
53
|
+
# client = Client()
|
|
54
|
+
|
|
55
|
+
# Validate an address (FREE)
|
|
56
|
+
result = client.addresses.validate(
|
|
57
|
+
street_address="1600 Pennsylvania Ave NW",
|
|
58
|
+
city="Washington",
|
|
59
|
+
state="DC",
|
|
60
|
+
zip_code="20500",
|
|
61
|
+
)
|
|
62
|
+
print(result["address"]["ZIPPlus4"]) # "0005"
|
|
63
|
+
|
|
64
|
+
# Track a package (FREE)
|
|
65
|
+
info = client.tracking.track("9400111899223033005282")
|
|
66
|
+
print(info["statusCategory"]) # "Delivered"
|
|
67
|
+
|
|
68
|
+
# Get delivery time estimates (FREE)
|
|
69
|
+
standards = client.standards.estimates("10001", "90210")
|
|
70
|
+
|
|
71
|
+
# Find drop-off locations (FREE)
|
|
72
|
+
locations = client.locations.dropoff("20500", mail_class="PRIORITY_MAIL")
|
|
73
|
+
|
|
74
|
+
# Get rate quotes
|
|
75
|
+
rates = client.prices.domestic("10001", "90210", weight=2.5)
|
|
76
|
+
print(rates["rates"]["rateOptions"][0]["totalPrice"])
|
|
77
|
+
|
|
78
|
+
# International rates
|
|
79
|
+
intl = client.prices.international("10001", "CA", weight=3.0)
|
|
80
|
+
|
|
81
|
+
# Create shipping labels (requires USPS enrollment + COP claims)
|
|
82
|
+
label = client.labels.create(
|
|
83
|
+
from_address={"streetAddress": "123 Sender St", "city": "New York", "state": "NY", "ZIPCode": "10001"},
|
|
84
|
+
to_address={"streetAddress": "456 Recipient Ave", "city": "LA", "state": "CA", "ZIPCode": "90001"},
|
|
85
|
+
mail_class="PRIORITY_MAIL",
|
|
86
|
+
weight=2.0,
|
|
87
|
+
)
|
|
88
|
+
print(label["trackingNumber"])
|
|
89
|
+
|
|
90
|
+
# Void a label
|
|
91
|
+
client.labels.void("9400111899223033005282")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Features
|
|
95
|
+
|
|
96
|
+
| Feature | Endpoint | Auth Required |
|
|
97
|
+
|---------|----------|--------------|
|
|
98
|
+
| Address Validation | `addresses.validate()` | OAuth only |
|
|
99
|
+
| City/State Lookup | `addresses.city_state()` | OAuth only |
|
|
100
|
+
| Package Tracking | `tracking.track()` | OAuth only |
|
|
101
|
+
| Service Standards | `standards.estimates()` | OAuth only |
|
|
102
|
+
| Drop-off Locations | `locations.dropoff()` | OAuth only |
|
|
103
|
+
| Domestic Prices | `prices.domestic()` | OAuth only |
|
|
104
|
+
| International Prices | `prices.international()` | OAuth only |
|
|
105
|
+
| Label Creation | `labels.create()` | OAuth + Payment Auth |
|
|
106
|
+
| Label Void | `labels.void()` | OAuth only |
|
|
107
|
+
|
|
108
|
+
## Authentication
|
|
109
|
+
|
|
110
|
+
The SDK handles OAuth 2.0 token lifecycle automatically:
|
|
111
|
+
|
|
112
|
+
- **Token caching**: Tokens are cached in memory and on disk (`~/.usps-v3/tokens.json`)
|
|
113
|
+
- **Auto-refresh**: Tokens refresh automatically 30 minutes before expiry
|
|
114
|
+
- **Thread-safe**: Safe for concurrent use across threads
|
|
115
|
+
|
|
116
|
+
### Getting Credentials
|
|
117
|
+
|
|
118
|
+
1. Register at [USPS Business Customer Gateway](https://gateway.usps.com)
|
|
119
|
+
2. Create an application in the API developer portal
|
|
120
|
+
3. Note your `client_id` and `client_secret`
|
|
121
|
+
|
|
122
|
+
For label creation, you also need:
|
|
123
|
+
- **CRID** (Customer Registration ID)
|
|
124
|
+
- **MIDs** (Mailer IDs — master + label owner)
|
|
125
|
+
- **EPA** (Enterprise Payment Account)
|
|
126
|
+
- **COP claims linking** at [cop.usps.com](https://cop.usps.com)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
client = Client(
|
|
130
|
+
client_id="...",
|
|
131
|
+
client_secret="...",
|
|
132
|
+
crid="56982563",
|
|
133
|
+
master_mid="904128936",
|
|
134
|
+
label_mid="904128937",
|
|
135
|
+
epa_account="1000405525",
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Error Handling
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from usps_v3 import Client, AuthError, ValidationError, RateLimitError, APIError
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
result = client.addresses.validate(street_address="123 Main St")
|
|
146
|
+
except ValidationError as e:
|
|
147
|
+
print(f"Bad input: {e} (field: {e.field})")
|
|
148
|
+
except RateLimitError as e:
|
|
149
|
+
print(f"Rate limited — retry after {e.retry_after}s")
|
|
150
|
+
except AuthError as e:
|
|
151
|
+
print(f"Auth failed: {e}")
|
|
152
|
+
except APIError as e:
|
|
153
|
+
print(f"USPS error ({e.status_code}): {e}")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## USPS Rate Limits
|
|
157
|
+
|
|
158
|
+
The v3 API defaults to **60 requests/hour** (down from unlimited in Web Tools). The SDK does not enforce this limit — USPS returns 429 when exceeded.
|
|
159
|
+
|
|
160
|
+
To request a higher limit, contact USPS at [emailus.usps.com](https://emailus.usps.com).
|
|
161
|
+
|
|
162
|
+
## Migration from Web Tools
|
|
163
|
+
|
|
164
|
+
If you're migrating from the retired USPS Web Tools XML API:
|
|
165
|
+
|
|
166
|
+
| Web Tools (XML) | v3 SDK (Python) |
|
|
167
|
+
|-----------------|-----------------|
|
|
168
|
+
| `<AddressValidateRequest>` | `client.addresses.validate(...)` |
|
|
169
|
+
| `<CityStateLookupRequest>` | `client.addresses.city_state(...)` |
|
|
170
|
+
| `<TrackFieldRequest>` | `client.tracking.track(...)` |
|
|
171
|
+
| `<RateV4Request>` | `client.prices.domestic(...)` |
|
|
172
|
+
| User ID auth | OAuth 2.0 (automatic) |
|
|
173
|
+
| XML response parsing | Python dicts (automatic) |
|
|
174
|
+
| Unlimited requests | 60/hr default (request increase) |
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
git clone https://github.com/revereveal/usps-v3.git
|
|
180
|
+
cd usps-v3
|
|
181
|
+
pip install -e ".[dev]"
|
|
182
|
+
pytest -v
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT — see [LICENSE](LICENSE).
|
|
188
|
+
|
|
189
|
+
Built by [RevAddress](https://revaddress.com) — direct USPS API integration for developers.
|
usps_v3-1.0.0/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# usps-v3
|
|
2
|
+
|
|
3
|
+
Python SDK for the **USPS Web Tools v3 REST API** — the replacement for the retired XML-based Web Tools.
|
|
4
|
+
|
|
5
|
+
Direct USPS integration. OAuth 2.0. No middleman. No per-label fees.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install usps-v3
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from usps_v3 import Client
|
|
17
|
+
|
|
18
|
+
# Credentials from USPS Business Customer Gateway
|
|
19
|
+
client = Client(client_id="your-id", client_secret="your-secret")
|
|
20
|
+
# Or set USPS_CLIENT_ID and USPS_CLIENT_SECRET environment variables:
|
|
21
|
+
# client = Client()
|
|
22
|
+
|
|
23
|
+
# Validate an address (FREE)
|
|
24
|
+
result = client.addresses.validate(
|
|
25
|
+
street_address="1600 Pennsylvania Ave NW",
|
|
26
|
+
city="Washington",
|
|
27
|
+
state="DC",
|
|
28
|
+
zip_code="20500",
|
|
29
|
+
)
|
|
30
|
+
print(result["address"]["ZIPPlus4"]) # "0005"
|
|
31
|
+
|
|
32
|
+
# Track a package (FREE)
|
|
33
|
+
info = client.tracking.track("9400111899223033005282")
|
|
34
|
+
print(info["statusCategory"]) # "Delivered"
|
|
35
|
+
|
|
36
|
+
# Get delivery time estimates (FREE)
|
|
37
|
+
standards = client.standards.estimates("10001", "90210")
|
|
38
|
+
|
|
39
|
+
# Find drop-off locations (FREE)
|
|
40
|
+
locations = client.locations.dropoff("20500", mail_class="PRIORITY_MAIL")
|
|
41
|
+
|
|
42
|
+
# Get rate quotes
|
|
43
|
+
rates = client.prices.domestic("10001", "90210", weight=2.5)
|
|
44
|
+
print(rates["rates"]["rateOptions"][0]["totalPrice"])
|
|
45
|
+
|
|
46
|
+
# International rates
|
|
47
|
+
intl = client.prices.international("10001", "CA", weight=3.0)
|
|
48
|
+
|
|
49
|
+
# Create shipping labels (requires USPS enrollment + COP claims)
|
|
50
|
+
label = client.labels.create(
|
|
51
|
+
from_address={"streetAddress": "123 Sender St", "city": "New York", "state": "NY", "ZIPCode": "10001"},
|
|
52
|
+
to_address={"streetAddress": "456 Recipient Ave", "city": "LA", "state": "CA", "ZIPCode": "90001"},
|
|
53
|
+
mail_class="PRIORITY_MAIL",
|
|
54
|
+
weight=2.0,
|
|
55
|
+
)
|
|
56
|
+
print(label["trackingNumber"])
|
|
57
|
+
|
|
58
|
+
# Void a label
|
|
59
|
+
client.labels.void("9400111899223033005282")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
| Feature | Endpoint | Auth Required |
|
|
65
|
+
|---------|----------|--------------|
|
|
66
|
+
| Address Validation | `addresses.validate()` | OAuth only |
|
|
67
|
+
| City/State Lookup | `addresses.city_state()` | OAuth only |
|
|
68
|
+
| Package Tracking | `tracking.track()` | OAuth only |
|
|
69
|
+
| Service Standards | `standards.estimates()` | OAuth only |
|
|
70
|
+
| Drop-off Locations | `locations.dropoff()` | OAuth only |
|
|
71
|
+
| Domestic Prices | `prices.domestic()` | OAuth only |
|
|
72
|
+
| International Prices | `prices.international()` | OAuth only |
|
|
73
|
+
| Label Creation | `labels.create()` | OAuth + Payment Auth |
|
|
74
|
+
| Label Void | `labels.void()` | OAuth only |
|
|
75
|
+
|
|
76
|
+
## Authentication
|
|
77
|
+
|
|
78
|
+
The SDK handles OAuth 2.0 token lifecycle automatically:
|
|
79
|
+
|
|
80
|
+
- **Token caching**: Tokens are cached in memory and on disk (`~/.usps-v3/tokens.json`)
|
|
81
|
+
- **Auto-refresh**: Tokens refresh automatically 30 minutes before expiry
|
|
82
|
+
- **Thread-safe**: Safe for concurrent use across threads
|
|
83
|
+
|
|
84
|
+
### Getting Credentials
|
|
85
|
+
|
|
86
|
+
1. Register at [USPS Business Customer Gateway](https://gateway.usps.com)
|
|
87
|
+
2. Create an application in the API developer portal
|
|
88
|
+
3. Note your `client_id` and `client_secret`
|
|
89
|
+
|
|
90
|
+
For label creation, you also need:
|
|
91
|
+
- **CRID** (Customer Registration ID)
|
|
92
|
+
- **MIDs** (Mailer IDs — master + label owner)
|
|
93
|
+
- **EPA** (Enterprise Payment Account)
|
|
94
|
+
- **COP claims linking** at [cop.usps.com](https://cop.usps.com)
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
client = Client(
|
|
98
|
+
client_id="...",
|
|
99
|
+
client_secret="...",
|
|
100
|
+
crid="56982563",
|
|
101
|
+
master_mid="904128936",
|
|
102
|
+
label_mid="904128937",
|
|
103
|
+
epa_account="1000405525",
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Error Handling
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from usps_v3 import Client, AuthError, ValidationError, RateLimitError, APIError
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
result = client.addresses.validate(street_address="123 Main St")
|
|
114
|
+
except ValidationError as e:
|
|
115
|
+
print(f"Bad input: {e} (field: {e.field})")
|
|
116
|
+
except RateLimitError as e:
|
|
117
|
+
print(f"Rate limited — retry after {e.retry_after}s")
|
|
118
|
+
except AuthError as e:
|
|
119
|
+
print(f"Auth failed: {e}")
|
|
120
|
+
except APIError as e:
|
|
121
|
+
print(f"USPS error ({e.status_code}): {e}")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## USPS Rate Limits
|
|
125
|
+
|
|
126
|
+
The v3 API defaults to **60 requests/hour** (down from unlimited in Web Tools). The SDK does not enforce this limit — USPS returns 429 when exceeded.
|
|
127
|
+
|
|
128
|
+
To request a higher limit, contact USPS at [emailus.usps.com](https://emailus.usps.com).
|
|
129
|
+
|
|
130
|
+
## Migration from Web Tools
|
|
131
|
+
|
|
132
|
+
If you're migrating from the retired USPS Web Tools XML API:
|
|
133
|
+
|
|
134
|
+
| Web Tools (XML) | v3 SDK (Python) |
|
|
135
|
+
|-----------------|-----------------|
|
|
136
|
+
| `<AddressValidateRequest>` | `client.addresses.validate(...)` |
|
|
137
|
+
| `<CityStateLookupRequest>` | `client.addresses.city_state(...)` |
|
|
138
|
+
| `<TrackFieldRequest>` | `client.tracking.track(...)` |
|
|
139
|
+
| `<RateV4Request>` | `client.prices.domestic(...)` |
|
|
140
|
+
| User ID auth | OAuth 2.0 (automatic) |
|
|
141
|
+
| XML response parsing | Python dicts (automatic) |
|
|
142
|
+
| Unlimited requests | 60/hr default (request increase) |
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
git clone https://github.com/revereveal/usps-v3.git
|
|
148
|
+
cd usps-v3
|
|
149
|
+
pip install -e ".[dev]"
|
|
150
|
+
pytest -v
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT — see [LICENSE](LICENSE).
|
|
156
|
+
|
|
157
|
+
Built by [RevAddress](https://revaddress.com) — direct USPS API integration for developers.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "usps-v3"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for USPS Web Tools v3 REST API — OAuth 2.0, address validation, tracking, labels, prices"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "James Lambert", email = "james@revasser.nyc" }]
|
|
13
|
+
keywords = ["usps", "shipping", "address-validation", "tracking", "labels", "postal", "api"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Topic :: Office/Business :: Financial :: Point-Of-Sale",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["httpx>=0.24.0"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://revaddress.com"
|
|
31
|
+
Documentation = "https://revaddress.com/docs"
|
|
32
|
+
Repository = "https://github.com/revereveal/usps-v3"
|
|
33
|
+
Issues = "https://github.com/revereveal/usps-v3/issues"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/usps_v3"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = ["pytest>=7.0", "pytest-httpx>=0.21", "respx>=0.20", "ruff>=0.3"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""USPS v3 Python SDK — direct integration with USPS Web Tools v3 REST API.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from usps_v3 import Client
|
|
5
|
+
|
|
6
|
+
client = Client(client_id="...", client_secret="...")
|
|
7
|
+
result = client.addresses.validate(street_address="1600 Pennsylvania Ave NW", city="Washington", state="DC")
|
|
8
|
+
|
|
9
|
+
Or with environment variables (USPS_CLIENT_ID, USPS_CLIENT_SECRET):
|
|
10
|
+
client = Client()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .client import Client
|
|
14
|
+
from .exceptions import APIError, AuthError, NetworkError, RateLimitError, USPSError, ValidationError
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Client",
|
|
19
|
+
"USPSError",
|
|
20
|
+
"AuthError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"RateLimitError",
|
|
23
|
+
"APIError",
|
|
24
|
+
"NetworkError",
|
|
25
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Address validation and city/state lookup — USPS Addresses v3.
|
|
2
|
+
|
|
3
|
+
Free tier: no license required.
|
|
4
|
+
|
|
5
|
+
USPS endpoints:
|
|
6
|
+
GET /addresses/v3/address?streetAddress=...&city=...&state=...&ZIPCode=...
|
|
7
|
+
GET /addresses/v3/city-state?ZIPCode=...
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from .auth import TokenManager
|
|
17
|
+
from .exceptions import APIError, NetworkError, RateLimitError, ValidationError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AddressesAPI:
|
|
21
|
+
"""Address validation and ZIP code lookup."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, http: httpx.Client, tokens: TokenManager, base_url: str):
|
|
24
|
+
self._http = http
|
|
25
|
+
self._tokens = tokens
|
|
26
|
+
self._base_url = base_url
|
|
27
|
+
|
|
28
|
+
def validate(
|
|
29
|
+
self,
|
|
30
|
+
street_address: str,
|
|
31
|
+
*,
|
|
32
|
+
secondary_address: str | None = None,
|
|
33
|
+
city: str | None = None,
|
|
34
|
+
state: str | None = None,
|
|
35
|
+
zip_code: str | None = None,
|
|
36
|
+
zip_plus4: str | None = None,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""Validate and standardize a US address.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
street_address: Street address line (required).
|
|
42
|
+
secondary_address: Apt, Suite, etc.
|
|
43
|
+
city: City name.
|
|
44
|
+
state: 2-letter state code.
|
|
45
|
+
zip_code: 5-digit ZIP code.
|
|
46
|
+
zip_plus4: 4-digit ZIP+4 extension.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dict with 'address', 'additionalInfo', 'corrections', 'matches' keys.
|
|
50
|
+
The 'address' dict contains the standardized address fields.
|
|
51
|
+
"""
|
|
52
|
+
if not street_address:
|
|
53
|
+
raise ValidationError("street_address is required", field="street_address")
|
|
54
|
+
|
|
55
|
+
params: dict[str, str] = {"streetAddress": street_address}
|
|
56
|
+
if secondary_address:
|
|
57
|
+
params["secondaryAddress"] = secondary_address
|
|
58
|
+
if city:
|
|
59
|
+
params["city"] = city
|
|
60
|
+
if state:
|
|
61
|
+
params["state"] = state
|
|
62
|
+
if zip_code:
|
|
63
|
+
params["ZIPCode"] = zip_code
|
|
64
|
+
if zip_plus4:
|
|
65
|
+
params["ZIPPlus4"] = zip_plus4
|
|
66
|
+
|
|
67
|
+
return self._request("GET", "/addresses/v3/address", params=params)
|
|
68
|
+
|
|
69
|
+
def city_state(self, zip_code: str) -> dict[str, Any]:
|
|
70
|
+
"""Look up city and state for a ZIP code.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
zip_code: 5-digit ZIP code.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dict with city and state information.
|
|
77
|
+
"""
|
|
78
|
+
if not zip_code:
|
|
79
|
+
raise ValidationError("zip_code is required", field="zip_code")
|
|
80
|
+
|
|
81
|
+
return self._request("GET", "/addresses/v3/city-state", params={"ZIPCode": zip_code})
|
|
82
|
+
|
|
83
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
84
|
+
token = self._tokens.get_oauth_token()
|
|
85
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
resp = self._http.request(
|
|
89
|
+
method,
|
|
90
|
+
f"{self._base_url}{path}",
|
|
91
|
+
headers=headers,
|
|
92
|
+
**kwargs,
|
|
93
|
+
)
|
|
94
|
+
except httpx.HTTPError as e:
|
|
95
|
+
raise NetworkError(f"Request failed: {e}") from e
|
|
96
|
+
|
|
97
|
+
if resp.status_code == 429:
|
|
98
|
+
retry_after = resp.headers.get("Retry-After")
|
|
99
|
+
raise RateLimitError(retry_after=int(retry_after) if retry_after else None)
|
|
100
|
+
|
|
101
|
+
if resp.status_code >= 400:
|
|
102
|
+
raise APIError(
|
|
103
|
+
f"USPS API error ({resp.status_code}): {resp.text}",
|
|
104
|
+
status_code=resp.status_code,
|
|
105
|
+
response_body=resp.text,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return resp.json()
|