salestax-python 0.1.0__tar.gz → 0.2.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.
- salestax_python-0.2.0/CHANGELOG.md +47 -0
- salestax_python-0.2.0/PKG-INFO +287 -0
- salestax_python-0.2.0/README.md +249 -0
- salestax_python-0.2.0/examples/async_usage.py +47 -0
- salestax_python-0.2.0/examples/basic_calculation.py +38 -0
- salestax_python-0.2.0/examples/batch_calculation.py +39 -0
- salestax_python-0.2.0/examples/check_coverage.py +17 -0
- salestax_python-0.2.0/examples/error_handling.py +27 -0
- salestax_python-0.2.0/examples/transaction_and_adjustment.py +41 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/pyproject.toml +8 -21
- salestax_python-0.2.0/salestax/__init__.py +115 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_config.py +6 -12
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_transport/http_client.py +34 -39
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_transport/types.py +2 -22
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_transport/urllib_transport.py +8 -10
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_version.py +1 -1
- salestax_python-0.2.0/salestax/client.py +303 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/errors.py +43 -44
- salestax_python-0.2.0/salestax/models.py +326 -0
- salestax_python-0.2.0/salestax/resources/__init__.py +11 -0
- salestax_python-0.2.0/salestax/resources/_validation.py +51 -0
- salestax_python-0.2.0/salestax/resources/batches.py +27 -0
- salestax_python-0.2.0/salestax/resources/calculations.py +119 -0
- salestax_python-0.2.0/salestax/resources/coverage.py +47 -0
- salestax_python-0.2.0/salestax/resources/transactions.py +146 -0
- salestax_python-0.2.0/tests/unit/test_adjustments.py +100 -0
- salestax_python-0.2.0/tests/unit/test_async.py +152 -0
- salestax_python-0.2.0/tests/unit/test_batches.py +21 -0
- salestax_python-0.2.0/tests/unit/test_calculations.py +146 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/unit/test_client.py +15 -2
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/unit/test_client_extras.py +39 -47
- salestax_python-0.2.0/tests/unit/test_coverage.py +54 -0
- salestax_python-0.2.0/tests/unit/test_errors.py +85 -0
- salestax_python-0.2.0/tests/unit/test_hooks.py +66 -0
- salestax_python-0.2.0/tests/unit/test_transactions.py +43 -0
- salestax_python-0.2.0/tests/unit/test_transport.py +117 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/unit/test_urllib_transport.py +3 -3
- salestax_python-0.1.0/CHANGELOG.md +0 -21
- salestax_python-0.1.0/PKG-INFO +0 -158
- salestax_python-0.1.0/README.md +0 -120
- salestax_python-0.1.0/examples/async_usage.py +0 -20
- salestax_python-0.1.0/examples/basic_calculation.py +0 -13
- salestax_python-0.1.0/examples/batch_calculation.py +0 -17
- salestax_python-0.1.0/examples/error_handling.py +0 -18
- salestax_python-0.1.0/salestax/__init__.py +0 -61
- salestax_python-0.1.0/salestax/client.py +0 -215
- salestax_python-0.1.0/salestax/models.py +0 -80
- salestax_python-0.1.0/salestax/resources/__init__.py +0 -5
- salestax_python-0.1.0/salestax/resources/jurisdictions.py +0 -27
- salestax_python-0.1.0/salestax/resources/rates.py +0 -25
- salestax_python-0.1.0/salestax/resources/tax.py +0 -146
- salestax_python-0.1.0/salestax/utils.py +0 -22
- salestax_python-0.1.0/tests/unit/test_async.py +0 -117
- salestax_python-0.1.0/tests/unit/test_errors.py +0 -50
- salestax_python-0.1.0/tests/unit/test_hooks.py +0 -52
- salestax_python-0.1.0/tests/unit/test_tax.py +0 -72
- salestax_python-0.1.0/tests/unit/test_transport.py +0 -110
- salestax_python-0.1.0/tests/unit/test_utils.py +0 -58
- {salestax_python-0.1.0 → salestax_python-0.2.0}/.gitignore +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/LICENSE +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_transport/__init__.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_transport/httpx_transport.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_transport/retry.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/_transport/user_agent.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/salestax/py.typed +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/__init__.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/conftest.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/integration/__init__.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/integration/test_live.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/unit/__init__.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/unit/test_httpx_transport.py +0 -0
- {salestax_python-0.1.0 → salestax_python-0.2.0}/tests/unit/test_retry.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
**BREAKING:** Redesigned the SDK to match the public OpenAPI contract.
|
|
12
|
+
|
|
13
|
+
- Replaced `client.tax`, `client.rates`, `client.jurisdictions` with
|
|
14
|
+
`client.calculations`, `client.transactions`, `client.transactions.adjustments`,
|
|
15
|
+
`client.batches`, and `client.coverage`.
|
|
16
|
+
- Request and response shapes now use snake_case matching the OpenAPI spec.
|
|
17
|
+
- Money and quantity are decimal strings, matching the wire format.
|
|
18
|
+
- Idempotency-Key is validated client-side against `[A-Za-z0-9][A-Za-z0-9._:-]{7,254}`.
|
|
19
|
+
- Error bodies are parsed as RFC 9457 problem+json. Field-level errors
|
|
20
|
+
populate `err.param` from `errors[0].pointer`.
|
|
21
|
+
- Base URL is now `https://api.salestaxcalculatorapi.com` (no `/v1` suffix).
|
|
22
|
+
- Added `expand="audit"` request option on all readable endpoints.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `client.transactions` and `client.transactions.adjustments` resources.
|
|
27
|
+
- `client.batches` resource for async batch polling.
|
|
28
|
+
- `client.coverage` resource for pre-calculation qualification checks.
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
|
|
32
|
+
- `client.tax.calculate`, `client.tax.calculate_batch`, `client.tax.calculate_batch_chunked`
|
|
33
|
+
- `client.rates.get`
|
|
34
|
+
- `client.jurisdictions.list`
|
|
35
|
+
- `salestax/utils.py` (chunk helper no longer needed)
|
|
36
|
+
|
|
37
|
+
## [0.1.0] - 2026-09-22
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- Initial release.
|
|
42
|
+
- `SalesTaxClient` (sync) with zero runtime dependencies.
|
|
43
|
+
- `AsyncSalesTaxClient` (async, requires `[async]` extra).
|
|
44
|
+
- Automatic retries with exponential backoff, jitter, and `Retry-After` support.
|
|
45
|
+
- Full error hierarchy: `RateLimitError`, `ValidationError`, `TimeoutError`, and more.
|
|
46
|
+
- Lifecycle hooks: `on_request`, `on_response`, `on_retry`.
|
|
47
|
+
- PEP 561 `py.typed` marker; `mypy --strict` clean.
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: salestax-python
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for the Sales Tax Calculator API.
|
|
5
|
+
Project-URL: Homepage, https://salestaxcalculatorapi.com
|
|
6
|
+
Project-URL: Documentation, https://salestaxcalculatorapi.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/InstaBlick-USA/salestax-python
|
|
8
|
+
Project-URL: Issues, https://github.com/InstaBlick-USA/salestax-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/InstaBlick-USA/salestax-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: InstaBlick USA <dev@salestaxcalculatorapi.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api-client,async,checkout,ecommerce,gst,hst,python,sales-tax,tax-api,vat
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
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 :: Accounting
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Provides-Extra: async
|
|
27
|
+
Requires-Dist: httpx>=0.27; extra == 'async'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
30
|
+
Requires-Dist: mypy>=1.9; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
36
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# salestax-python
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/salestax-python/)
|
|
42
|
+
[](https://pypi.org/project/salestax-python/)
|
|
43
|
+
[](https://github.com/InstaBlick-USA/salestax-python/actions)
|
|
44
|
+
[](./LICENSE)
|
|
45
|
+
|
|
46
|
+
Official Python SDK for the **[Sales Tax Calculator API](https://salestaxcalculatorapi.com)** — immutable sales tax calculations, transactions, adjustments, batches, and coverage checks.
|
|
47
|
+
|
|
48
|
+
Zero runtime dependencies for the sync client. Full type hints.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install salestax-python
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Async support:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install salestax-python[async]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quickstart
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from salestax import SalesTaxClient
|
|
66
|
+
|
|
67
|
+
with SalesTaxClient() as client: # reads SALESTAX_API_KEY
|
|
68
|
+
calc = client.calculations.create(
|
|
69
|
+
currency="CAD",
|
|
70
|
+
tax_behavior="exclusive",
|
|
71
|
+
billing_event="subscription_start",
|
|
72
|
+
seller={
|
|
73
|
+
"country": "CA",
|
|
74
|
+
"channel_role": "direct_legal_supplier",
|
|
75
|
+
"registrations": [
|
|
76
|
+
{"country": "CA", "state": "ON", "type": "gst_hst", "effective_from": "2026-01-01"},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
customer={
|
|
80
|
+
"type": "consumer",
|
|
81
|
+
"address": {"country": "CA", "state": "ON", "postal_code": "M5V 2T6"},
|
|
82
|
+
},
|
|
83
|
+
lines=[
|
|
84
|
+
{"reference": "subscription", "amount": "100.00", "quantity": "1", "tax_code": "saas"},
|
|
85
|
+
],
|
|
86
|
+
idempotency_key="order-1001-abc12345",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(calc["outcome"]) # 'calculated'
|
|
90
|
+
print(calc["tax"]) # '13.00'
|
|
91
|
+
print(calc["total"]) # '113.00'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Get your API key at **[salestaxcalculatorapi.com](https://salestaxcalculatorapi.com)**
|
|
95
|
+
|
|
96
|
+
## Resources
|
|
97
|
+
|
|
98
|
+
| Resource | Methods |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `client.calculations` | `create`, `get`, `create_batch`, `get_batch` |
|
|
101
|
+
| `client.transactions` | `create`, `get` |
|
|
102
|
+
| `client.transactions.adjustments` | `create`, `list`, `get` |
|
|
103
|
+
| `client.batches` | `get` |
|
|
104
|
+
| `client.coverage` | `check` |
|
|
105
|
+
|
|
106
|
+
## Transactions
|
|
107
|
+
|
|
108
|
+
Finalize a calculation as an immutable commercial record:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
txn = client.transactions.create(
|
|
112
|
+
calculation_id=calc["id"],
|
|
113
|
+
reference="order-1001",
|
|
114
|
+
idempotency_key="order-1001-txn-001",
|
|
115
|
+
)
|
|
116
|
+
print(txn["adjusted_amount"], txn["adjusted_tax"])
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Adjustments
|
|
120
|
+
|
|
121
|
+
Reverse part of a transaction by exact amount or proportional quantity:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
adj = client.transactions.adjustments.create(
|
|
125
|
+
txn["id"],
|
|
126
|
+
reference="refund-order-1001",
|
|
127
|
+
reason="refund",
|
|
128
|
+
lines=[{"line_id": "line_01J6...", "amount": "25.00"}],
|
|
129
|
+
idempotency_key="refund-order-1001-001",
|
|
130
|
+
)
|
|
131
|
+
print(adj["tax"])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Batches
|
|
135
|
+
|
|
136
|
+
Process up to 100 calculations asynchronously:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
batch = client.calculations.create_batch(
|
|
140
|
+
[calc_a, calc_b, calc_c],
|
|
141
|
+
reference="batch-2026-09-23",
|
|
142
|
+
idempotency_key="batch-2026-09-23-xyz",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
status = client.batches.get(batch["id"])
|
|
146
|
+
for item in status.get("items", []):
|
|
147
|
+
print(item["index"], item["status"])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Coverage
|
|
151
|
+
|
|
152
|
+
Check qualification before you calculate:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
coverage = client.coverage.check(
|
|
156
|
+
country="CA",
|
|
157
|
+
state="ON",
|
|
158
|
+
tax_code="saas",
|
|
159
|
+
transaction_type="sale",
|
|
160
|
+
)
|
|
161
|
+
print(coverage["qualification"]) # 'qualified' | 'review_required' | 'unsupported'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Async
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import asyncio
|
|
168
|
+
from salestax import AsyncSalesTaxClient
|
|
169
|
+
|
|
170
|
+
async def main():
|
|
171
|
+
async with AsyncSalesTaxClient() as client:
|
|
172
|
+
calc = await client.calculations.create(
|
|
173
|
+
currency="CAD",
|
|
174
|
+
tax_behavior="exclusive",
|
|
175
|
+
billing_event="subscription_start",
|
|
176
|
+
seller={
|
|
177
|
+
"country": "CA",
|
|
178
|
+
"channel_role": "direct_legal_supplier",
|
|
179
|
+
"registrations": [
|
|
180
|
+
{"country": "CA", "state": "ON", "type": "gst_hst",
|
|
181
|
+
"effective_from": "2026-01-01"},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
customer={
|
|
185
|
+
"type": "consumer",
|
|
186
|
+
"address": {"country": "CA", "state": "ON", "postal_code": "M5V 2T6"},
|
|
187
|
+
},
|
|
188
|
+
lines=[{"reference": "sub", "amount": "100.00", "tax_code": "saas"}],
|
|
189
|
+
idempotency_key="order-1001-abc12345",
|
|
190
|
+
)
|
|
191
|
+
print(calc["tax"])
|
|
192
|
+
|
|
193
|
+
asyncio.run(main())
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Audit expansion
|
|
197
|
+
|
|
198
|
+
Request the customer-safe audit expansion on any calculable resource:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
calc = client.calculations.create(
|
|
202
|
+
# ...same params as above...
|
|
203
|
+
expand="audit",
|
|
204
|
+
)
|
|
205
|
+
print(calc.get("audit", {}).get("components"))
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Error Handling
|
|
209
|
+
|
|
210
|
+
Every status code maps to a typed error class. Field-level errors carry a JSON Pointer in `err.param`.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from salestax import RateLimitError, SalesTaxError, ValidationError
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
client.calculations.create(...)
|
|
217
|
+
except RateLimitError as err:
|
|
218
|
+
# err.retry_after_ms - back off
|
|
219
|
+
# err.request_id
|
|
220
|
+
...
|
|
221
|
+
except ValidationError as err:
|
|
222
|
+
# err.param - JSON Pointer to the offending field, e.g. '/lines/0/amount'
|
|
223
|
+
...
|
|
224
|
+
except SalesTaxError as err:
|
|
225
|
+
# err.code, err.request_id, err.retryable, err.status_code
|
|
226
|
+
...
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Idempotency
|
|
230
|
+
|
|
231
|
+
Every mutating request accepts an `Idempotency-Key`. Reuse the same key when retrying the same operation. The key must be 8-255 characters matching `[A-Za-z0-9][A-Za-z0-9._:-]{7,254}`.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
client.calculations.create(..., idempotency_key="order-1001-abc12345")
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Reusing a key with a different request body returns `409 idempotency_conflict`.
|
|
238
|
+
|
|
239
|
+
## Configuration
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from salestax import RetryPolicy, SalesTaxClient
|
|
243
|
+
|
|
244
|
+
client = SalesTaxClient(
|
|
245
|
+
api_key="stca_...",
|
|
246
|
+
timeout_ms=20_000,
|
|
247
|
+
retry=RetryPolicy(
|
|
248
|
+
max_retries=4,
|
|
249
|
+
initial_delay_ms=250,
|
|
250
|
+
max_delay_ms=8_000,
|
|
251
|
+
backoff_factor=2.0,
|
|
252
|
+
jitter=True,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Defaults
|
|
258
|
+
|
|
259
|
+
| Option | Default |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `base_url` | `https://api.salestaxcalculatorapi.com` |
|
|
262
|
+
| `timeout_ms` | `30000` |
|
|
263
|
+
| `retry.max_retries` | `2` |
|
|
264
|
+
| `retry.initial_delay_ms` | `250` |
|
|
265
|
+
| `retry.max_delay_ms` | `8000` |
|
|
266
|
+
| `retry.backoff_factor` | `2.0` |
|
|
267
|
+
| `retry.jitter` | `True` |
|
|
268
|
+
|
|
269
|
+
## Behavior
|
|
270
|
+
|
|
271
|
+
- **Retries** — 5xx, network failures, and 429 responses are retried automatically with exponential backoff and full jitter. `Retry-After` and `retry_after_seconds` are honored when present. 4xx responses other than 429 are returned to the caller unchanged.
|
|
272
|
+
- **Timeouts** — each request is aborted after `timeout_ms` and surfaced as a `TimeoutError`.
|
|
273
|
+
- **Hooks** — throwing inside a hook never breaks the request flow. The exception is swallowed and the SDK continues.
|
|
274
|
+
- **Money and quantity** — always decimal strings matching the OpenAPI `Money` and `Quantity` patterns. `amount` is the total for the line, not a per-unit price.
|
|
275
|
+
- **Unknown fields** — the API rejects unknown JSON fields. The SDK passes the request body through unchanged so server-side validation is authoritative.
|
|
276
|
+
|
|
277
|
+
## Also Available For
|
|
278
|
+
|
|
279
|
+
- [salestax-node](https://github.com/InstaBlick-USA/salestax-node) — Node.js / TypeScript
|
|
280
|
+
|
|
281
|
+
## Documentation
|
|
282
|
+
|
|
283
|
+
Full API reference at **[salestaxcalculatorapi.com/docs](https://salestaxcalculatorapi.com/docs)**
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT (c) [InstaBlick USA](https://github.com/InstaBlick-USA)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# salestax-python
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/salestax-python/)
|
|
4
|
+
[](https://pypi.org/project/salestax-python/)
|
|
5
|
+
[](https://github.com/InstaBlick-USA/salestax-python/actions)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
Official Python SDK for the **[Sales Tax Calculator API](https://salestaxcalculatorapi.com)** — immutable sales tax calculations, transactions, adjustments, batches, and coverage checks.
|
|
9
|
+
|
|
10
|
+
Zero runtime dependencies for the sync client. Full type hints.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install salestax-python
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Async support:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install salestax-python[async]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from salestax import SalesTaxClient
|
|
28
|
+
|
|
29
|
+
with SalesTaxClient() as client: # reads SALESTAX_API_KEY
|
|
30
|
+
calc = client.calculations.create(
|
|
31
|
+
currency="CAD",
|
|
32
|
+
tax_behavior="exclusive",
|
|
33
|
+
billing_event="subscription_start",
|
|
34
|
+
seller={
|
|
35
|
+
"country": "CA",
|
|
36
|
+
"channel_role": "direct_legal_supplier",
|
|
37
|
+
"registrations": [
|
|
38
|
+
{"country": "CA", "state": "ON", "type": "gst_hst", "effective_from": "2026-01-01"},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
customer={
|
|
42
|
+
"type": "consumer",
|
|
43
|
+
"address": {"country": "CA", "state": "ON", "postal_code": "M5V 2T6"},
|
|
44
|
+
},
|
|
45
|
+
lines=[
|
|
46
|
+
{"reference": "subscription", "amount": "100.00", "quantity": "1", "tax_code": "saas"},
|
|
47
|
+
],
|
|
48
|
+
idempotency_key="order-1001-abc12345",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
print(calc["outcome"]) # 'calculated'
|
|
52
|
+
print(calc["tax"]) # '13.00'
|
|
53
|
+
print(calc["total"]) # '113.00'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Get your API key at **[salestaxcalculatorapi.com](https://salestaxcalculatorapi.com)**
|
|
57
|
+
|
|
58
|
+
## Resources
|
|
59
|
+
|
|
60
|
+
| Resource | Methods |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `client.calculations` | `create`, `get`, `create_batch`, `get_batch` |
|
|
63
|
+
| `client.transactions` | `create`, `get` |
|
|
64
|
+
| `client.transactions.adjustments` | `create`, `list`, `get` |
|
|
65
|
+
| `client.batches` | `get` |
|
|
66
|
+
| `client.coverage` | `check` |
|
|
67
|
+
|
|
68
|
+
## Transactions
|
|
69
|
+
|
|
70
|
+
Finalize a calculation as an immutable commercial record:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
txn = client.transactions.create(
|
|
74
|
+
calculation_id=calc["id"],
|
|
75
|
+
reference="order-1001",
|
|
76
|
+
idempotency_key="order-1001-txn-001",
|
|
77
|
+
)
|
|
78
|
+
print(txn["adjusted_amount"], txn["adjusted_tax"])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Adjustments
|
|
82
|
+
|
|
83
|
+
Reverse part of a transaction by exact amount or proportional quantity:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
adj = client.transactions.adjustments.create(
|
|
87
|
+
txn["id"],
|
|
88
|
+
reference="refund-order-1001",
|
|
89
|
+
reason="refund",
|
|
90
|
+
lines=[{"line_id": "line_01J6...", "amount": "25.00"}],
|
|
91
|
+
idempotency_key="refund-order-1001-001",
|
|
92
|
+
)
|
|
93
|
+
print(adj["tax"])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Batches
|
|
97
|
+
|
|
98
|
+
Process up to 100 calculations asynchronously:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
batch = client.calculations.create_batch(
|
|
102
|
+
[calc_a, calc_b, calc_c],
|
|
103
|
+
reference="batch-2026-09-23",
|
|
104
|
+
idempotency_key="batch-2026-09-23-xyz",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
status = client.batches.get(batch["id"])
|
|
108
|
+
for item in status.get("items", []):
|
|
109
|
+
print(item["index"], item["status"])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Coverage
|
|
113
|
+
|
|
114
|
+
Check qualification before you calculate:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
coverage = client.coverage.check(
|
|
118
|
+
country="CA",
|
|
119
|
+
state="ON",
|
|
120
|
+
tax_code="saas",
|
|
121
|
+
transaction_type="sale",
|
|
122
|
+
)
|
|
123
|
+
print(coverage["qualification"]) # 'qualified' | 'review_required' | 'unsupported'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Async
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import asyncio
|
|
130
|
+
from salestax import AsyncSalesTaxClient
|
|
131
|
+
|
|
132
|
+
async def main():
|
|
133
|
+
async with AsyncSalesTaxClient() as client:
|
|
134
|
+
calc = await client.calculations.create(
|
|
135
|
+
currency="CAD",
|
|
136
|
+
tax_behavior="exclusive",
|
|
137
|
+
billing_event="subscription_start",
|
|
138
|
+
seller={
|
|
139
|
+
"country": "CA",
|
|
140
|
+
"channel_role": "direct_legal_supplier",
|
|
141
|
+
"registrations": [
|
|
142
|
+
{"country": "CA", "state": "ON", "type": "gst_hst",
|
|
143
|
+
"effective_from": "2026-01-01"},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
customer={
|
|
147
|
+
"type": "consumer",
|
|
148
|
+
"address": {"country": "CA", "state": "ON", "postal_code": "M5V 2T6"},
|
|
149
|
+
},
|
|
150
|
+
lines=[{"reference": "sub", "amount": "100.00", "tax_code": "saas"}],
|
|
151
|
+
idempotency_key="order-1001-abc12345",
|
|
152
|
+
)
|
|
153
|
+
print(calc["tax"])
|
|
154
|
+
|
|
155
|
+
asyncio.run(main())
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Audit expansion
|
|
159
|
+
|
|
160
|
+
Request the customer-safe audit expansion on any calculable resource:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
calc = client.calculations.create(
|
|
164
|
+
# ...same params as above...
|
|
165
|
+
expand="audit",
|
|
166
|
+
)
|
|
167
|
+
print(calc.get("audit", {}).get("components"))
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Error Handling
|
|
171
|
+
|
|
172
|
+
Every status code maps to a typed error class. Field-level errors carry a JSON Pointer in `err.param`.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from salestax import RateLimitError, SalesTaxError, ValidationError
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
client.calculations.create(...)
|
|
179
|
+
except RateLimitError as err:
|
|
180
|
+
# err.retry_after_ms - back off
|
|
181
|
+
# err.request_id
|
|
182
|
+
...
|
|
183
|
+
except ValidationError as err:
|
|
184
|
+
# err.param - JSON Pointer to the offending field, e.g. '/lines/0/amount'
|
|
185
|
+
...
|
|
186
|
+
except SalesTaxError as err:
|
|
187
|
+
# err.code, err.request_id, err.retryable, err.status_code
|
|
188
|
+
...
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Idempotency
|
|
192
|
+
|
|
193
|
+
Every mutating request accepts an `Idempotency-Key`. Reuse the same key when retrying the same operation. The key must be 8-255 characters matching `[A-Za-z0-9][A-Za-z0-9._:-]{7,254}`.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
client.calculations.create(..., idempotency_key="order-1001-abc12345")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Reusing a key with a different request body returns `409 idempotency_conflict`.
|
|
200
|
+
|
|
201
|
+
## Configuration
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from salestax import RetryPolicy, SalesTaxClient
|
|
205
|
+
|
|
206
|
+
client = SalesTaxClient(
|
|
207
|
+
api_key="stca_...",
|
|
208
|
+
timeout_ms=20_000,
|
|
209
|
+
retry=RetryPolicy(
|
|
210
|
+
max_retries=4,
|
|
211
|
+
initial_delay_ms=250,
|
|
212
|
+
max_delay_ms=8_000,
|
|
213
|
+
backoff_factor=2.0,
|
|
214
|
+
jitter=True,
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Defaults
|
|
220
|
+
|
|
221
|
+
| Option | Default |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `base_url` | `https://api.salestaxcalculatorapi.com` |
|
|
224
|
+
| `timeout_ms` | `30000` |
|
|
225
|
+
| `retry.max_retries` | `2` |
|
|
226
|
+
| `retry.initial_delay_ms` | `250` |
|
|
227
|
+
| `retry.max_delay_ms` | `8000` |
|
|
228
|
+
| `retry.backoff_factor` | `2.0` |
|
|
229
|
+
| `retry.jitter` | `True` |
|
|
230
|
+
|
|
231
|
+
## Behavior
|
|
232
|
+
|
|
233
|
+
- **Retries** — 5xx, network failures, and 429 responses are retried automatically with exponential backoff and full jitter. `Retry-After` and `retry_after_seconds` are honored when present. 4xx responses other than 429 are returned to the caller unchanged.
|
|
234
|
+
- **Timeouts** — each request is aborted after `timeout_ms` and surfaced as a `TimeoutError`.
|
|
235
|
+
- **Hooks** — throwing inside a hook never breaks the request flow. The exception is swallowed and the SDK continues.
|
|
236
|
+
- **Money and quantity** — always decimal strings matching the OpenAPI `Money` and `Quantity` patterns. `amount` is the total for the line, not a per-unit price.
|
|
237
|
+
- **Unknown fields** — the API rejects unknown JSON fields. The SDK passes the request body through unchanged so server-side validation is authoritative.
|
|
238
|
+
|
|
239
|
+
## Also Available For
|
|
240
|
+
|
|
241
|
+
- [salestax-node](https://github.com/InstaBlick-USA/salestax-node) — Node.js / TypeScript
|
|
242
|
+
|
|
243
|
+
## Documentation
|
|
244
|
+
|
|
245
|
+
Full API reference at **[salestaxcalculatorapi.com/docs](https://salestaxcalculatorapi.com/docs)**
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT (c) [InstaBlick USA](https://github.com/InstaBlick-USA)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Async client example. Requires: pip install salestax-python[async]"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from salestax import AsyncSalesTaxClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main() -> None:
|
|
9
|
+
async with AsyncSalesTaxClient() as client:
|
|
10
|
+
seller = {
|
|
11
|
+
"country": "CA",
|
|
12
|
+
"channel_role": "direct_legal_supplier",
|
|
13
|
+
"registrations": [
|
|
14
|
+
{"country": "CA", "state": "ON", "type": "gst_hst",
|
|
15
|
+
"effective_from": "2026-01-01"},
|
|
16
|
+
],
|
|
17
|
+
}
|
|
18
|
+
customer = {
|
|
19
|
+
"type": "consumer",
|
|
20
|
+
"address": {"country": "CA", "state": "ON", "postal_code": "M5V 2T6"},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
calc_a, calc_b, cov = await asyncio.gather(
|
|
24
|
+
client.calculations.create(
|
|
25
|
+
currency="CAD", tax_behavior="exclusive",
|
|
26
|
+
billing_event="subscription_start",
|
|
27
|
+
seller=seller, customer=customer,
|
|
28
|
+
lines=[{"reference": "a", "amount": "100.00", "tax_code": "saas"}],
|
|
29
|
+
idempotency_key="order-a-abc12345",
|
|
30
|
+
),
|
|
31
|
+
client.calculations.create(
|
|
32
|
+
currency="CAD", tax_behavior="exclusive",
|
|
33
|
+
billing_event="subscription_start",
|
|
34
|
+
seller=seller, customer=customer,
|
|
35
|
+
lines=[{"reference": "b", "amount": "250.00", "tax_code": "saas"}],
|
|
36
|
+
idempotency_key="order-b-abc12345",
|
|
37
|
+
),
|
|
38
|
+
client.coverage.check(
|
|
39
|
+
country="CA", state="ON", tax_code="saas", transaction_type="sale",
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
print("Calc A:", calc_a["tax"], "Calc B:", calc_b["tax"])
|
|
43
|
+
print("Coverage:", cov["qualification"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Create a single sales tax calculation.
|
|
2
|
+
|
|
3
|
+
Run:
|
|
4
|
+
SALESTAX_API_KEY=stca_... python examples/basic_calculation.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from salestax import SalesTaxClient
|
|
8
|
+
|
|
9
|
+
with SalesTaxClient() as client:
|
|
10
|
+
calc = client.calculations.create(
|
|
11
|
+
reference="order-1001",
|
|
12
|
+
transaction_date="2026-09-23",
|
|
13
|
+
currency="CAD",
|
|
14
|
+
tax_behavior="exclusive",
|
|
15
|
+
billing_event="subscription_start",
|
|
16
|
+
seller={
|
|
17
|
+
"country": "CA",
|
|
18
|
+
"channel_role": "direct_legal_supplier",
|
|
19
|
+
"registrations": [
|
|
20
|
+
{"country": "CA", "state": "ON", "type": "gst_hst",
|
|
21
|
+
"effective_from": "2026-01-01"},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
customer={
|
|
25
|
+
"type": "consumer",
|
|
26
|
+
"address": {"country": "CA", "state": "ON", "postal_code": "M5V 2T6"},
|
|
27
|
+
},
|
|
28
|
+
lines=[
|
|
29
|
+
{"reference": "subscription", "amount": "100.00",
|
|
30
|
+
"quantity": "1", "tax_code": "saas"},
|
|
31
|
+
],
|
|
32
|
+
idempotency_key="order-1001-abc12345",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
print("Outcome:", calc["outcome"])
|
|
36
|
+
print("Subtotal:", calc["subtotal"])
|
|
37
|
+
print("Tax:", calc["tax"])
|
|
38
|
+
print("Total:", calc["total"])
|