salestax-python 0.1.0__py3-none-any.whl
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/__init__.py +61 -0
- salestax/_config.py +53 -0
- salestax/_transport/__init__.py +18 -0
- salestax/_transport/http_client.py +287 -0
- salestax/_transport/httpx_transport.py +54 -0
- salestax/_transport/retry.py +49 -0
- salestax/_transport/types.py +91 -0
- salestax/_transport/urllib_transport.py +55 -0
- salestax/_transport/user_agent.py +15 -0
- salestax/_version.py +3 -0
- salestax/client.py +215 -0
- salestax/errors.py +202 -0
- salestax/models.py +80 -0
- salestax/py.typed +1 -0
- salestax/resources/__init__.py +5 -0
- salestax/resources/jurisdictions.py +27 -0
- salestax/resources/rates.py +25 -0
- salestax/resources/tax.py +146 -0
- salestax/utils.py +22 -0
- salestax_python-0.1.0.dist-info/METADATA +158 -0
- salestax_python-0.1.0.dist-info/RECORD +23 -0
- salestax_python-0.1.0.dist-info/WHEEL +4 -0
- salestax_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""``client.tax`` resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any, Union
|
|
7
|
+
|
|
8
|
+
from .._config import MAX_BATCH_SIZE
|
|
9
|
+
from ..errors import ValidationError
|
|
10
|
+
from ..models import BatchResult, CalculateTaxParams, TaxCalculation
|
|
11
|
+
from ..utils import chunk
|
|
12
|
+
|
|
13
|
+
_TransactionLike = Union[CalculateTaxParams, dict[str, Any]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _amount(value: Any) -> float:
|
|
17
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
18
|
+
raise ValidationError(
|
|
19
|
+
"INVALID_AMOUNT",
|
|
20
|
+
"amount must be a non-negative number",
|
|
21
|
+
status_code=400,
|
|
22
|
+
param="amount",
|
|
23
|
+
)
|
|
24
|
+
amount = float(value)
|
|
25
|
+
if amount < 0:
|
|
26
|
+
raise ValidationError(
|
|
27
|
+
"INVALID_AMOUNT",
|
|
28
|
+
"amount must be a non-negative number",
|
|
29
|
+
status_code=400,
|
|
30
|
+
param="amount",
|
|
31
|
+
)
|
|
32
|
+
return amount
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TaxResource:
|
|
36
|
+
"""Tax calculation endpoints. Do not instantiate directly."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, http: Any, *, chunk_batch: bool = False) -> None:
|
|
39
|
+
self._http = http
|
|
40
|
+
self._chunk_batch = chunk_batch
|
|
41
|
+
|
|
42
|
+
def calculate(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
zip_code: str,
|
|
46
|
+
amount: float,
|
|
47
|
+
state: str | None = None,
|
|
48
|
+
country: str | None = None,
|
|
49
|
+
city: str | None = None,
|
|
50
|
+
idempotency_key: str | None = None,
|
|
51
|
+
retryable: bool = True,
|
|
52
|
+
) -> TaxCalculation:
|
|
53
|
+
"""Calculate tax for a single transaction.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValidationError: If ``zip_code`` or ``amount`` is invalid.
|
|
57
|
+
ApiError: If the API returns a non-2xx response.
|
|
58
|
+
"""
|
|
59
|
+
if not zip_code:
|
|
60
|
+
raise ValidationError(
|
|
61
|
+
"MISSING_PARAM", "zip_code is required", status_code=400, param="zip_code"
|
|
62
|
+
)
|
|
63
|
+
_amount(amount)
|
|
64
|
+
|
|
65
|
+
payload: dict[str, Any] = {"zipCode": zip_code, "amount": amount}
|
|
66
|
+
if state is not None:
|
|
67
|
+
payload["state"] = state
|
|
68
|
+
if country is not None:
|
|
69
|
+
payload["country"] = country
|
|
70
|
+
if city is not None:
|
|
71
|
+
payload["city"] = city
|
|
72
|
+
|
|
73
|
+
return self._http.send("POST", "/calculate", body=payload) # type: ignore[no-any-return]
|
|
74
|
+
|
|
75
|
+
def calculate_batch(
|
|
76
|
+
self,
|
|
77
|
+
transactions: Sequence[_TransactionLike],
|
|
78
|
+
*,
|
|
79
|
+
idempotency_key: str | None = None,
|
|
80
|
+
retryable: bool = True,
|
|
81
|
+
) -> BatchResult:
|
|
82
|
+
"""Calculate tax for up to 100 transactions in one call.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ValidationError: If the batch is empty, exceeds 100 items without
|
|
86
|
+
``chunk_batch=True``, or contains an invalid amount.
|
|
87
|
+
"""
|
|
88
|
+
self._validate_batch(transactions)
|
|
89
|
+
for t in transactions:
|
|
90
|
+
_amount(t.get("amount"))
|
|
91
|
+
payload = {"transactions": list(transactions)}
|
|
92
|
+
return self._http.send( # type: ignore[no-any-return]
|
|
93
|
+
"POST",
|
|
94
|
+
"/calculate/batch",
|
|
95
|
+
body=payload,
|
|
96
|
+
options=_request_options(idempotency_key, retryable),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def calculate_batch_chunked(
|
|
100
|
+
self,
|
|
101
|
+
transactions: Sequence[_TransactionLike],
|
|
102
|
+
*,
|
|
103
|
+
idempotency_key: str | None = None,
|
|
104
|
+
retryable: bool = True,
|
|
105
|
+
) -> BatchResult:
|
|
106
|
+
"""Calculate tax for an arbitrary number of transactions.
|
|
107
|
+
|
|
108
|
+
Automatically splits the input into batches of ``MAX_BATCH_SIZE`` and
|
|
109
|
+
concatenates results in the original order.
|
|
110
|
+
"""
|
|
111
|
+
if not transactions:
|
|
112
|
+
raise ValidationError(
|
|
113
|
+
"EMPTY_BATCH", "Batch must contain at least one transaction", status_code=400
|
|
114
|
+
)
|
|
115
|
+
for t in transactions:
|
|
116
|
+
_amount(t.get("amount"))
|
|
117
|
+
|
|
118
|
+
results: list[TaxCalculation] = []
|
|
119
|
+
for batch in chunk(list(transactions), MAX_BATCH_SIZE):
|
|
120
|
+
res = self._http.send(
|
|
121
|
+
"POST",
|
|
122
|
+
"/calculate/batch",
|
|
123
|
+
body={"transactions": batch},
|
|
124
|
+
options=_request_options(idempotency_key, retryable),
|
|
125
|
+
)
|
|
126
|
+
results.extend(res.get("results", []))
|
|
127
|
+
return {"results": results, "count": len(results)}
|
|
128
|
+
|
|
129
|
+
def _validate_batch(self, transactions: Sequence[_TransactionLike]) -> None:
|
|
130
|
+
if not transactions:
|
|
131
|
+
raise ValidationError(
|
|
132
|
+
"EMPTY_BATCH", "Batch must contain at least one transaction", status_code=400
|
|
133
|
+
)
|
|
134
|
+
if len(transactions) > MAX_BATCH_SIZE and not self._chunk_batch:
|
|
135
|
+
raise ValidationError(
|
|
136
|
+
"BATCH_LIMIT_EXCEEDED",
|
|
137
|
+
f"Batch is limited to {MAX_BATCH_SIZE} transactions. "
|
|
138
|
+
"Use calculate_batch_chunked() or enable chunk_batch=True.",
|
|
139
|
+
status_code=400,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _request_options(idempotency_key: str | None, retryable: bool) -> Any:
|
|
144
|
+
from .._transport.types import RequestOptions
|
|
145
|
+
|
|
146
|
+
return RequestOptions(idempotency_key=idempotency_key, retryable=retryable)
|
salestax/utils.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Small internal helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def chunk(items: Sequence[T], size: int) -> list[list[T]]:
|
|
12
|
+
"""Split ``items`` into lists of at most ``size`` elements."""
|
|
13
|
+
if size <= 0:
|
|
14
|
+
raise ValueError("size must be positive")
|
|
15
|
+
return [list(items[i : i + size]) for i in range(0, len(items), size)]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def redact(value: str, keep: int = 4) -> str:
|
|
19
|
+
"""Redact all but the last ``keep`` characters. Used in log output."""
|
|
20
|
+
if len(value) <= keep:
|
|
21
|
+
return "*" * len(value)
|
|
22
|
+
return "*" * (len(value) - keep) + value[-keep:]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: salestax-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Sales Tax Calculator API — real-time sales tax across 70+ countries, 51 US jurisdictions, and batch processing.
|
|
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,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)** —
|
|
47
|
+
real-time sales tax for 70+ countries, 51 US jurisdictions, and 13 Canadian provinces.
|
|
48
|
+
Batch up to 100 transactions. Zero runtime dependencies (sync). 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
|
+
tax = client.tax.calculate(zip_code="90210", amount=100)
|
|
69
|
+
|
|
70
|
+
print(tax["taxAmount"]) # 9.75
|
|
71
|
+
print(tax["totalAmount"]) # 109.75
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Get your API key → **[salestaxcalculatorapi.com](https://salestaxcalculatorapi.com)**
|
|
75
|
+
|
|
76
|
+
## Features
|
|
77
|
+
|
|
78
|
+
- **Zero runtime dependencies** for the sync client — stdlib `urllib` only.
|
|
79
|
+
- **Optional async client** via `httpx` (`pip install salestax-python[async]`).
|
|
80
|
+
- **Automatic retries** with exponential backoff + jitter, honors `Retry-After`.
|
|
81
|
+
- **Typed errors** — `RateLimitError`, `ValidationError`, `TimeoutError`, and more.
|
|
82
|
+
No string parsing. Every error carries `code`, `request_id`, and (where applicable) `param`.
|
|
83
|
+
- **Lifecycle hooks** — `on_request`, `on_response`, `on_retry` for logging and telemetry.
|
|
84
|
+
- **Batch chunking** — `calculate_batch_chunked()` splits >100 automatically.
|
|
85
|
+
- **Fully typed** — ships `py.typed`, `mypy --strict` clean.
|
|
86
|
+
|
|
87
|
+
## Async
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import asyncio
|
|
91
|
+
from salestax import AsyncSalesTaxClient
|
|
92
|
+
|
|
93
|
+
async def main():
|
|
94
|
+
async with AsyncSalesTaxClient() as client:
|
|
95
|
+
tax = await client.tax.calculate(zip_code="90210", amount=100)
|
|
96
|
+
print(tax["taxAmount"])
|
|
97
|
+
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Batch
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
with SalesTaxClient() as client:
|
|
105
|
+
result = client.tax.calculate_batch_chunked(
|
|
106
|
+
transactions, # any length
|
|
107
|
+
idempotency_key="order-12345",
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Error Handling
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from salestax import RateLimitError, ValidationError
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
client.tax.calculate(zip_code="90210", amount=100)
|
|
118
|
+
except RateLimitError as err:
|
|
119
|
+
# err.retry_after_ms — back off
|
|
120
|
+
...
|
|
121
|
+
except ValidationError as err:
|
|
122
|
+
# err.param — which field failed
|
|
123
|
+
...
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from salestax import SalesTaxClient, RetryPolicy
|
|
130
|
+
|
|
131
|
+
client = SalesTaxClient(
|
|
132
|
+
api_key="sk_live_...",
|
|
133
|
+
timeout_ms=20_000,
|
|
134
|
+
chunk_batch=True,
|
|
135
|
+
retry=RetryPolicy(max_retries=4),
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Hooks
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
client = SalesTaxClient(hooks={
|
|
143
|
+
"on_retry": lambda info: print(f"retry {info['attempt']} in {info['delay_ms']}ms"),
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Also Available For
|
|
148
|
+
|
|
149
|
+
- [salestax-node](https://github.com/InstaBlick-USA/salestax-node) — Node.js / TypeScript
|
|
150
|
+
- [salestax-ruby](https://github.com/InstaBlick-USA/salestax-ruby) — Ruby
|
|
151
|
+
|
|
152
|
+
## Documentation
|
|
153
|
+
|
|
154
|
+
Full API reference → **[salestaxcalculatorapi.com/docs](https://salestaxcalculatorapi.com/docs)**
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT © [InstaBlick USA](https://github.com/InstaBlick-USA)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
salestax/__init__.py,sha256=rpr-zj2hpvKdXa9Oz5kMtcfmBArwkED1kAz8jLeN6yw,1320
|
|
2
|
+
salestax/_config.py,sha256=Y2OqwHEUQVpclhMUKiXmtGHR-qYTxejwgniebkXfLYU,1659
|
|
3
|
+
salestax/_version.py,sha256=VmPUojNETb-e_3dWOA7kZeosNlKu8t67NvIMY5AV5lU,73
|
|
4
|
+
salestax/client.py,sha256=sPAnXbqJSC59QiZo_8D3FgfXpAz59WQZp72RFpvCpX4,7524
|
|
5
|
+
salestax/errors.py,sha256=ZQw7jmnZSFiysXiUyP1akdOHf0jCZvBKmRXEV2AOvAA,5307
|
|
6
|
+
salestax/models.py,sha256=n6vfoWGyvue595tzpdlcbSlATIIhQBpl-2FyqwXrxOQ,1703
|
|
7
|
+
salestax/py.typed,sha256=S2JdbeRy7souiuGCm5NHAx_wf4AZZewwlWSTyuYq89o,44
|
|
8
|
+
salestax/utils.py,sha256=pzJdO_fhOlJwVG8t2zc-pCCpQaqp8RhpVYxuQIuadMA,655
|
|
9
|
+
salestax/_transport/__init__.py,sha256=dWzJhvFxSNMs5-z7fnzYbfO_IcHpy_f44S0qp5GjYbc,497
|
|
10
|
+
salestax/_transport/http_client.py,sha256=KgVpvi3s2o1LEZcNevkPYdjlp5YSXMqhLKgiStnE-ao,9399
|
|
11
|
+
salestax/_transport/httpx_transport.py,sha256=E_AUrIiYmaQcmuQKBfn3eii4i2mi4tuN-NjNZyVR8VY,1605
|
|
12
|
+
salestax/_transport/retry.py,sha256=46mhtRnRoSqRSBL5-R0PQnkIBJAdsIhOJXLIhhpYuEU,1435
|
|
13
|
+
salestax/_transport/types.py,sha256=5T4TMAjJf2nME79V_THufa8CqCsba9X5nezWiJqw9lc,2336
|
|
14
|
+
salestax/_transport/urllib_transport.py,sha256=wom8qIrYIUSV33lD0OBuKowsAcMdcNwMsgCZ8WuZG2M,2028
|
|
15
|
+
salestax/_transport/user_agent.py,sha256=wOE5_SMA5kk3KmLUXq3Pe2sUr08ZfbFjoE5gKipLSmI,444
|
|
16
|
+
salestax/resources/__init__.py,sha256=DD4u3HhflZkywAHWGAF0BDMooc-t55NuclLsIMuha6g,180
|
|
17
|
+
salestax/resources/jurisdictions.py,sha256=nenRJp15ds_6DBd8lIsV40U_CJqviYRMWzTWgmSAt9Y,830
|
|
18
|
+
salestax/resources/rates.py,sha256=jiWztqFmAklITTetGt7p3eaOdmp0FVX4pZSO3nhWluo,780
|
|
19
|
+
salestax/resources/tax.py,sha256=sVYSG0Y-w1aNY39f-C7c8ttPuDhe1TKaFlXEuBGXDJM,4925
|
|
20
|
+
salestax_python-0.1.0.dist-info/METADATA,sha256=EvACOB77pM9sKAEN5AhHR9ny6HrAf7jUB7SzFOLzQ70,5207
|
|
21
|
+
salestax_python-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
22
|
+
salestax_python-0.1.0.dist-info/licenses/LICENSE,sha256=KEd6gVA2mih1LR627--d2IqpDOnsqkR9aMn5DGg_PK4,1070
|
|
23
|
+
salestax_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 InstaBlick-USA
|
|
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.
|