Metadata-Version: 2.5
Name: taxql
Version: 0.1.0
Summary: Official Python client for the TaxQL API — U.S. sales tax lookups at half the price.
Project-URL: Homepage, https://taxql.com
Project-URL: Documentation, https://docs.taxql.com
Project-URL: Repository, https://github.com/taxql/taxql-python
Project-URL: Issues, https://github.com/taxql/taxql-python/issues
Author-email: TaxQL <support@taxql.com>
License: Commercial — see https://taxql.com/terms
License-File: LICENSE
Keywords: api,saas,sales-tax,sales-tax-api,tax
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Accounting
Requires-Python: >=3.10
Requires-Dist: httpx>=0.24
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Description-Content-Type: text/markdown

# TaxQL Python SDK

Official Python client for the [TaxQL API](https://taxql.com).
Address-accurate U.S. sales tax lookups across all 50 states + DC.

```bash
pip install taxql
```

## Quickstart

```python
from taxql import TaxQL

client = TaxQL(api_key="your-key")

response = client.lookup(
    state="tx",
    address="6515 Cottonwood Creek Dr",
    city="Frisco",
    zip="75034",
)

print(f"Rate: {response.combined_rate * 100:.4f}%")
print(f"State: {response.rate.state}  County: {response.rate.county}")
print(f"Confidence: {response.confidence}")
```

Output:

```
Rate: 8.2500%
State: TX  County: COLLIN
Confidence: exact
```

## Why TaxQL

- **Address-accurate**, not ZIP-approximated. We resolve to the
  jurisdiction at the address (including SPDs / ESDs / MUDs), not the
  city in the address line.
- **Best rate by default.** Each lookup returns a single applicable
  rate plus a `confidence` signal and any advisory `warnings`.
- **Historical queries.** Pass `as_of=date(2026, 6, 1)` to retrieve
  the rate in effect on that date.
- **Half the price** of incumbent providers.

## Lookup modes

Provide exactly one input mode per call:

```python
# Address (highest precision)
client.lookup(state="tx", address="6515 Cottonwood Creek Dr",
              city="Frisco", zip="75034")

# ZIP only (single best rate; lower confidence on straddling ZIPs)
client.lookup(state="wa", zip="98039")

# Location name (DoR-published place name)
client.lookup(state="wa", location="Seattle")

# Lat/lng
client.lookup(state="ca", lat=34.05, lng=-118.25)
```

## Response shape

`lookup()` returns a `TaxResponse` with a `rate` block and a `meta`
block. Convenience properties cover the common path:

```python
r = client.lookup(state="tx", address="...", city="...", zip="...")

r.combined_rate      # float — total applicable rate, e.g. 0.0825
r.confidence         # "exact" | "high" | "medium" | "low" | "none"
r.warnings           # list of advisories (see below)

r.rate.state         # "TX"
r.rate.county        # "COLLIN"
r.rate.state_rate    # 0.0625
r.rate.combined_district_rate  # sum of special-district rates
```

Rate fields arrive from the API as fixed 5-decimal strings and are
parsed to floats for you. For the verbose per-jurisdiction body (every
candidate row, component breakdown, resolved-place detail), request
`mode=full` on the raw HTTP API.

### Warnings

`response.warnings` is a list whose items are either plain strings or
structured objects carrying a `code`:

```python
for w in response.warnings:
    if isinstance(w, dict) and w.get("code") == "place_input_mismatch":
        print(f"input '{w['customer_input_place']}' resolved to "
              f"'{w['resolved_place_name']}'")
    elif isinstance(w, str):
        print(w)
```

## Async client

```python
import asyncio
from taxql import AsyncTaxQL

async def main():
    async with AsyncTaxQL(api_key="your-key") as client:
        # Three concurrent lookups
        responses = await asyncio.gather(
            client.lookup(state="tx", zip="75034"),
            client.lookup(state="wa", zip="98039"),
            client.lookup(state="ca", zip="94022"),
        )
        for r in responses:
            print(r.rate.state, f"{r.combined_rate * 100:.4f}%")

asyncio.run(main())
```

## Historical queries

Pass `as_of` to retrieve the rate in effect on a specific date:

```python
from datetime import date

# Q2 2026 (current as of 2026-06-01)
q2 = client.lookup(state="wa", zip="98039", as_of=date(2026, 6, 1))

# Q3 2026 (loaded ahead of effective date)
q3 = client.lookup(state="wa", zip="98039", as_of=date(2026, 7, 15))
```

Supported on states with effective-period history (TX, CA, FL, WA,
NY, and all SST states). Single-snapshot states (NM, MO, IL, CO, LA,
AL, AZ, AK) return the current rate regardless of `as_of`.

## Error handling

```python
from taxql import (
    TaxQL, AuthError, PaymentRequiredError, ForbiddenError,
    RateLimitError, NotFoundError, ValidationError, ServiceError, TaxQLError,
)

try:
    client.lookup(state="tx", zip="75034")
except AuthError:
    print("Invalid API key — sign up at https://taxql.com/signup")
except PaymentRequiredError:
    print("Billing/subscription issue — check the dashboard")   # 402
except ForbiddenError:
    print("Your plan doesn't include this")                     # 403
except RateLimitError as e:
    print(f"Rate limited; retry after {e.retry_after}s")
except NotFoundError:
    print("Address could not be resolved")
except ValidationError as e:
    print(f"Bad input: {e}")
except ServiceError:
    print("Upstream temporarily down")
except TaxQLError as e:
    print(f"Other API error [{e.error_code}] (ref {e.support_reference})")
```

The SDK automatically retries `429` (rate limit) and `5xx`
(service error) responses up to `max_retries=3` times with
exponential backoff (and honors the `Retry-After` header on 429).
`4xx` errors other than 429 raise immediately — no point retrying a
malformed request. Every exception carries `status_code` and the
parsed `response_body` (e.g. `exc.response_body["error"]["code"]`).

## Configuration

```python
TaxQL(
    api_key="your-key",
    base_url="https://api.taxql.com",   # default
    timeout=10.0,                        # seconds
    max_retries=3,                       # 429 + 5xx + timeout
    http_client=None,                    # inject your own httpx.Client
)
```

If you inject your own `httpx.Client`, the SDK won't close it on
`__exit__` — manage its lifecycle yourself. Useful for sharing a
connection pool across multiple SDK instances or for custom transport
(e.g., adding proxies or TLS cert pinning).

## Forward compatibility

The response models use `extra="allow"` so newer API versions adding
fields don't break older clients. You can always access the raw
response dict via `response.model_dump()`.

## Examples

Runnable examples live in [`examples/`](examples/):

- `examples/basic.py` — sync address lookup, single state
- `examples/async_lookup.py` — async, multiple states in parallel
- `examples/historical.py` — `as_of` comparison across quarters

## Documentation

Full API docs at https://docs.taxql.com.
OpenAPI spec at https://api.taxql.com/openapi.json.

## License

Commercial — see https://taxql.com/terms.

## Support

`support@taxql.com` — or open an issue at
https://github.com/taxql/taxql-python/issues.
