Metadata-Version: 2.4
Name: ipgeolocationio
Version: 3.0.1
Summary: Typed Python SDK for the IPGeolocation.io IP location API, with sync and async clients.
Author-email: IPGeolocation Team <support@ipgeolocation.io>
License-Expression: MIT
Project-URL: Homepage, https://ipgeolocation.io/
Project-URL: Documentation, https://ipgeolocation.io/documentation/ip-geolocation-api-python-sdk.html
Project-URL: Repository, https://github.com/IPGeolocation/ip-geolocation-api-python-sdk
Project-URL: Issues, https://github.com/IPGeolocation/ip-geolocation-api-python-sdk/issues
Keywords: ipgeolocation,geolocation,vpn-detection,proxy-detection,threat-intelligence,asn,timezone
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
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: Programming Language :: Python :: 3.15
Classifier: Typing :: Typed
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1,>=0.28.1
Requires-Dist: requests<3,>=2.33.0
Requires-Dist: urllib3[zstd]<3,>=2.8.0
Requires-Dist: anyio<5,>=4.14.2
Requires-Dist: certifi>=2026.10.23
Provides-Extra: dev
Requires-Dist: httpx[http2]<1,>=0.28.1; extra == "dev"
Requires-Dist: urllib3[brotli,zstd]<3,>=2.8.0; extra == "dev"
Requires-Dist: pytest<10,>=9.0.3; extra == "dev"
Requires-Dist: pytest-cov<8,>=7.0.0; extra == "dev"
Requires-Dist: pyright<2,>=1.1.408; extra == "dev"
Requires-Dist: ruff<0.16,>=0.15.6; extra == "dev"
Requires-Dist: mypy<2,>=1.19.1; extra == "dev"
Requires-Dist: types-requests<3,>=2.32.4; extra == "dev"
Requires-Dist: build<2,>=1.6.1; extra == "dev"
Requires-Dist: setuptools>=84.1.0; extra == "dev"
Provides-Extra: audit
Requires-Dist: pip-audit<3,>=2.10.1; extra == "audit"
Requires-Dist: filelock>=4.2.1; extra == "audit"
Requires-Dist: setuptools>=84.1.0; extra == "audit"
Dynamic: license-file

# IPGeolocation.io Python SDK

