Skip to content

Commit d102829

Browse files
committed
Add support for the Anonymous Plus database
1 parent dc243af commit d102829

File tree

6 files changed

+208
-2
lines changed

6 files changed

+208
-2
lines changed

HISTORY.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
History
55
-------
66

7+
5.1.0
8+
++++++++++++++++++
9+
10+
* Support for the GeoIP Anonymous Plus database has been added. To do a lookup
11+
in this database, use the ``anonymous_plus`` method on ``Reader``.
12+
13+
714
5.0.1 (2025-01-28)
815
++++++++++++++++++
916

README.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,42 @@ Anonymous IP Database
268268
>>> response.network
269269
IPv4Network('203.0.113.0/24')
270270
271+
Anonymous Plus Database
272+
^^^^^^^^^^^^^^^^^^^^^^^
273+
274+
.. code-block:: pycon
275+
276+
>>> import geoip2.database
277+
>>>
278+
>>> # This creates a Reader object. You should use the same object
279+
>>> # across multiple requests as creation of it is expensive.
280+
>>> with geoip2.database.Reader('/path/to/GeoIP-Anonymous-Plus.mmdb') as reader:
281+
>>>
282+
>>> response = reader.anonymous_plus('203.0.113.0')
283+
>>>
284+
>>> response.anonymizer_confidence
285+
30
286+
>>> response.is_anonymous
287+
True
288+
>>> response.is_anonymous_vpn
289+
True
290+
>>> response.is_hosting_provider
291+
False
292+
>>> response.is_public_proxy
293+
False
294+
>>> response.is_residential_proxy
295+
False
296+
>>> response.is_tor_exit_node
297+
False
298+
>>> response.ip_address
299+
'203.0.113.0'
300+
>>> response.network
301+
IPv4Network('203.0.113.0/24')
302+
>>> response.network_last_seen
303+
datetime.date(2025, 4, 18)
304+
>>> response.provider_name
305+
FooBar VPNs
306+
271307
ASN Database
272308
^^^^^^^^^^^^
273309

geoip2/database.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
ASN,
2828
ISP,
2929
AnonymousIP,
30+
AnonymousPlus,
3031
City,
3132
ConnectionType,
3233
Country,
@@ -167,6 +168,23 @@ def anonymous_ip(self, ip_address: IPAddress) -> AnonymousIP:
167168
),
168169
)
169170

171+
def anonymous_plus(self, ip_address: IPAddress) -> AnonymousPlus:
172+
"""Get the AnonymousPlus object for the IP address.
173+
174+
:param ip_address: IPv4 or IPv6 address as a string.
175+
176+
:returns: :py:class:`geoip2.models.AnonymousPlus` object
177+
178+
"""
179+
return cast(
180+
AnonymousPlus,
181+
self._flat_model_for(
182+
geoip2.models.AnonymousPlus,
183+
"GeoIP-Anonymous-Plus",
184+
ip_address,
185+
),
186+
)
187+
170188
def asn(self, ip_address: IPAddress) -> ASN:
171189
"""Get the ASN object for the IP address.
172190

geoip2/models.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
"""
1313

1414
# pylint: disable=too-many-instance-attributes,too-few-public-methods,too-many-arguments
15+
import datetime
1516
import ipaddress
1617
from abc import ABCMeta
1718
from collections.abc import Sequence
18-
from typing import Optional, Union
1919
from ipaddress import IPv4Address, IPv6Address
20+
from typing import Optional, Union
2021

2122
import geoip2.records
2223
from geoip2._internal import Model
@@ -512,6 +513,128 @@ def __init__(
512513
self.is_tor_exit_node = is_tor_exit_node
513514

514515

516+
class AnonymousPlus(AnonymousIP):
517+
"""Model class for the GeoIP Anonymous Plus.
518+
519+
This class provides the following attribute:
520+
521+
.. attribute: anonymizer_confidence
522+
523+
A score ranging from 1 to 99 that is our percent confidence that the
524+
network is currently part of an actively used VPN service.
525+
526+
:type: str
527+
528+
.. attribute:: is_anonymous
529+
530+
This is true if the IP address belongs to any sort of anonymous network.
531+
532+
:type: bool
533+
534+
.. attribute:: is_anonymous_vpn
535+
536+
This is true if the IP address is registered to an anonymous VPN
537+
provider.
538+
539+
If a VPN provider does not register subnets under names associated with
540+
them, we will likely only flag their IP ranges using the
541+
``is_hosting_provider`` attribute.
542+
543+
:type: bool
544+
545+
.. attribute:: is_hosting_provider
546+
547+
This is true if the IP address belongs to a hosting or VPN provider
548+
(see description of ``is_anonymous_vpn`` attribute).
549+
550+
:type: bool
551+
552+
.. attribute:: is_public_proxy
553+
554+
This is true if the IP address belongs to a public proxy.
555+
556+
:type: bool
557+
558+
.. attribute:: is_residential_proxy
559+
560+
This is true if the IP address is on a suspected anonymizing network
561+
and belongs to a residential ISP.
562+
563+
:type: bool
564+
565+
.. attribute:: is_tor_exit_node
566+
567+
This is true if the IP address is a Tor exit node.
568+
569+
:type: bool
570+
571+
.. attribute:: ip_address
572+
573+
The IP address used in the lookup.
574+
575+
:type: ipaddress.IPv4Address or ipaddress.IPv6Address
576+
577+
.. attribute:: network
578+
579+
The network associated with the record. In particular, this is the
580+
largest network where all of the fields besides ip_address have the same
581+
value.
582+
583+
:type: ipaddress.IPv4Network or ipaddress.IPv6Network
584+
585+
.. attribute:: network_last_seen
586+
587+
The last day that the network was sighted in our analysis of anonymized
588+
networks.
589+
590+
:type: str
591+
592+
.. attribute:: provider_name
593+
594+
The name of the VPN provider (e.g., NordVPN, SurfShark, etc.) associated
595+
with the network.
596+
597+
:type: str
598+
"""
599+
600+
anonymizer_confidence: Optional[int]
601+
network_last_seen: Optional[datetime.date]
602+
provider_name: Optional[str]
603+
604+
def __init__(
605+
self,
606+
ip_address: IPAddress,
607+
*,
608+
anonymizer_confidence: Optional[int] = None,
609+
is_anonymous: bool = False,
610+
is_anonymous_vpn: bool = False,
611+
is_hosting_provider: bool = False,
612+
is_public_proxy: bool = False,
613+
is_residential_proxy: bool = False,
614+
is_tor_exit_node: bool = False,
615+
network: Optional[str] = None,
616+
network_last_seen: Optional[str] = None,
617+
prefix_len: Optional[int] = None,
618+
provider_name: Optional[str] = None,
619+
**_,
620+
) -> None:
621+
super().__init__(
622+
is_anonymous=is_anonymous,
623+
is_anonymous_vpn=is_anonymous_vpn,
624+
is_hosting_provider=is_hosting_provider,
625+
is_public_proxy=is_public_proxy,
626+
is_residential_proxy=is_residential_proxy,
627+
is_tor_exit_node=is_tor_exit_node,
628+
ip_address=ip_address,
629+
network=network,
630+
prefix_len=prefix_len,
631+
)
632+
self.anonymizer_confidence = anonymizer_confidence
633+
if network_last_seen is not None:
634+
self.network_last_seen = datetime.date.fromisoformat(network_last_seen)
635+
self.provider_name = provider_name
636+
637+
515638
class ASN(SimpleModel):
516639
"""Model class for the GeoLite2 ASN.
517640

tests/data

Submodule data updated 57 files

tests/database_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22

33

4+
import datetime
45
import ipaddress
56
import sys
67
import unittest
@@ -86,6 +87,27 @@ def test_anonymous_ip(self) -> None:
8687
self.assertEqual(record.network, ipaddress.ip_network("1.2.0.0/16"))
8788
reader.close()
8889

90+
def test_anonymous_plus(self) -> None:
91+
with geoip2.database.Reader(
92+
"tests/data/test-data/GeoIP-Anonymous-Plus-Test.mmdb",
93+
) as reader:
94+
95+
ip_address = "1.2.0.1"
96+
97+
record = reader.anonymous_plus(ip_address)
98+
99+
self.assertEqual(record.anonymizer_confidence, 30)
100+
self.assertEqual(record.is_anonymous, True)
101+
self.assertEqual(record.is_anonymous_vpn, True)
102+
self.assertEqual(record.is_hosting_provider, False)
103+
self.assertEqual(record.is_public_proxy, False)
104+
self.assertEqual(record.is_residential_proxy, False)
105+
self.assertEqual(record.is_tor_exit_node, False)
106+
self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address))
107+
self.assertEqual(record.network, ipaddress.ip_network("1.2.0.1/32"))
108+
self.assertEqual(record.network_last_seen, datetime.date(2025, 4, 14))
109+
self.assertEqual(record.provider_name, "foo")
110+
89111
def test_anonymous_ip_all_set(self) -> None:
90112
reader = geoip2.database.Reader(
91113
"tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb",

0 commit comments

Comments
 (0)