Skip to content

Python

The originet package wraps the Rust table with PyO3. It has no Python dependencies and ships type stubs (py.typed).

Install

From PyPI:

uv pip install originet          # or: pip install originet

Wheels are published for Linux x86_64 (manylinux2014) and aarch64 (manylinux_2_28), Linux with musl, such as Alpine (musllinux_1_2, x86_64 and aarch64), macOS (Intel, Apple Silicon) and Windows, for CPython 3.10 and newer (abi3), and are also attached to each GitHub release. Anything else falls back to the sdist, which compiles the extension and needs a Rust toolchain 1.88+. The repository's main installs the same way:

uv pip install "originet @ git+https://github.com/estcarisimo/originet#subdirectory=python"

Loading a table

from originet import Table

table = Table.from_caida()                                   # newest IPv4 + IPv6 snapshots
table = Table.from_caida("2019-06-01", ipv6=False)           # newest on or before that day, IPv4 only
table = Table.from_caida(cache_dir="/data/pfx2as", offline=True)   # only what is cached
table = Table.from_files(ipv4="routeviews-rv2-20261021-0800.pfx2as.gz",
                         ipv6="routeviews-rv6-20261021-1200.pfx2as.gz")

from_caida takes date ("YYYY-MM-DD" or None), and keyword-only cache_dir, ipv4, ipv6, offline, refresh. It raises ValueError for a bad date or a date before the dataset, OSError when offline and something is not cached or a file cannot be read, and RuntimeError for a failed download. Loading the IPv4 snapshot (1.1 M prefixes) takes about a second; the table is immutable afterwards and safe to share between threads.

table.snapshots lists what was loaded:

>>> table.snapshots
[{'family': 'ipv4', 'source': 'caida', 'date': '2026-10-21',
  'file': 'routeviews-rv2-20261021-0800.pfx2as.gz',
  'location': 'https://publicdata.caida.org/datasets/routing/routeviews-prefix2as/2026/10/routeviews-rv2-20261021-0800.pfx2as.gz',
  'prefixes': 1129523}, ...]
>>> table.len_v4, table.len_v6, len(table)
(1129523, 265790, 1395313)

Looking up

>>> m = table.lookup("1.1.8.9")
>>> m
Match(ip='1.1.8.9', prefix='1.1.8.0/24', asn=149511, origins='149511_138421')
>>> m.asn, m.prefix, m.origins, m.asns(), m.is_multi_origin, m.is_as_set
(149511, '1.1.8.0/24', '149511_138421', [149511, 138421], True, False)
>>> table.lookup("10.0.0.1") is None
True
>>> table.lookup("not an ip")
ValueError: "not an ip": invalid IP address syntax

For many addresses use the bulk methods; they parse and look up in Rust with the GIL released, so other threads keep running:

hits = table.lookup_many(ips)                  # list[Match | None], same order as ips
asns = table.lookup_asns(ips)                  # list[int | None], cheaper when only the ASN matters
asns = table.lookup_asns(ips, strict=False)    # invalid inputs become None instead of raising

Match objects are immutable, hashable and compare by ip, prefix and origins.

With pandas

import pandas as pd
from originet import Table

df = pd.read_csv("flows.csv")
table = Table.from_caida("2024-01-01")
hits = table.lookup_many(df["dst_ip"].astype(str).tolist(), strict=False)
df["asn"] = [h.asn if h else None for h in hits]
df["prefix"] = [h.prefix if h else None for h in hits]

Development

cd python
uv sync --locked          # builds the extension in editable mode
uv run pytest -q
uv run ruff check . && uv run ruff format --check . && uv run mypy

The tests use the fixtures in ../tests/fixtures/ and never touch the network. On an Apple Silicon Mac the Rust toolchain must be the arm64 one (rustup); an x86_64 Homebrew toolchain cannot build the extension for an arm64 Python.