Metadata-Version: 2.4
Name: whereip
Version: 0.1.6
Summary: The whole world's IP addresses, located offline in 3.7 MB
Project-URL: Documentation, https://github.com/tn3w/whereip#readme
Project-URL: Source, https://github.com/tn3w/whereip
Project-URL: Changelog, https://github.com/tn3w/whereip/blob/master/CHANGELOG.md
Project-URL: Funding, https://www.buymeacoffee.com/tn3w
Author-email: tn3w <tn3w@protonmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: geoip,geolocation,ip,offline
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Internet
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: tzdata; sys_platform == 'win32'
Provides-Extra: metro
Requires-Dist: whereip-metro; extra == 'metro'
Provides-Extra: network
Requires-Dist: whereip-network; extra == 'network'
Description-Content-Type: text/markdown

<div align="center">
<a href="https://pypi.org/project/whereip">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tn3w/whereip/master/.github/title-dark.png">
<img src="https://raw.githubusercontent.com/tn3w/whereip/master/.github/title-light.png" width="346" alt="whereip">
</picture>
</a>

**The whole world's IP addresses, located offline in 3.7 MB.**<br>
The database ships inside the wheel, so a lookup is a memory read, not a request.

</div>

```bash
pip install whereip
```

```python
import whereip

place = whereip.lookup("1.1.1.1")  # str, int, packed bytes or ipaddress object
```

`None` where the database has nothing, `ValueError` where the input is not an
address; IPv4-mapped and 6to4 go through their IPv4.

```python
>>> place.city, place.region.name, place.country.name, place.country.code
('Brisbane', 'Queensland', 'Australia', 'AU')

>>> place.postal_code, place.country.currency.code, place.country.is_eu
('4000', 'AUD', False)
```

## Every field

```python
Place(
    city='Brisbane',
    city_ascii='Brisbane',
    geonames_id=2174003,
    population=2780063,
    place_type='PPLA',
    is_capital=False,
    is_regional_capital=True,
    postal_code='4000',
    postal_code_partial=False,
    timezone='Australia/Brisbane',
    hierarchy=('Australia', 'Queensland', 'Brisbane', 'Brisbane'),
    point=Point(
        latitude=-27.468,
        longitude=153.028,
        elevation_m=25,
        accuracy_km=32.1,
        granularity='city',
        confidence=0.49,
        geohash='r7hgdpxe6',
        plus_code='5R4MG2JH+R6',
        bounds=Bounds(west=152.703, south=-27.757, east=153.353, north=-27.179),
    ),
    region=Region(
        name='Queensland',
        code='04',
        iso='AU-QLD',
        type='State',
        geonames_id=2152274,
    ),
    district=District(
        name='Brisbane',
        code='31000',
        geonames_id=7839562,
    ),
    country=Country(
        code='AU',
        name='Australia',
        official_name='Commonwealth of Australia',
        native_name='Australia',
        translations={'ara': 'أستراليا', 'ces': 'Austrálie', 'deu': 'Australien',
                      'fra': 'Australie', 'jpn': 'オーストラリア', 'zho': '澳大利亚',
                      ...},
        continent='Oceania',
        continent_code='OC',
        is_eu=False,
        currency=Currency(code='AUD', name='Australian dollar', symbol='$'),
        currencies=('AUD',),
        calling_code='61',
        iso3='AUS',
        numeric_code='036',
        cioc='AUS',
        tld='.au',
        tlds=('.au',),
        languages=(Language(code='en', locale='en-AU', name='English'),),
        postal_regex='^(\\d{4})$',
        postal_format='####',
        capital='Canberra',
        capital_latitude=-35.28346,
        capital_longitude=149.12807,
        latitude=-27.0,
        longitude=133.0,
        bounds=Bounds(west=96.8215, south=-54.7504, east=159.1065, north=-9.2402),
        area_km2=7686850.0,
        population=27614411,
        borders=(),
        geonames_id=2077456,
        landlocked=False,
        independent=True,
        un_member=True,
        demonym='Australian',
        demonym_female='Australian',
        alt_spellings=('AU',),
        un_region='Oceania',
        un_subregion='Australia and New Zealand',
        intermediate_region='',
        m49_region='009',
        m49_subregion='053',
        start_of_week='monday',
        timezones=('Antarctica/Macquarie', 'Australia/Adelaide',
                   'Australia/Brisbane', 'Australia/Broken_Hill', ...),
        driving_side='left',
        flag='🇦🇺',
        flag_url='https://flagcdn.com/w320/au.png',
    ),
)
```

`place.time` is the zone's time, read on access:

```python
Clock(
    zone="Australia/Brisbane",
    name="Australian Eastern Standard Time",
    dst_name="Australian Eastern Daylight Time",
    abbreviation="AEST",
    local=datetime.datetime(
        2026, 8, 5, 19, 20, tzinfo=zoneinfo.ZoneInfo(key="Australia/Brisbane")
    ),
    utc_offset=datetime.timedelta(seconds=36000),
    standard_offset=datetime.timedelta(seconds=36000),
    daylight_offset=datetime.timedelta(seconds=36000),
    dst_active=False,
    uses_dst=False,
    dst_start=None,
    dst_end=None,
    next_transition=None,
)
```

`whereip.network` is keyed by the routing table, not by place: the announcement
covering the address, then the network behind it. A separate 5.2 MB wheel.

```bash
pip install "whereip[network]"
```

```python
>>> whereip.network("1.1.1.1")
Network(
    asn=13335,
    name='CLOUDFLARENET',
    prefix=IPv4Network('1.1.1.0/24'),
    rpki='valid',
    roas=1,
    operator=Operator(
        company='Cloudflare, Inc.',
        brand='Cloudflare',
        domain='cloudflare.com',
        category='content',
        tier=2,
        peering='heavy',
        scope='global',
        rir='arin',
        since=2010,
        website='https://www.cloudflare.com',
        street='101 Townsend St',
        city='San Francisco',
        state='CA',
        postal_code='94107-1934',
        abuse_email='abuse@cloudflare.com',
        country=Country(code='US', name='United States', ...),  # as above
    ),
)
```

`prefix` is the announcement the address falls in, so `1.1.1.1` and `1.0.0.1` reach one
`operator` through two prefixes. `rpki` is `valid`, `invalid` or `unknown`, `roas` how
many ROAs agree.

`brand` drops the legal form and the words every network carries from `company` and the
handle, so `GOOGLE` and `Google LLC` both read `Google`; `domain` is the host of
`website`, else of `abuse_email`.

`category` is `isp`, `mobile`, `hosting`, `content`, `enterprise`, `education`,
`government`, `non-profit`, `transit` or `exchange`; `tier` 1 transit-free, 2 has
customers, 3 edge; `peering` `heavy` or `light`; `scope` `global`, `continental`,
`regional`, `national` or `local`; `rir` the registry, `since` the delegation year.
Empty string or `0` where a source says nothing.

`operator.place` searches the postal address, read on access like `place.time`:

```python
>>> whereip.network("1.1.1.1").operator.place.hierarchy
('United States', 'California', 'City and County of San Francisco', 'San Francisco')
```

A real `Place`, so coordinates, zone and region codes follow; the operator's own
country wins ties, and `None` where PeeringDB names no city.

`whereip.metro` is keyed by network, markets split cities. United States only,
a separate 0.6 MB wheel.

```bash
pip install "whereip[metro]"
```

```python
>>> whereip.metro("23.20.0.1")
Metro(code=511, label='Washington, DC')

>>> whereip.metro("3.144.226.127").code, whereip.metro("4.16.214.196").code  # one city
(619, 637)
```

`code` is MaxMind's `metro_code`, frozen, so it drifts from Nielsen's boundaries;
`label` is the market's largest city, **not** Nielsen's name, absent for six.

`None`: `district` without a second level, `time` without the zone, `network` where
nobody announces the address, `metro` outside the United States. Every object is a
frozen dataclass, so `asdict()` gives a plain dict.

`region.iso` is the ISO 3166-2 code, `region.code` the GeoNames admin1 number

## Backwards

```python
whereip.nearest(-27.5, 153.0)  # Place | None, closest to a coordinate
whereip.search("Paris, France")  # list[Place], best match first
whereip.networks(-27.5, 153.0)  # iterator of that place's IPv4Network | IPv6Network
```

`nearest` measures by angle, holding at any distance, across the antimeridian and at
the poles.

```python
>>> [place.hierarchy for place in whereip.search("paris texas", 2)]
[('United States', 'Texas', 'Lamar County', 'Paris'),
 ('France', 'Île-de-France', 'Paris', 'Paris')]
```

`search` scores query words against the place, its region, district and country, the
place's own name counting double and population breaking ties; `limit` defaults to
five, and `Munchen`, `München` and `Munich` all reach the same city. It inverts every
name on the first call, 130 ms and 11 MB, then answers in 0.01 to 1.5 ms.

`networks` reads a whole index end to end, so it takes a moment; every other call is
immediate.

## Good for

- Local time, currency and language before the user types anything
- Country routing, pricing and compliance without a third-party call
- Bulk log enrichment, at ~590k lookups/s, ~280k with the announcing network
- Abuse handling and RPKI triage from the same process, no whois or RDAP
- Air-gapped deployments, where no address leaves the process

## Accuracy

```python
>>> place.point.granularity, place.point.accuracy_km, place.point.confidence
('city', 32.1, 0.49)

>>> whereip.lookup("8.8.8.8").city  # anycast, no city of its own
'Hutchinson'
```

`accuracy_km` is the network's spread, from 5 km up; `granularity` bands it
`city` → `region` → `country`. The coordinate is the middle of `bounds`, never a
street address.

## Data

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tn3w/whereip/master/.github/map-dark.png">
<img src="https://raw.githubusercontent.com/tn3w/whereip/master/.github/map-light.png" width="840" alt="Every place in the database, one dot each">
</picture>


19,599 places · 2,344 regions · 10,081 districts · 219 countries · 332 timezones,
rebuilt monthly from MaxMind, IP2Location, GeoNames and Natural Earth.
`Whereip().built` dates your copy. Python 3.10+, no dependencies but `tzdata` on
Windows. The network extra adds 86k ASNs over 959k boundaries, from a RIPE RIS RIB,
RPKI ROAs, the NRO delegations, CAIDA, PeeringDB and
[asn-abuse](https://github.com/tn3w/asn-abuse); the metro extra 210 markets.

## Your own file

```python
from whereip import Whereip
from whereip_network import Table

with Whereip("whereip.bin") as places, Table("network.bin") as networks:
    places.lookup("1.1.1.1")
    networks.lookup(0x01010101, False)  # (facts, prefix length, rpki, roas)
```

Memory-mapped and read-only, so processes and threads share one file. Blocks decode on
first use: lookups warm from ~230k to ~590k/s at up to 10 MB, `network.bin` to ~1.3M/s
at up to 16 MB, ~280k/s through `whereip.network` once the objects are built.

## Mini file

[`whereip_mini.py`](https://github.com/tn3w/whereip/blob/master/whereip_mini.py) is
the same lookup with no package around it: drop it beside a `whereip.bin` and it runs.

```python
>>> from whereip_mini import lookup
>>> lookup("1.1.1.1").country.currency.code
'AUD'
```

Results are dicts that read as attributes too, so a place is JSON-ready. 252 countries
come inlined; every other field fills in once the full `reference.tsv` turns up beside
it or in an installed `whereip`.

## Development

```bash
uv run pytest          # 587 tests, 100% branch coverage
uv run mypy
uv run basedpyright
uvx ruff check
uv build --all-packages  # whereip, whereip-network, whereip-metro, one uv workspace

cd builder && cargo fmt --check && cargo clippy
```

## LICENSE

MIT

<br>

*Assisted-by: Claude:claude-opus-5*
