Metadata-Version: 2.5
Name: ipmax
Version: 0.1.0
Summary: Python client for the IP-Max GeoIP and IP intelligence API
Project-URL: Homepage, https://ipm.ax
Project-URL: Repository, https://github.com/IPMaxxing/python-sdk
Project-URL: Issues, https://github.com/IPMaxxing/python-sdk/issues
Author-email: IP-Max <opensource@ipm.ax>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: asn,geoip,geolocation,ip,ip-intelligence,ipmax,rpki
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.5
Description-Content-Type: text/markdown

# ipmax

Python client for the [IP-Max](https://ipm.ax) GeoIP and IP intelligence API, with sync and async clients and fully typed responses.

## Install

```sh
pip install ipmax
```

Requires Python 3.10 or newer.

## Quickstart

```python
from ipmax import IPMax

with IPMax("sg_live_...") as client:
    geo = client.geoip("8.8.8.8")
    print(geo.geo.city, geo.geo.country_code, geo.as_.name)

    intel = client.intelligence("8.8.8.8")
    print(intel.network_class.primary, intel.is_hosting)
```

The async client has the same methods:

```python
import asyncio

from ipmax import AsyncIPMax


async def main() -> None:
    async with AsyncIPMax() as client:
        catalog = await client.catalog()
        account = await client.account()
        print(catalog.prices, account.wallets)


asyncio.run(main())
```

When no key is passed, the client reads `IPMAX_API_KEY` from the environment. `catalog()` works without a key.

Every lookup is billed and sends a fresh `Idempotency-Key`, which is reused when the request is retried. Pass your own with `client.geoip(ip, idempotency_key="...")` to make a lookup safe to repeat across processes.

## Configuration

| Option | Default | Description |
| --- | --- | --- |
| `api_key` | `IPMAX_API_KEY` | API key sent as a bearer token |
| `base_url` | `https://api.ipm.ax` | API origin |
| `timeout` | `10.0` | Seconds per attempt |
| `max_retries` | `2` | Retries on network errors, 408, 429 and 5xx, with exponential backoff and `Retry-After` support |
| `cache_size` | `1024` | Successful lookups kept in memory; `0` disables the cache |
| `cache_ttl` | `300.0` | Seconds a cached lookup stays fresh |
| `http_client` | new client | Your own `httpx.Client` or `httpx.AsyncClient`, for proxies or custom transports |

Cached lookups make no request and cost nothing. `catalog()` and `account()` are never cached. A lookup with an explicit idempotency key always goes to the API.

## Error handling

```python
import ipmax

try:
    client.geoip("8.8.8.8")
except ipmax.InsufficientBalanceError:
    ...
except ipmax.RateLimitError as error:
    print("retry in", error.retry_after)
except ipmax.ApiError as error:
    print(error.status, error.code, error.message, error.request_id)
except ipmax.ConnectionError:
    ...
```

| Exception | When |
| --- | --- |
| `IPMaxError` | Base class for everything below |
| `ApiError` | Any non-2xx response; carries `status`, `code`, `message`, `request_id`, `retryable`, `retry_after` |
| `InvalidRequestError` | 400, 413, 415 |
| `AuthenticationError` | 401 |
| `InsufficientBalanceError` | 402 |
| `NotFoundError` | 404 |
| `ConflictError` | 409 |
| `RateLimitError` | 429 |
| `ServerError` | 5xx |
| `ConnectionError` | The request could not be completed |
| `TimeoutError` | The request timed out; a subclass of `ConnectionError` |

`ErrorCode` holds the known values of `error.code`, such as `ErrorCode.INSUFFICIENT_BALANCE`. New codes may appear and are passed through as plain integers.

## License

[Apache-2.0](LICENSE)
