Official Django integration for the Ipregistry IP geolocation and threat data API.
Project description
Ipregistry Django
The official Django integration for the Ipregistry IP geolocation
and threat data API, built on top of
ipregistry-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_lookupcommand and Django system checks for misconfiguration.
Requirements
- Django 5.2+ (5.2 LTS and 6.0 are tested)
- Python 3.10+
Installation
pip install ipregistry-django
Set your API key (get one here), preferably via the environment:
export IPREGISTRY_API_KEY=YOUR_API_KEY
Then enable the app and middleware in settings.py:
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 object.
The API is only queried when the attribute is first accessed, at most once per request:
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:
from ipregistry_django import get_ipregistry
info = get_ipregistry(request) # plain IpInfo or None, memoized, never raises
Async views
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:
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:
MIDDLEWARE = [
# ...
"ipregistry_django.middleware.BlockCountriesMiddleware",
]
IPREGISTRY = {
"BLOCK_COUNTRIES": ["KP", "SY"], # or ALLOW_COUNTRIES for an allow-list
}
Per-view, with decorators:
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
MIDDLEWARE = [
# ...
"ipregistry_django.middleware.BlockThreatsMiddleware",
]
IPREGISTRY = {
"BLOCK_THREATS": ["proxy", "tor", "vpn"], # optional extra signals
}
from ipregistry_django.decorators import block_threats
@block_threats("proxy", "tor")
def view(request): ...
IPs flagged as threats, attackers or abusers are always blocked. Anonymizing services
(anonymous, proxy, relay, tor, vpn) are opt-in signals. Blocked requests raise
PermissionDenied — just like permission_required — so your project's handler403 and
custom 403 template render the response.
For class-based views, use Django's method_decorator:
from django.utils.decorators import method_decorator
@method_decorator(block_threats("vpn"), name="dispatch")
class CheckoutView(View): ...
GDPR / EU detection
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:
TEMPLATES = [{
"OPTIONS": {
"context_processors": [
# ...
"ipregistry_django.context_processors.ipregistry",
],
},
}]
{% if ipregistry %}Hello, visitor from {{ ipregistry.location.country.name }}!{% endif %}
Configuration
All settings live in a single optional IPREGISTRY dict. Defaults shown:
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:
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:
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:
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
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
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ipregistry_django-1.1.0.tar.gz.
File metadata
- Download URL: ipregistry_django-1.1.0.tar.gz
- Upload date:
- Size: 21.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04f6524a46491e812573685dd56f0fc5f192c34c70c2fe44775b66fd22a6f763
|
|
| MD5 |
fa84336434c3c281945da086fc38688e
|
|
| BLAKE2b-256 |
e3cf764864244f13794620577bb6a622bfda828d0b9a61770ca3569cdb337384
|
Provenance
The following attestation bundles were made for ipregistry_django-1.1.0.tar.gz:
Publisher:
release.yaml on ipregistry/ipregistry-django
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ipregistry_django-1.1.0.tar.gz -
Subject digest:
04f6524a46491e812573685dd56f0fc5f192c34c70c2fe44775b66fd22a6f763 - Sigstore transparency entry: 2128088686
- Sigstore integration time:
-
Permalink:
ipregistry/ipregistry-django@b60d12cab89bef2803df09afc1fdb3f0027db9ce -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ipregistry
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@b60d12cab89bef2803df09afc1fdb3f0027db9ce -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file ipregistry_django-1.1.0-py3-none-any.whl.
File metadata
- Download URL: ipregistry_django-1.1.0-py3-none-any.whl
- Upload date:
- Size: 31.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c23c925ef99c665500649ca860968cee58bc6406afe11192274c466d582f1db8
|
|
| MD5 |
e4272f15688ae740527113fbf50a7915
|
|
| BLAKE2b-256 |
229be7b85adea14cf456818aa83de175120b0e2afcec012a9c89c92b980e58f7
|
Provenance
The following attestation bundles were made for ipregistry_django-1.1.0-py3-none-any.whl:
Publisher:
release.yaml on ipregistry/ipregistry-django
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ipregistry_django-1.1.0-py3-none-any.whl -
Subject digest:
c23c925ef99c665500649ca860968cee58bc6406afe11192274c466d582f1db8 - Sigstore transparency entry: 2128088836
- Sigstore integration time:
-
Permalink:
ipregistry/ipregistry-django@b60d12cab89bef2803df09afc1fdb3f0027db9ce -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ipregistry
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@b60d12cab89bef2803df09afc1fdb3f0027db9ce -
Trigger Event:
workflow_dispatch
-
Statement type: