Location, network and abuse information for any IP address in one offline file.
No API, no rate limit, no lookup leaving the machine.
pip install "plevin[country,full]"
import plevin
found = plevin.lookup("1.1.1.1") # str, int, packed bytes or ipaddress object
Always a Result, never None. ValueError where the input is not an address.
>>> found.place.city.name, found.place.city.region.name, found.place.country.name
('Brisbane', 'Queensland', 'Australia')
>>> found.network.asn, found.network.operator.brand, found.network.cidr
(13335, 'Cloudflare', '1.1.1.0/24')
>>> exit_node = plevin.lookup("185.220.101.1")
>>> exit_node.abuse.service, exit_node.abuse.risk, exit_node.abuse.is_tor_exit_node
('tor_exit_node', 0.97, True)
The database is a separate wheel, found without being given a path. Install one, or several and the richest wins.
pip install "plevin[db]" |
16.9 MB | every field |
pip install "plevin[place]" |
6.3 MB | city, region, postal, coordinates, metro |
pip install "plevin[network]" |
10.3 MB | ASN, operator, routing, abuse |
pip install "plevin[country]" |
423 KB | the country code |
PLEVIN_DB=/path/to/plevin.plv or plevin.use("plevin.plv") reads a file of your own
instead; plevin.Plevin(path) opens one without touching the module's.
Every field
found.place is where the address is, found.network who announces it, found.abuse
what has been seen from it. Any of the three is None where the build carries none of
it, and every leaf is None rather than "" or 0 where a source says nothing.
Place(
lat=-27.4675,
lon=153.0281,
accuracy=200,
confidence=36,
granularity='city',
city=City(
id=2174003,
name='Brisbane',
ascii='Brisbane',
country='AU',
population=2780063,
elevation=27,
postal='4000',
postal_partial=None,
timezone='Australia/Brisbane',
type='regional capital',
capital='region',
region=Region(id=2152274, code='04', iso='AU-QLD', name='Queensland',
type='State'),
district=District(id=7839562, code='31000', name='Brisbane'),
metro=None,
),
country=Country(
code='AU',
name='Australia',
official=None,
common=None,
iso3='AUS',
numeric='036',
flag='🇦🇺',
european_union=False,
driving_side='left',
),
time=Time(
timezone='Australia/Brisbane',
abbreviation='AEST',
local='2026-08-13T19:20:00+10:00',
utc_offset='+10:00',
is_dst=False,
dst_start=None,
dst_end=None,
),
)
country and time are derived, not stored: country from the two-letter code
through pycountry, time from the zone name
through zoneinfo, both only with the full extra. Without it the code, the flag, the
EU and driving-side answers and the zone name still come through. capital says which
capital the city is, region.iso is ISO 3166-2 and region.code the GeoNames admin1
number, postal_partial is the leading part of postal a source could only narrow
that far.
Network(
asn=13335,
handle='CLOUDFLARENET',
prefix=24,
cidr='1.1.1.0/24',
start='1.1.1.0',
end='1.1.1.255',
rpki='valid',
roas=1,
operator=Operator(
company='Cloudflare, Inc.',
brand='Cloudflare',
domain='cloudflare.com',
website='https://www.cloudflare.com',
category='content',
tier=2,
peering=356,
scope='Global',
rir='arin',
since=2010,
street='101 Townsend St',
state='CA',
postal='94107-1934',
country='US',
abuse_email='abuse@cloudflare.com',
city=City(name='San Francisco', ...), # a full City, as above
),
carrier=Carrier(user_type='hosting', user_count=19, mcc=None, mnc=None,
is_mobile=False),
)
cidr is the announcement the address falls in, masked out of the address itself, so
1.1.1.1 and 1.0.0.1 reach one operator through two prefixes. rpki is valid,
invalid or unknown and roas how many ROAs agree. brand drops the legal form and
the words every network carries, so GOOGLE and Google LLC both read Google;
domain is the host of website, else of abuse_email. tier is 1 transit-free, 2
has customers, 3 edge; peering the exchange count; category one of residential,
business, hosting, education, government, military, cdn, content,
infrastructure, cellular, search_engine_spider, traveler, transit, exchange
or non-profit.
>>> plevin.lookup("185.220.101.1").abuse
Abuse(
name='Tor',
service='tor_exit_node',
evidence='measured',
risk=0.97,
network_risk=None,
last_seen_days=1,
is_anycast=False,
is_satellite=False,
is_hosting_provider=True,
is_proxy=False,
is_public_proxy=False,
is_residential_proxy=False,
is_anonymous_vpn=False,
is_tor_exit_node=True,
is_private_relay=False,
is_anonymous=True,
)
risk is 0 to 1 for the address, network_risk the same for the whole ASN, None
where nothing has ever been seen — which is not a risk of zero. evidence is
published, measured, reported or inferred, strongest first; service is
tor_exit_node, private_relay, anonymous_vpn, residential_proxy or
public_proxy, most specific first. A public proxy on a residential or cellular line
reads as residential_proxy with evidence='inferred'. The ten booleans are read off
service and the carrier's type, never stored.
What an address says on its own
Answered without the database, so they hold for every address:
>>> found = plevin.lookup("2606:4700::1111")
>>> found.number, found.compressed
(50543257672059871404715951523469725969, '2606:4700::1111')
>>> found.expanded
'2606:4700:0000:0000:0000:0000:0000:1111'
>>> found.arpa
'1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.6.0.6.2.ip6.arpa'
is_global and is_bogon, then is_private, is_loopback, is_multicast,
is_reserved, is_link_local, is_unique_local, is_documentation, is_shared
(100.64/10) and is_benchmark (198.18/15), bisected out of the IANA special-purpose
registries.
>>> plevin.lookup("::ffff:8.8.8.8").tunnel, plevin.lookup("::ffff:8.8.8.8").embedded_ipv4
('ipv4-mapped', '8.8.8.8')
>>> plevin.lookup("2002:808:808::1").is_6to4
True
tunnel is ipv4-mapped, 6to4, teredo, nat64 or None, with embedded_ipv4
the address it carries; is_ipv4_mapped, is_6to4 and is_teredo beside it.
Good for
- Country routing, pricing and compliance without a third-party call
- Local time and flag before the user types anything
- Bulk log enrichment, at 2M lookups/s on repeats
- Abuse handling and RPKI triage in the same process, no whois or RDAP
- Air-gapped deployments, where no address leaves the process
Data
| v4 boundaries | 2,182,442, plus 3,832,016 host rows |
| v6 boundaries | 375,752 |
| cities | 76,805 in 3,177 regions |
| districts, metros | 19,941 and 210 |
| ASNs, operators | 86,237 and 80,354 |
| timezones | 394 |
| abuse records | 2,147 over 156 feeds |
Rebuilt monthly from MaxMind GeoLite2, IP2Location LITE, GeoNames, Natural Earth, a
RIPE RIS RIB, RPKI ROAs, the NRO delegations, CAIDA, PeeringDB,
asn-abuse and the feeds in
builder/data/feeds.json. Plevin(path).built dates your
copy, .selection names which fields it carries and .fields lists them.
Python 3.10+. No dependencies on 3.14, where compression.zstd is in the standard
library; pyzstd below it. pycountry and, on Windows, tzdata come with the full
extra.
Speed
| open | 2 ms, mmapped and read-only |
| first answer | 12 ms |
| repeats | 2,060,000/s |
| uniformly random v4 | 16,000/s, every one a fresh block decode |
Blocks decode on reach and stay decoded, so a real log lands between the two: the boundary a lookup found, the rows it linked to and the answer itself are all kept.
Your own file
from plevin import Plevin
with_places = Plevin("dist/plevin.metro-place.plv")
with_places.lookup("1.1.1.1").place.city.name
Read-only and memory-mapped, so processes and threads share one file. For the stored
rows without any of the shaping above — dictionaries, codes already read as words —
Plevin(path).file.row(value, wide) is the reader underneath.
Builder
The Rust builder, its sources, the file format and the selection language are in
builder/README.md.
cd builder && cargo build --release
./target/release/plevin-builder # dist/plevin.plv, every field
./target/release/plevin-builder place+metro # dist/plevin.metro-place.plv
Mini file
plevin_mini.py is the lookup with no package around it.
Drop it beside a .plv and it runs.
python plevin_mini.py plevin.plv 8.8.8.8
>>> from plevin_mini import Plevin
>>> Plevin("plevin.plv").lookup("8.8.8.8")["network"]["asn"]
15169
Plain dictionaries of the stored rows, codes already read as words, and nothing derived: no models, no country, no clock, no discovery. 3,600,000 lookups a second warm. It is linted and type-checked with the package.
Development
cd python
uv run pytest # 168 tests, 100% branch coverage
uv run mypy
uv run basedpyright
uvx ruff check . ../plevin_mini.py --config pyproject.toml
uv build --wheel
cd ../builder && cargo fmt --check && cargo clippy
License
Apache 2.0 for the reader and the builder, see LICENSE. The database carries
the licenses of the sources it was built from, listed in
builder/README.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
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 plevin-0.1.0-py3-none-any.whl.
File metadata
- Download URL: plevin-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae8e72e00378d616c296f25e562b10f03f37b88d91ce9a61e831f4de8570b5c3
|
|
| MD5 |
3410c9464085b5db72a1d57a16d9fad5
|
|
| BLAKE2b-256 |
c88764c0df6ce9dc3becac3ed7d4efc4e7d9a44366dcc83fdf133af4bdba306e
|
Provenance
The following attestation bundles were made for plevin-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on tn3w/plevin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
plevin-0.1.0-py3-none-any.whl -
Subject digest:
ae8e72e00378d616c296f25e562b10f03f37b88d91ce9a61e831f4de8570b5c3 - Sigstore transparency entry: 2449301221
- Sigstore integration time:
-
Permalink:
tn3w/plevin@2f141f812cb2a95589e24f0aa476ac7216dafd17 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/tn3w
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2f141f812cb2a95589e24f0aa476ac7216dafd17 -
Trigger Event:
push
-
Statement type: