Overview
The IPFly Python SDK wraps the IPFly API in a single dependency-free module. It relies only on the standard library (urllib, threading, ipaddress, json) and handles the operational details — caching, retries, rate limiting, and thread-safe request de-duplication — so your integration code can stay focused on what to do with the response.
TTL + LRU caching
In-memory by default, or persisted to a JSON file across process runs for CLI scripts and cron jobs.
Automatic retries
Exponential backoff with jitter on network errors and 5xx responses. 4xx fails fast, no wasted retries.
Thread-safe de-duplication
Two threads looking up the same IP at the same moment share a single HTTP request.
Batch lookups
A bounded ThreadPoolExecutor looks up many IPs at once; a bad IP never fails the whole batch.
Client-side rate limiting
A token-bucket limiter smooths bursts so you don't blow through your plan's requests/second.
Zero dependencies
One file, standard library only. Python 3.7+. MIT licensed.
Where This SDK Belongs
This client is meant to run server-side — in a script, a web backend, a worker, a notebook. It is not meant to be shipped inside a distributed application (a desktop app, a compiled mobile app, anything an end user's machine executes) where your API token could be extracted from the binary.
If you need geolocation results in a browser-facing app, put this SDK behind your own backend endpoint and call that endpoint from the client (see the Flask proxy example below), or use the IPFly JavaScript SDK with a domain-restricted token for prototyping.
Installation
No PyPI package is published — copy ipfly_sdk.py into your project, or host it internally and pull it in your build step.
# Drop ipfly_sdk.py directly into your source tree, e.g.: cp ipfly_sdk.py my_project/vendor/ipfly_sdk.py
curl -O https://ipfly.world/libs/python-sdk/ipfly_sdk.py # or wget https://ipfly.world/libs/python-sdk/ipfly_sdk.py
from ipfly_sdk import IPFlyClient, IPFlyError, is_valid_ip client = IPFlyClient(token="YOUR_TOKEN")
Quick Start
Create a client once, then call lookup() wherever you need geolocation data.
from ipfly_sdk import IPFlyClient, IPFlyError
client = IPFlyClient(
token="YOUR_TOKEN",
include="security", # optional: request the security/ASN field set on every call
)
# Look up a specific IP
data = client.lookup("8.8.8.8")
print(data["city"], data["country_name"], data["security"]["is_vpn"])
# Look up the caller's own IP (omit the ip argument)
me = client.lookup_self()
print("You appear to be in", me["city"])
# Handle errors explicitly
try:
client.lookup("not-an-ip")
except IPFlyError as err:
print(err.code, err.message)
# Use as a context manager to clean up cache/state automatically
with IPFlyClient(token="YOUR_TOKEN") as c:
print(c.lookup("1.1.1.1")["country_name"])Client Parameters
All parameters are passed to IPFlyClient(...).
| Parameter | Type | Default | Description |
|---|---|---|---|
token | str | required | Your IPFly API token. |
base_url | str | ipfly.world/api | Override to point at your own backend proxy. |
include | str | None | Default include value sent with every request (e.g. "security"). Overridable per call. |
timeout | float | 8.0 | Per-request timeout in seconds. |
retries | int | 2 | Retry attempts for network errors and 5xx. 4xx is never retried. |
retry_delay | float | 0.3 | Base delay (seconds) for exponential backoff; jitter is added automatically. |
rate_limit | float | 0 | Max requests/second this client will issue. 0 = unlimited. |
concurrency | int | 6 | Default max worker threads for batch_lookup. |
cache | dict | see below | Response cache settings — see Cache Options. |
opener | OpenerDirector | None | Custom urllib.request opener — for corporate proxies, custom SSL contexts, etc. |
debug | bool | False | Print internal lifecycle events to stdout (tokens are redacted). |
on_request | callable | None | (ip, url) -> None — fired right before a network request is sent. |
on_response | callable | None | (ip, data, from_cache) -> None — fired after a successful lookup. |
on_error | callable | None | (ip, error) -> None — fired when a lookup ultimately fails, after retries. |
on_cache_hit | callable | None | (ip, data) -> None — fired when a cached response is served instead of a network call. |
Cache Options
Passed as the cache dict, e.g. IPFlyClient(token=..., cache={"storage": "file", "ttl": 3600}).
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | True | Set False to disable caching entirely. |
ttl | float | 300 | How long a cached response stays valid, in seconds. |
max_size | int | 500 | Max cached entries before least-recently-used entries are evicted. |
storage | str | 'memory' | 'memory' or 'file'. 'file' persists the cache to a JSON file across process runs — good for CLI scripts and cron jobs, not built for high write concurrency. |
path | str | .ipfly_cache.json | File path used when storage='file'. |
.lookup() & .lookup_self()
client.lookup(ip=None, include=None, skip_cache=False, timeout=None) looks up a single IP. Omit ip (or pass None) to geolocate the caller — or use the client.lookup_self(**kwargs) shorthand.
client.lookup(
"1.1.1.1",
include="security", # overrides the client-level `include` for this call only
skip_cache=True, # force a fresh network request, bypassing the cache
timeout=3.0, # override the client's default timeout for this call
)Returns the parsed JSON response as a dict (see Response Shape). Raises IPFlyError on failure (see Error Handling).
.batch_lookup()
client.batch_lookup(ips, concurrency=None, **kwargs) looks up a list of IPs using a bounded ThreadPoolExecutor (concurrency overrides the client default). This call never raises for an individual IP failure — it returns a list of BatchResult in the same order as ips.
from ipfly_sdk import BatchResult
results = client.batch_lookup(["8.8.8.8", "1.1.1.1", "not-an-ip"])
for r in results:
if r.ok:
print(r.ip, "->", r.data["city"])
else:
print(r.ip, "failed:", r.error.code)BatchResult is a small dataclass: ip: str, ok: bool, data: Optional[dict], error: Optional[IPFlyError].
Cache Controls
| Method | Returns | Description |
|---|---|---|
client.clear_cache() | None | Empties the response cache immediately. |
client.cache_stats() | dict | Returns {"enabled", "size", "ttl", "max_size"} — useful for logging or a health-check endpoint. |
client.close() | None | Clears the cache and any in-flight request bookkeeping. Called automatically when used as a context manager. |
is_valid_ip()
A module-level helper that validates an IPv4 or IPv6 string without making a request.
from ipfly_sdk import is_valid_ip
is_valid_ip("8.8.8.8") # True
is_valid_ip("2001:4860:4860::8888") # True
is_valid_ip("not-an-ip") # FalseResponse Shape
Successful lookups return the same JSON structure the IPFly API returns, unchanged, as a Python dict — see the full field reference in the API docs. Shape depends on your plan and the include value requested.
{
"ip": "8.8.8.8",
"hostname": "dns.google",
"country_name": "United States",
"city": "Mountain View",
"latitude": "37.4056",
"longitude": "-122.0775",
"time_zone": {"name": "America/New_York", "...": "..."},
"asn": {"asn": "AS15169", "name": "Google LLC", "...": "..."},
"security": {"is_vpn": False, "is_tor": False, "...": "..."},
}Error Handling
All failures raise IPFlyError, a plain Exception subclass carrying message, status, code, and cause attributes.
| Code | Meaning |
|---|---|
| MISSING_TOKEN | No token was supplied when creating the client. |
| INVALID_IP | The IP string failed local validation before any request was sent. |
| TIMEOUT | The request exceeded timeout. |
| NETWORK_ERROR | The request failed before a response was received (offline, DNS, connection refused, etc). |
| HTTP_ERROR | The API responded with a non-2xx status — check status (401 = bad token, 403 = plan/permission issue, 404 = private/bogon IP, 429/5xx = retry-worthy). |
| BAD_RESPONSE | The server responded but the body wasn't valid JSON. |
from ipfly_sdk import IPFlyClient, IPFlyError
client = IPFlyClient(token="YOUR_TOKEN")
try:
client.lookup("8.8.8.8")
except IPFlyError as err:
if err.code == "TIMEOUT":
... # retry later, log, fall back to a default
elif err.status == 401:
... # token is invalid — surface a config error, don't retry
print(err.message)Performance Notes
Caching
Avoids re-querying the same IP within the TTL window — handy for a web app looking up the same visitor repeatedly, or a batch job re-processing overlapping log files.
De-duplication
If two request-handling threads call lookup("1.2.3.4") at the same moment, only one HTTP request goes out; both get the same result.
Rate limiting
A courtesy limiter on the client side that smooths bursts (e.g. a bulk-enrichment script) so you don't blow through your plan's requests/second in a tight loop.
Batching
Uses a bounded ThreadPoolExecutor, not raw asyncio — good for I/O-bound HTTP calls with no async dependency. Already on asyncio? Wrap calls with loop.run_in_executor(None, client.lookup, ip).
Examples
Two common integration patterns: enriching a CSV of visitor IPs in bulk, and proxying instead of embedding your token in a client-facing app.
import csv
from ipfly_sdk import IPFlyClient
client = IPFlyClient(token="YOUR_TOKEN", cache={"storage": "file", "path": "geo_cache.json"})
with open("visitors.csv") as infile:
reader = csv.DictReader(infile)
ips = [row["ip"] for row in reader]
for result in client.batch_lookup(ips, concurrency=8):
if result.ok:
print(result.ip, "->", result.data["country_name"])
else:
print(result.ip, "-> error:", result.error.code)from flask import Flask, request, jsonify
from ipfly_sdk import IPFlyClient, IPFlyError
import os
app = Flask(__name__)
client = IPFlyClient(token=os.environ["IPFLY_TOKEN"])
@app.get("/api/geo")
def geo():
ip = request.args.get("ip") or request.remote_addr
try:
return jsonify(client.lookup(ip, include="security"))
except IPFlyError as err:
return jsonify({"error": err.code, "message": err.message}), err.status or 502In the Flask example, your frontend (browser JS, mobile app, etc.) calls /api/geo on your own server — the IPFly token stays in this process and is never sent to the client.
Command-Line Usage
The module ships a tiny CLI for quick checks.
export IPFLY_TOKEN=your_token_here python -m ipfly_sdk 8.8.8.8 python -m ipfly_sdk # looks up your own IP python -m ipfly_sdk 8.8.8.8 --include security
Python Version Support
Requires Python 3.7+ (uses dataclasses and from __future__ import annotations). No third-party packages — works anywhere the standard library is available, including minimal containers and locked-down environments.
License
MIT — use it, fork it, ship it.