Official Ipregistry integration for FastAPI: dependency-driven IP geolocation and threat detection for every request
Project description
Ipregistry FastAPI
Official Ipregistry integration for FastAPI: dependency-driven IP geolocation and threat detection for every request, built on the asynchronous Ipregistry Python client.
from fastapi import FastAPI
from ipregistry_fastapi import IpInfoDep, ipregistry_lifespan
app = FastAPI(lifespan=ipregistry_lifespan)
@app.get("/")
async def index(info: IpInfoDep):
return {"country": info.location.country.name if info else None}
Features
- Async-first: non-blocking lookups through the official asynchronous Ipregistry client, with connection reuse managed by the application lifespan.
- Idiomatic dependencies:
IpInfoDep,IpregistryDepandClientIpDepannotated types plug straight into path operations — HTTP and WebSocket alike; one cached lookup per request, shared by every consumer. All dependencies are coroutines: no threadpool dispatch on the request path. - Guards: block countries (HTTP 451) and threats (HTTP 403) per route, per router, or app-wide.
- Middleware: optional pure-ASGI middleware for global enrichment and blocking, with static asset and bot skipping to save credits.
- Safe by default: fails open, never sends private or reserved addresses to the API, validates proxy headers, and never logs full client IPs.
- Configuration via environment:
IPREGISTRY_*variables parsed with pydantic-settings. - Typed responses: lookups return the client's Pydantic v2 models, ready to embed in your own response models.
- First-class testing:
IpregistryFakeswaps in canned responses with call assertions — no network, no API key.
Getting started
Requirements
- Python 3.10+
- FastAPI 0.115+
- An Ipregistry API key — the free tier includes 100,000 lookups
Installation
pip install ipregistry-fastapi
or with uv:
uv add ipregistry-fastapi
Quick start
Set your API key:
export IPREGISTRY_API_KEY=YOUR_API_KEY
Wire the lifespan and use the dependency:
from fastapi import FastAPI
from ipregistry_fastapi import IpInfoDep, ipregistry_lifespan
app = FastAPI(lifespan=ipregistry_lifespan)
@app.get("/whoami")
async def whoami(info: IpInfoDep):
if info is None:
return {"detail": "No IP intelligence available"}
return {
"ip": info.ip,
"country": info.location.country.name,
"city": info.location.city,
"threat": info.security.is_threat,
}
IpInfoDep resolves to an IpInfo model — or None when the
client IP is private (e.g. on localhost) or the lookup failed. One lookup runs per request no
matter how many dependencies, guards or middleware consume it, and responses are cached in memory
across requests.
To configure programmatically instead of via the environment:
from ipregistry_fastapi import IpregistrySettings, add_ipregistry, ipregistry_lifespan
app = FastAPI(lifespan=ipregistry_lifespan)
add_ipregistry(app, IpregistrySettings(api_key="...", base_url="eu", fields="location,security"))
Blocking countries
block_countries returns a dependency enforcing an ISO 3166-1 alpha-2 country list. Blocked
visitors receive 451 Unavailable For Legal Reasons by default. Invalid codes fail at declaration
time, not at request time.
from fastapi import APIRouter, Depends
from ipregistry_fastapi import block_countries
router = APIRouter(dependencies=[Depends(block_countries("KP", "SY"))])
# Allowlist mode: only serve the EU single market
checkout = APIRouter(dependencies=[Depends(block_countries(EU_COUNTRIES, mode="allow"))])
Requests whose country cannot be determined are allowed unless you pass unknown="block".
Blocking threats
block_threats blocks IPs flagged as threats, attackers or abusers, and responds with 403.
Anonymization signals are opt-in — a VPN user is not necessarily hostile:
from fastapi import Depends, FastAPI
from ipregistry_fastapi import block_threats
app = FastAPI(dependencies=[Depends(block_threats())])
# Stricter variant for sensitive routes:
admin = APIRouter(dependencies=[Depends(block_threats(proxy=True, tor=True, vpn=True, relay=True))])
Requests without security data are allowed (fail open), so an Ipregistry outage never locks users out of your API.
Middleware
Prefer dependencies for per-route rules. Reach for the middleware to enforce app-wide policies or eagerly enrich every request — including ones that never match a route:
from ipregistry_fastapi import IpregistryMiddleware, block_countries, block_threats
app.add_middleware(
IpregistryMiddleware,
actions=[block_countries("KP"), block_threats(tor=True)],
skip_bots=True, # skip lookups for crawler user agents
skip_static_assets=True, # default: skip .css/.js/images/fonts
skip=lambda request: request.url.path.startswith("/health"),
)
The middleware stores the lookup on request.state.ipregistry, so IpInfoDep and route guards
reuse it for free.
Fail open or closed
By default a failed lookup lets the request proceed with info = None (fail open). To refuse
requests instead when IP intelligence is unavailable:
- set
IPREGISTRY_FAIL_OPEN=false—IpInfoDepand blocking dependencies respond with503; - or pass
fail_closed=True(or a custom status code) toIpregistryMiddleware.
GDPR & EU detection
from ipregistry_fastapi import IpInfoDep, is_eu
@app.get("/consent")
async def consent(info: IpInfoDep):
return {"show_banner": is_eu(info, assume_eu=True)}
is_eu reads location.in_eu; assume_eu=True errs on the side of showing a consent banner when
the location is unknown. For EU data residency, set IPREGISTRY_BASE_URL=eu to route all API
requests to https://eu.api.ipregistry.co.
Related helpers: is_threat(info, vpn=True, ...) and is_bot(request) (heuristic, no API call).
Configuration
All settings load from the environment via
pydantic-settings, or can be passed
programmatically with IpregistrySettings:
| Environment variable | Default | Description |
|---|---|---|
IPREGISTRY_API_KEY |
— | Required. Your Ipregistry API key. |
IPREGISTRY_BASE_URL |
default | API endpoint; eu selects the EU-only endpoint; full URLs verbatim. |
IPREGISTRY_FIELDS |
all | Default field selection, e.g. location,security. |
IPREGISTRY_HOSTNAME |
false |
Resolve the hostname of looked up IPs. |
IPREGISTRY_TIMEOUT |
5.0 |
Request timeout in seconds. |
IPREGISTRY_RETRIES |
1 |
Retry attempts after a failed request. |
IPREGISTRY_RETRY_INTERVAL |
0.5 |
Base delay between retries (exponential backoff). |
IPREGISTRY_RETRY_ON_SERVER_ERROR |
true |
Retry on 5xx responses. |
IPREGISTRY_RETRY_ON_TOO_MANY_REQUESTS |
false |
Retry on 429 responses (honors Retry-After). |
IPREGISTRY_CACHE_ENABLED |
true |
Cache lookups in memory. |
IPREGISTRY_CACHE_MAX_SIZE |
2048 |
Maximum cached lookups. |
IPREGISTRY_CACHE_TTL |
600 |
Cache time-to-live in seconds. |
IPREGISTRY_DEVELOPMENT_IP |
— | Fallback IP when the client IP is private (e.g. localhost). |
IPREGISTRY_FAIL_OPEN |
true |
Proceed without data when a lookup fails. |
IPREGISTRY_IP_SOURCE |
client |
How to resolve the client IP (see below). |
Selecting only the fields you need (e.g. IPREGISTRY_FIELDS=location,security) reduces latency
and response size.
IP extraction behind proxies
By default (client) the library uses the ASGI client address — the right choice when uvicorn
runs with --proxy-headers --forwarded-allow-ips or behind
ProxyHeadersMiddleware, which already resolve the real client IP.
If your server does not rewrite the client address, pick the header set by a proxy you trust:
IPREGISTRY_IP_SOURCE |
Headers consulted (in order) |
|---|---|
client (default) |
none — ASGI client address |
auto |
X-Real-IP, X-Forwarded-For |
cloudflare |
CF-Connecting-IP |
nginx |
X-Real-IP, X-Forwarded-For |
forwarded-for |
X-Forwarded-For (first entry) |
header:<name> |
the named header |
A callable ip_extractor can also be passed to Ipregistry/add_ipregistry for full control.
Values are sanitized (ports, IPv6 brackets and zone IDs stripped) and validated; only configure
headers your proxy overwrites — client-supplied headers are trivially spoofable. Private,
loopback and carrier-grade NAT addresses are never sent to the API; set
IPREGISTRY_DEVELOPMENT_IP to test with a fixed address on localhost.
Caching and credits
Lookups are cached in memory (2048 entries, 10 minutes by default), so repeated requests from the same address consume a single credit. On top of that:
- one lookup per request, however many consumers;
- static asset and bot skipping in the middleware;
- private/reserved IPs short-circuit before reaching the API.
Multi-worker deployments (uvicorn --workers, gunicorn) hold one cache per process.
Testing your app
IpregistryFake is a drop-in service with canned responses and no network access:
from fastapi.testclient import TestClient
from ipregistry_fastapi import add_ipregistry
from ipregistry_fastapi.testing import IpregistryFake
fake = IpregistryFake({
"66.165.2.7": {"location": {"country": {"code": "US"}}},
"*": {"security": {"is_threat": False}}, # any other address
})
add_ipregistry(app, service=fake)
client = TestClient(app)
response = client.get("/whoami", headers={"x-real-ip": "66.165.2.7"})
fake.assert_looked_up("66.165.2.7")
Stubs are API-shaped dicts, IpInfo instances, or exceptions to raise. Unlike the real service,
the fake accepts private addresses so requests made through TestClient resolve out of the box.
Assertions: assert_looked_up, assert_not_looked_up, assert_looked_up_times,
assert_nothing_looked_up, assert_origin_looked_up, assert_user_agents_parsed.
FastAPI's standard dependency_overrides
mechanism works too, and propagates to IpInfoDep and the blocking guards:
from ipregistry_fastapi import get_ipregistry
app.dependency_overrides[get_ipregistry] = lambda: fake
Errors
Request-aware helpers (IpInfoDep, guards, middleware) never raise on lookup failure — they fail
open and record the exception on request.state.ipregistry_error. Explicit lookups raise the
client's exceptions:
from ipregistry_fastapi import ApiError, ClientError, ErrorCode, IpregistryDep
@app.get("/lookup/{ip}")
async def lookup(ip: str, ipregistry: IpregistryDep):
try:
return await ipregistry.lookup(ip)
except ApiError as error:
if error.error_code == ErrorCode.INVALID_IP_ADDRESS:
raise HTTPException(status_code=400, detail=error.message)
raise HTTPException(status_code=502, detail="Lookup failed")
except ClientError:
raise HTTPException(status_code=504, detail="Ipregistry unreachable")
Beyond IP lookups
The full asynchronous client remains available for batch lookups, ASN data, user-agent parsing and credits introspection:
@app.post("/enrich")
async def enrich(ips: list[str], ipregistry: IpregistryDep):
results = await ipregistry.lookup_batch(ips) # order-preserving
origin = await ipregistry.lookup_origin() # the server's own IP
asn = await ipregistry.client.lookup_asn(400923) # raw client escape hatch
Migrating from ipregistry-python
Existing ipregistry client code keeps working — this package layers FastAPI wiring on top:
- replace manual client creation with
ipregistry_lifespan/add_ipregistry; - replace per-endpoint lookups of the request IP with
IpInfoDep; - move country/threat checks into
block_countries/block_threatsdependencies; - configuration moves to
IPREGISTRY_*environment variables.
Other 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_fastapi-1.0.0.tar.gz.
File metadata
- Download URL: ipregistry_fastapi-1.0.0.tar.gz
- Upload date:
- Size: 22.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23bfca24e2a096412a0a76925a58743d5863176c3e0bd1fc306f8e9b3a566dd9
|
|
| MD5 |
60c6c44b5d6a4b95e2daea33b092a830
|
|
| BLAKE2b-256 |
bc9c63f8891adbc937731fae2dbcbe7ad0305cb3e0771daeb600b9e95ef12208
|
Provenance
The following attestation bundles were made for ipregistry_fastapi-1.0.0.tar.gz:
Publisher:
release.yaml on ipregistry/ipregistry-fastapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ipregistry_fastapi-1.0.0.tar.gz -
Subject digest:
23bfca24e2a096412a0a76925a58743d5863176c3e0bd1fc306f8e9b3a566dd9 - Sigstore transparency entry: 2125565175
- Sigstore integration time:
-
Permalink:
ipregistry/ipregistry-fastapi@059b0f5b5c45e7fb5d407d6cea6f3b3056081e81 -
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@059b0f5b5c45e7fb5d407d6cea6f3b3056081e81 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file ipregistry_fastapi-1.0.0-py3-none-any.whl.
File metadata
- Download URL: ipregistry_fastapi-1.0.0-py3-none-any.whl
- Upload date:
- Size: 31.3 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 |
b54c9e4ccc47dd9e869650f54c364ae492785014e315a63408490c4c9388ccbc
|
|
| MD5 |
d3bf3e3bfe6b8c0feeb72483b634d277
|
|
| BLAKE2b-256 |
e09bcd2fb8d8cfaa812d7de825f98e705a238aae9df5e9b64ce52d92dba16252
|
Provenance
The following attestation bundles were made for ipregistry_fastapi-1.0.0-py3-none-any.whl:
Publisher:
release.yaml on ipregistry/ipregistry-fastapi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ipregistry_fastapi-1.0.0-py3-none-any.whl -
Subject digest:
b54c9e4ccc47dd9e869650f54c364ae492785014e315a63408490c4c9388ccbc - Sigstore transparency entry: 2125565351
- Sigstore integration time:
-
Permalink:
ipregistry/ipregistry-fastapi@059b0f5b5c45e7fb5d407d6cea6f3b3056081e81 -
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@059b0f5b5c45e7fb5d407d6cea6f3b3056081e81 -
Trigger Event:
workflow_dispatch
-
Statement type: