Metadata-Version: 2.4
Name: ipregistry-django
Version: 1.0.0
Summary: Official Django integration for the Ipregistry IP geolocation and threat data API.
Keywords: django,geoip,geolocation,ip,ip-address,ip-data,ip-geolocation,ipregistry,middleware,proxy-detection,threat-data,vpn-detection
Author: Ipregistry
Author-email: Ipregistry <support@ipregistry.co>
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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 :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Dist: django>=5.2
Requires-Dist: ipregistry>=5.0.0,<6.0.0
Requires-Python: >=3.10
Project-URL: Changelog, https://github.com/ipregistry/ipregistry-django/blob/main/CHANGELOG.md
Project-URL: Documentation, https://ipregistry.co/docs
Project-URL: Homepage, https://ipregistry.co
Project-URL: Issues, https://github.com/ipregistry/ipregistry-django/issues
Project-URL: Source, https://github.com/ipregistry/ipregistry-django
Description-Content-Type: text/markdown

# Ipregistry Django

[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![CI](https://github.com/ipregistry/ipregistry-django/actions/workflows/ci.yml/badge.svg)](https://github.com/ipregistry/ipregistry-django/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/ipregistry-django.svg)](https://pypi.org/project/ipregistry-django/)

The official Django integration for the [Ipregistry](https://ipregistry.co) IP geolocation
and threat data API, built on top of [`ipregistry`](https://github.com/ipregistry/ipregistry-python).

```python
def view(request):
    if request.ipregistry and request.ipregistry.location.country.code == "FR":
        ...
```

## Features

- **`request.ipregistry`** — geolocation and threat data anywhere a request is in scope,
  queried at most once per request and only when actually accessed.
- **Middleware and view decorators** to block countries (451) or threats such as proxies,
  Tor and VPNs (403), site-wide or per-view.
- **Django caching** — repeated visits from the same IP cost a single credit, backed by
  any configured cache (Redis, Valkey, Memcached, ...).
- **GDPR helper** — `is_eu(request)` to decide when to show consent banners.
- **Safe by default** — fails open on API errors, never sends private IPs, never logs
  full IP addresses, supports async views and ASGI.
- **Testable** — `fake_ipregistry()` stubs lookups with zero HTTP and records assertions.
- **`manage.py ipregistry_lookup`** command and Django system checks for misconfiguration.

## Requirements

- Django 5.2+ (5.2 LTS and 6.0 are tested)
- Python 3.10+

## Installation

```console
pip install ipregistry-django
```

Set your API key ([get one here](https://dashboard.ipregistry.co)), preferably via the
environment:

```console
export IPREGISTRY_API_KEY=YOUR_API_KEY
```

Then enable the app and middleware in `settings.py`:

```python
INSTALLED_APPS = [
    # ...
    "ipregistry_django",
]

MIDDLEWARE = [
    # ...
    "ipregistry_django.middleware.IpregistryMiddleware",
]
```

That's it. Every view (and everything downstream of the middleware) can now read
`request.ipregistry`.

## Usage

### Request data

`request.ipregistry` is a lazy [`IpInfo`](https://ipregistry.co/docs/responses) object.
The API is only queried when the attribute is first accessed, at most once per request:

```python
def checkout(request):
    info = request.ipregistry
    if info:
        currency = info.currency.code
        country = info.location.country.name
        is_vpn = info.security.is_vpn
```

On lookup failure (or when the client IP is private and no `DEVELOPMENT_IP` is set),
`request.ipregistry` evaluates to `None`-like falsy and the exception is available at
`request.ipregistry_error`. Prefer truthiness checks (`if request.ipregistry:`) over
`is None` because of the lazy wrapper — or call `get_ipregistry(request)` directly:

```python
from ipregistry_django import get_ipregistry

info = get_ipregistry(request)  # plain IpInfo or None, memoized, never raises
```

### Async views

```python
from ipregistry_django import aget_ipregistry

async def view(request):
    info = await aget_ipregistry(request)
```

### Direct lookups

For lookups outside the request cycle (Celery tasks, shell, ...), use the module-level
functions — they share the configured client and cache, and raise on failure:

```python
import ipregistry_django

response = ipregistry_django.lookup_ip("54.85.132.205")
print(response.data.location.country.name)

response = ipregistry_django.batch_lookup_ips(["8.8.8.8", "1.1.1.1"])
response = ipregistry_django.lookup_asn(42)
response = ipregistry_django.parse_user_agent("Mozilla/5.0 ...")
response = ipregistry_django.origin_lookup_ip()
```

### Blocking countries

Site-wide, with middleware:

```python
MIDDLEWARE = [
    # ...
    "ipregistry_django.middleware.BlockCountriesMiddleware",
]

IPREGISTRY = {
    "BLOCK_COUNTRIES": ["KP", "SY"],  # or ALLOW_COUNTRIES for an allow-list
}
```

Per-view, with decorators:

```python
from ipregistry_django.decorators import allow_countries, block_countries

@block_countries("KP", "SY")
def view(request): ...

@allow_countries("DE", "FR")
def eu_only_view(request): ...
```

Blocked visitors receive a `451 Unavailable For Legal Reasons` response. Visitors whose
country cannot be determined pass through (fail-open).

### Blocking threats

```python
MIDDLEWARE = [
    # ...
    "ipregistry_django.middleware.BlockThreatsMiddleware",
]

IPREGISTRY = {
    "BLOCK_THREATS": ["proxy", "tor", "vpn"],  # optional extra signals
}
```

```python
from ipregistry_django.decorators import block_threats

@block_threats("proxy", "tor")
def view(request): ...
```

IPs flagged as threats, attackers or abusers are always blocked with a `403`. Anonymizing
services (`anonymous`, `proxy`, `relay`, `tor`, `vpn`) are opt-in signals.

For class-based views, use Django's `method_decorator`:

```python
from django.utils.decorators import method_decorator

@method_decorator(block_threats("vpn"), name="dispatch")
class CheckoutView(View): ...
```

### GDPR / EU detection

```python
from ipregistry_django import is_bot, is_eu, is_threat

if is_eu(request, assume_eu=True):  # assume EU when unknown: show the banner
    ...

if is_threat(request, proxy=True, tor=True):
    ...

if is_bot(request):  # User-Agent heuristic, no API call, no credits
    ...
```

### Templates

Add the context processor to render lookup data directly in templates:

```python
TEMPLATES = [{
    "OPTIONS": {
        "context_processors": [
            # ...
            "ipregistry_django.context_processors.ipregistry",
        ],
    },
}]
```

```html
{% if ipregistry %}Hello, visitor from {{ ipregistry.location.country.name }}!{% endif %}
```

## Configuration

All settings live in a single optional `IPREGISTRY` dict. Defaults shown:

```python
IPREGISTRY = {
    "ALLOW_COUNTRIES": [],        # allow-list for BlockCountriesMiddleware
    "API_KEY": "",                # falls back to the IPREGISTRY_API_KEY env var
    "BASE_URL": None,             # None: default endpoint, "eu": EU endpoint, else verbatim
    "BLOCK_COUNTRIES": [],        # deny-list for BlockCountriesMiddleware
    "BLOCK_THREATS": [],          # extra signals for BlockThreatsMiddleware
    "CACHE": True,                # cache lookups with Django's cache framework
    "CACHE_ALIAS": "default",     # which CACHES alias to use
    "CACHE_KEY_PREFIX": "ipregistry",
    "CACHE_TTL": 600,             # seconds
    "CLIENT_IP_HEADER": None,     # e.g. "X-Forwarded-For" or "CF-Connecting-IP"
    "DEVELOPMENT_IP": None,       # public IP to use when the client IP is private
    "EXEMPT_PATHS": None,         # blocker-exempt path prefixes; None: STATIC_URL/MEDIA_URL
    "FAIL_OPEN": True,            # False: blockers answer 503 on lookup failure
    "FIELDS": None,               # e.g. "ip,location,security" to lower payload size
    "HOSTNAME": False,            # resolve reverse DNS hostnames (extra credits)
    "RETRIES": 1,
    "RETRY_INTERVAL": 1.0,
    "RETRY_ON_SERVER_ERROR": True,
    "RETRY_ON_TOO_MANY_REQUESTS": False,
    "TIMEOUT": 5,                 # seconds
}
```

`API_KEY` and `BASE_URL` also read the `IPREGISTRY_API_KEY` and `IPREGISTRY_BASE_URL`
environment variables when not set explicitly.

### Client IP and reverse proxies

By default the client IP comes from `REMOTE_ADDR`. Behind a reverse proxy or CDN, set
`CLIENT_IP_HEADER` to the header your proxy controls — never one clients can spoof:

```python
IPREGISTRY = {"CLIENT_IP_HEADER": "CF-Connecting-IP"}  # Cloudflare
IPREGISTRY = {"CLIENT_IP_HEADER": "X-Forwarded-For"}   # nginx with a correct set_real_ip
```

The first *public* address found wins. Private and reserved addresses are never sent to
the API; during local development, set `DEVELOPMENT_IP` to a fixed public IP so lookups
work from localhost.

### Caching

Lookups are cached in the `CACHE_ALIAS` Django cache for `CACHE_TTL` seconds (keys are
hashed, so any backend works, including Memcached). On top of that, results are memoized
per request, so accessing `request.ipregistry` repeatedly costs nothing.

### EU endpoint

To ensure requests are handled by Ipregistry nodes hosted in the EU only:

```python
IPREGISTRY = {"BASE_URL": "eu"}
```

### Errors

`request.ipregistry`, `get_ipregistry()`, guards, middleware and decorators never raise —
they fail open and log to the `"ipregistry"` logger (with anonymized IPs). Set
`"FAIL_OPEN": False` to have the blocking middleware and decorators answer `503` when a
lookup fails. The direct lookup functions raise `ipregistry.ApiError` /
`ipregistry.ClientError`.

Run `python manage.py check` to surface configuration problems (missing API key, unknown
setting keys, invalid country codes or threat signals, unknown cache alias).

## Testing your app

`fake_ipregistry` swaps the shared client for a stub — no HTTP, private test-client IPs
allowed:

```python
from ipregistry_django.testing import fake_ipregistry

def test_french_visitor(client):
    stubs = {"*": {"location": {"country": {"code": "FR", "name": "France"}}}}
    with fake_ipregistry(stubs) as fake:
        response = client.get("/")
        assert "Bienvenue" in response.text
        fake.assert_looked_up("127.0.0.1")
```

Stub specific IPs, `"*"` for any IP, `"origin"` for origin lookups; values can be dicts,
`IpInfo` instances or exceptions. Assertions include `assert_looked_up`,
`assert_looked_up_times`, `assert_not_looked_up`, `assert_nothing_looked_up`,
`assert_origin_looked_up` and `assert_user_agents_parsed`.

## Management command

```console
python manage.py ipregistry_lookup                  # this machine's public IP
python manage.py ipregistry_lookup 8.8.8.8 1.1.1.1  # one or more IPs
python manage.py ipregistry_lookup 8.8.8.8 --fields ip,location --hostname
```

## Resources

- [Ipregistry documentation](https://ipregistry.co/docs)
- [Response fields](https://ipregistry.co/docs/responses)
- [Python SDK](https://github.com/ipregistry/ipregistry-python)

## License

[Apache License 2.0](LICENSE)