Typed Python SDK for the [IPGeolocation.io](https://ipgeolocation.io/) IP Geolocation API.

- Single lookup (`/v3/ipgeo`) and bulk lookup (`/v3/ipgeo-bulk`)
- Sync client built on `requests` and async client built on `httpx`
- Typed response models plus raw JSON/XML methods
- Python 3.10+

## Table of Contents

- [Install](#install)
- [Quick Start](#quick-start)
- [Authentication](#authentication)
- [Notes](#notes)
- [Plan Behavior](#plan-behavior)
- [Examples](#examples)
- [Request Options](#request-options)
- [Response Metadata](#response-metadata)
- [Errors](#errors)
- [Links](#links)
- [Development](#development)

## Install

```bash
pip install ipgeolocationio
```

PyPI package: `ipgeolocationio`
Package page: <https://pypi.org/project/ipgeolocationio/>
Import: `ipgeolocation`

## Quick Start

```python
from ipgeolocation import (
    IpGeolocationClient,
    IpGeolocationClientConfig,
    LookupIpGeolocationRequest,
)

config = IpGeolocationClientConfig(api_key="YOUR_API_KEY")

with IpGeolocationClient(config) as client:
    response = client.lookup_ip_geolocation(
        LookupIpGeolocationRequest(ip="8.8.8.8")
    )

    print(response.data.ip)

    if response.data.location is not None:
        print(response.data.location.country_name)
        print(response.data.location.city)

    if response.data.time_zone is not None:
        print(response.data.time_zone.name)
```

### Async

```python
import asyncio

from ipgeolocation import (
    AsyncIpGeolocationClient,
    IpGeolocationClientConfig,
    LookupIpGeolocationRequest,
)


async def main() -> None:
    config = IpGeolocationClientConfig(api_key="YOUR_API_KEY")

    async with AsyncIpGeolocationClient(config) as client:
        response = await client.lookup_ip_geolocation(
            LookupIpGeolocationRequest(ip="8.8.8.8")
        )
        print(response.data.ip)

        if response.data.location is not None:
            print(response.data.location.country_name)


asyncio.run(main())
```

The async client exposes the same methods as coroutines. Use `async with` for cleanup, or call `await client.aclose()` manually.

## Authentication

**API key**: API key authentication supports single lookups on all plans. Bulk lookup requires a paid plan.

```python
config = IpGeolocationClientConfig(api_key="YOUR_API_KEY")
```

### Client Config

| Field | Use |
|---|---|
| `api_key` | API key auth for single lookup and bulk lookup |
| `base_url` | Use the final address of your API or proxy. The standard client follows redirects only if the protocol, host and port stay the same. The async client stops at redirects by default. |
| `connect_timeout` | Time to wait for the connection in seconds |
| `read_timeout` | Time to wait for the response body in seconds |

## Notes

- **Typed methods require JSON.** For XML output, use the `_raw` methods with `output=ResponseFormat.XML`.
- **Optional modules need `include`.** Fields like `security`, `abuse`, `user_agent`, `hostname`, `geo_accuracy`, and `dma_code` only appear in the response when you pass the matching `include` value. Without it, those fields will be `None`.
- **`fields` and `excludes` only filter the response.** They do not enable optional modules or unlock paid data.
- **Domain lookup is paid only.** Passing a domain as `ip="google.com"` works on paid plans. Free plans get a 401.
- **The SDK does not retry.** Timeouts, server errors, and rate limits raise exceptions directly. Implement your own retry logic if you need it.
- **Do not reuse a closed client.** After `close()`, `aclose()`, or leaving a `with` or `async with` block, further requests raise `ValidationException`.

## Plan Behavior

Responses vary by plan.

### Capabilities

| Capability | Free | Paid |
|---|---|---|
| Single IPv4/IPv6 lookup | Yes | Yes |
| Domain lookup | No | Yes |
| Bulk lookup (`/v3/ipgeo-bulk`) | No | Yes |
| Non-English `lang` | No | Yes |
| Optional modules via `include` | No | Yes |
| `include=["*"]` | Base response only | All plan-available modules |

### Default Response Sections

Default single-lookup response:

| Section | Free | Paid |
|---|---|---|
| `location` | Yes | Yes |
| `country_metadata` | Yes | Yes |
| `currency` | Yes | Yes |
| `asn` (basic: `as_number`, `organization`, `country`) | Yes | Yes |
| `asn` (full: adds `type`, `domain`, `date_allocated`, `rir`) | No | Yes |
| `time_zone` | Yes | Yes |
| `network` | No | Yes |
| `company` | No | Yes |

## Examples

The examples below assume you already have a configured client in scope. See [Quick Start](#quick-start) for setup.

### Caller IP

Omit the `ip` parameter to look up the IP of the machine making the request:

```python
response = client.lookup_ip_geolocation(LookupIpGeolocationRequest())
print(response.data.ip)  # your public IP
```

### Domain Lookup (Paid)

When you look up a domain, the response includes the resolved IP and the original domain name:

```python
response = client.lookup_ip_geolocation(
    LookupIpGeolocationRequest(ip="google.com")
)
print(response.data.ip)       # resolved IP address
print(response.data.domain)   # "google.com"
```

### Security and Abuse

```python
response = client.lookup_ip_geolocation(
    LookupIpGeolocationRequest(
        ip="9.9.9.9",
        include=["security", "abuse"],
    )
)

if response.data.security is not None:
    print(response.data.security.threat_score)

if response.data.abuse is not None and response.data.abuse.emails:
    print(response.data.abuse.emails[0])
```

### User-Agent Parsing

To parse a visitor's user-agent string, pass `include=["user_agent"]` and send the visitor string in the request `User-Agent` header:

```python
visitor_ua = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) "
    "AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9"
)

response = client.lookup_ip_geolocation(
    LookupIpGeolocationRequest(
        ip="115.240.90.163",
        include=["user_agent"],
        headers={"User-Agent": visitor_ua},
    )
)

if response.data.user_agent is not None:
    print(response.data.user_agent.name)
    print(response.data.user_agent.operating_system)
```

> **Note:** The `user_agent` field on the request model is different. It overrides the outbound `User-Agent` header for the HTTP request itself and takes precedence over `headers["User-Agent"]`. If you set both, the API parses the `user_agent` value.

### Filtered Response

Use `fields` to keep specific fields, or `excludes` to remove them:

```python
response = client.lookup_ip_geolocation(
    LookupIpGeolocationRequest(
        ip="8.8.8.8",
        include=["security"],
        fields=["location.country_name", "security.threat_score", "security.is_vpn"],
        excludes=["currency"],
    )
)

if response.data.location is not None:
    print(response.data.location.country_name)

if response.data.security is not None:
    print(response.data.security.threat_score)
    print(response.data.security.is_vpn)
```

### Raw XML

Typed methods only support JSON. For XML, use a raw method:

```python
from ipgeolocation import ResponseFormat

response = client.lookup_ip_geolocation_raw(
    LookupIpGeolocationRequest(ip="8.8.8.8", output=ResponseFormat.XML)
)
print(response.data)  # raw XML string
```

### Bulk Lookup (Paid)

Bulk lookup uses POST and accepts up to 50,000 IPs or domains. Each item in the response is either `BulkLookupSuccess` or `BulkLookupError`:

```python
from ipgeolocation import (
    BulkLookupError,
    BulkLookupIpGeolocationRequest,
    BulkLookupSuccess,
)

response = client.bulk_lookup_ip_geolocation(
    BulkLookupIpGeolocationRequest(
        ips=["8.8.8.8", "invalid-ip", "1.1.1.1"],
        include=["security"],
    )
)

for result in response.data:
    if isinstance(result, BulkLookupSuccess):
        print(result.data.ip, result.data.security)  # 8.8.8.8 Security(...)
    elif isinstance(result, BulkLookupError):
        print(result.error.message)  # 'invalid-ip' is not a valid IP address.
```

## Request Options

### Single Lookup

| Field | Type | Notes |
|---|---|---|
| `ip` | `str` or `None` | IPv4, IPv6, or domain (domain is paid only). Omit for caller IP lookup. |
| `lang` | `Language`, `str`, or `None` | Response language. Non-English requires a paid plan. |
| `include` | sequence of strings | Optional modules to enable. See [include values](#include-values). |
| `fields` | sequence of strings | Response field filter. Does not unlock data. |
| `excludes` | sequence of strings | Response field filter. Does not unlock data. |
| `output` | `ResponseFormat` or `str` | `"json"` (default) or `"xml"`. Typed methods require JSON. |
| `user_agent` | `str` or `None` | Overrides the outbound `User-Agent` header. Takes precedence over `headers["User-Agent"]`. |
| `headers` | mapping of strings | Custom request headers. `Accept` is always SDK-managed. Sequence values are sent as one comma-joined header line. |

### Bulk Lookup

Bulk lookup accepts the same fields as single lookup, plus:

| Field | Type | Notes |
|---|---|---|
| `ips` | sequence of strings | Required. Up to 50,000 IPs or domains. |

Set `api_key` in your client settings for bulk lookups. The SDK sets `Content-Type: application/json` for you.

### `include` Values

| Value | What it adds |
|---|---|
| `security` | `security` object (threat score, VPN/proxy/Tor detection, bot detection) |
| `abuse` | `abuse` object (abuse contact info, emails, phone numbers) |
| `user_agent` | `user_agent` object (browser, device, OS parsed from the request `User-Agent` header) |
| `hostname` | `hostname` field (reverse DNS lookup) |
| `liveHostname` | `hostname` field (live DNS) |
| `hostnameFallbackLive` | `hostname` field (fallback to live DNS) |
| `geo_accuracy` | `location.locality`, `location.accuracy_radius`, `location.confidence` |
| `dma_code` | `location.dma_code` |
| `*` | All optional modules available on your plan |

Supported `lang` values: `en`, `de`, `ru`, `ja`, `fr`, `cn`, `es`, `cs`, `it`, `ko`, `fa`, `pt`, `ar` (Arabic)

## Response Metadata

Every SDK method returns `ApiResponse(data=..., metadata=...)`.

- Typed single lookup returns `IpGeolocationResponse`
- A typed bulk lookup returns one `BulkLookupResult` per input. If the API sends too few or too many results, the SDK raises `SerializationException`.
- Raw methods return the response body as a string

| Field | Type | Description |
|---|---|---|
| `status_code` | `int` | HTTP status code |
| `duration_ms` | `int` | Wall-clock request time in milliseconds, measured by the SDK |
| `credits_charged` | `int` or `None` | Parsed from the `X-Credits-Charged` response header |
| `successful_records` | `int` or `None` | Parsed from the `X-Successful-Record` response header |
| `raw_headers` | read-only mapping | All response headers. Values are tuples of strings. |

Header access helpers:

```python
metadata.header_values("Header-Name")        # all values as a tuple
metadata.first_header_value("Header-Name")   # first value or None
```

### JSON Serialization

Use `to_json()` or `to_pretty_json()` to print SDK objects as JSON. When you print your client settings, the SDK hides your API key.

```python
from ipgeolocation import JsonOutputMode, to_pretty_json

# Compact mode (default): omits None fields
print(to_pretty_json(response.data))

# Full mode: includes None fields as null
print(to_pretty_json(response.data, mode=JsonOutputMode.FULL))
```

## Errors

All exceptions inherit from `IpGeolocationException`. The SDK does not retry failed requests.

**Before the request is sent:**

- `ValidationException` for bad config, invalid request parameters, or calling a closed client

**Transport problems:**

- `RequestTimeoutException` for timeouts
- `TransportException` for connection or other HTTP-level failures

**API errors (non-2xx responses):**

| Exception | HTTP Status |
|---|---|
| `BadRequestException` | 400 |
| `UnauthorizedException` | 401 |
| `NotFoundException` | 404 |
| `MethodNotAllowedException` | 405 |
| `PayloadTooLargeException` | 413 |
| `UnsupportedMediaTypeException` | 415 |
| `LockedException` | 423 |
| `RateLimitException` | 429 |
| `ClientClosedRequestException` | 499 |
| `ServerErrorException` | 5xx |
| `ApiException` | Any other non-2xx |

All API exceptions have `.status_code` and `.api_message` attributes. If the error body is too large or the connection closes early, you still get the HTTP status. The SDK hides your API key in error messages, including errors for individual IPs in a typed bulk lookup.

## Links

- [PyPI package](https://pypi.org/project/ipgeolocationio/)
- [Python SDK documentation](https://ipgeolocation.io/documentation/ip-geolocation-api-python-sdk.html)
- [API documentation](https://ipgeolocation.io/documentation/ip-location-api.html)
- [Authentication](https://ipgeolocation.io/documentation/api-authentication.html)
- [Response formats](https://ipgeolocation.io/documentation/api-response-formats.html)
- [Credits and usage](https://ipgeolocation.io/documentation/credits-usage.html)
- [GitHub repository](https://github.com/IPGeolocation/ip-geolocation-api-python-sdk)
