Skip to content

Commit 22149c7

Browse files
oschwaldclaude
andcommitted
Add IP risk and anonymizer fields to Insights
This adds support for new web service fields providing enhanced VPN/proxy detection and IP risk scoring. A new Anonymizer record consolidates anonymous IP flags with additional fields (anonymizer_confidence, network_last_seen, provider_name). The ip_risk_snapshot field enables risk-based decision making. Anonymous IP flags in Traits are deprecated in favor of the new anonymizer object location. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f033ca4 commit 22149c7

File tree

7 files changed

+243
-16
lines changed

7 files changed

+243
-16
lines changed

HISTORY.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ History
1313
support.
1414
* Setuptools has been replaced with the uv build backend for building the
1515
package.
16+
* A new ``anonymizer`` object has been added to ``geoip2.models.Insights``.
17+
This object is a ``geoip2.records.Anonymizer`` and contains the following
18+
fields: ``anonymizer_confidence``, ``network_last_seen``, ``provider_name``,
19+
``is_anonymous``, ``is_anonymous_vpn``, ``is_hosting_provider``,
20+
``is_public_proxy``, ``is_residential_proxy``, and ``is_tor_exit_node``.
21+
These provide information about VPN and proxy usage.
22+
* A new ``ip_risk_snapshot`` property has been added to
23+
``geoip2.records.Traits``. This is a float ranging from 0.01 to 99 that
24+
represents the risk associated with the IP address. A higher score indicates
25+
a higher risk. This field is only available from the Insights end point.
26+
* The following properties on ``geoip2.records.Traits`` have been deprecated:
27+
``is_anonymous``, ``is_anonymous_vpn``, ``is_hosting_provider``,
28+
``is_public_proxy``, ``is_residential_proxy``, and ``is_tor_exit_node``.
29+
Please use the ``anonymizer`` object in the ``Insights`` model instead.
1630

1731
5.1.0 (2025-05-05)
1832
++++++++++++++++++

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ ignore = [
8888
[tool.ruff.lint.per-file-ignores]
8989
"docs/*" = ["ALL"]
9090
"src/geoip2/{models,records}.py" = [ "ANN401", "D107", "PLR0913" ]
91-
"tests/*" = ["ANN201", "D"]
91+
# FBT003: We use assertIs with boolean literals to verify values are actual
92+
# booleans (True/False), not just truthy/falsy values
93+
"tests/*" = ["ANN201", "D", "FBT003"]
9294

9395
[tool.tox]
9496
env_list = [

src/geoip2/_internal.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Internal utilities."""
22

3+
import datetime
34
import json
45
from abc import ABCMeta
56
from typing import Any
@@ -42,6 +43,8 @@ def to_dict(self) -> dict[str, Any]: # noqa: C901, PLR0912
4243
elif isinstance(value, dict):
4344
if value:
4445
result[key] = value
46+
elif isinstance(value, datetime.date):
47+
result[key] = value.isoformat()
4548
elif value is not None and value is not False:
4649
result[key] = value
4750

src/geoip2/models.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,47 @@ def __init__(
149149
class Insights(City):
150150
"""Model for the GeoIP2 Insights web service."""
151151

152+
anonymizer: geoip2.records.Anonymizer
153+
"""Anonymizer object for the requested IP address. This object contains
154+
information about VPN and proxy usage.
155+
"""
156+
157+
def __init__(
158+
self,
159+
locales: Sequence[str] | None,
160+
*,
161+
anonymizer: dict[str, Any] | None = None,
162+
city: dict[str, Any] | None = None,
163+
continent: dict[str, Any] | None = None,
164+
country: dict[str, Any] | None = None,
165+
location: dict[str, Any] | None = None,
166+
ip_address: IPAddress | None = None,
167+
maxmind: dict[str, Any] | None = None,
168+
postal: dict[str, Any] | None = None,
169+
prefix_len: int | None = None,
170+
registered_country: dict[str, Any] | None = None,
171+
represented_country: dict[str, Any] | None = None,
172+
subdivisions: list[dict[str, Any]] | None = None,
173+
traits: dict[str, Any] | None = None,
174+
**_: Any,
175+
) -> None:
176+
super().__init__(
177+
locales,
178+
city=city,
179+
continent=continent,
180+
country=country,
181+
location=location,
182+
ip_address=ip_address,
183+
maxmind=maxmind,
184+
postal=postal,
185+
prefix_len=prefix_len,
186+
registered_country=registered_country,
187+
represented_country=represented_country,
188+
subdivisions=subdivisions,
189+
traits=traits,
190+
)
191+
self.anonymizer = geoip2.records.Anonymizer(**(anonymizer or {}))
192+
152193

153194
class Enterprise(City):
154195
"""Model for the GeoIP2 Enterprise database."""

src/geoip2/records.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import datetime
56
import ipaddress
67
from abc import ABCMeta
78
from ipaddress import IPv4Address, IPv6Address
@@ -261,6 +262,98 @@ def __init__(self, *, queries_remaining: int | None = None, **_: Any) -> None:
261262
self.queries_remaining = queries_remaining
262263

263264

265+
class Anonymizer(Record):
266+
"""Contains data for the anonymizer record associated with an IP address.
267+
268+
This class contains the anonymizer data associated with an IP address.
269+
270+
This record is returned by ``insights``.
271+
"""
272+
273+
anonymizer_confidence: int | None
274+
"""A score ranging from 1 to 99 that represents our percent confidence that
275+
the network is currently part of an actively used VPN service. Currently
276+
only values 30 and 99 are provided. This attribute is only available from
277+
the Insights end point.
278+
"""
279+
280+
network_last_seen: datetime.date | None
281+
"""The last day that the network was sighted in our analysis of anonymized
282+
networks. This attribute is only available from the Insights end point.
283+
"""
284+
285+
provider_name: str | None
286+
"""The name of the VPN provider (e.g., NordVPN, SurfShark, etc.) associated
287+
with the network. This attribute is only available from the Insights end
288+
point.
289+
"""
290+
291+
is_anonymous: bool
292+
"""This is true if the IP address belongs to any sort of anonymous network.
293+
This attribute is only available from the Insights end point.
294+
"""
295+
296+
is_anonymous_vpn: bool
297+
"""This is true if the IP address is registered to an anonymous VPN provider.
298+
299+
If a VPN provider does not register subnets under names associated with
300+
them, we will likely only flag their IP ranges using the
301+
``is_hosting_provider`` attribute.
302+
303+
This attribute is only available from the Insights end point.
304+
"""
305+
306+
is_hosting_provider: bool
307+
"""This is true if the IP address belongs to a hosting or VPN provider
308+
(see description of ``is_anonymous_vpn`` attribute). This attribute is only
309+
available from the Insights end point.
310+
"""
311+
312+
is_public_proxy: bool
313+
"""This is true if the IP address belongs to a public proxy. This attribute
314+
is only available from the Insights end point.
315+
"""
316+
317+
is_residential_proxy: bool
318+
"""This is true if the IP address is on a suspected anonymizing network
319+
and belongs to a residential ISP. This attribute is only available from the
320+
Insights end point.
321+
"""
322+
323+
is_tor_exit_node: bool
324+
"""This is true if the IP address is a Tor exit node. This attribute is only
325+
available from the Insights end point.
326+
"""
327+
328+
def __init__(
329+
self,
330+
*,
331+
anonymizer_confidence: int | None = None,
332+
is_anonymous: bool = False,
333+
is_anonymous_vpn: bool = False,
334+
is_hosting_provider: bool = False,
335+
is_public_proxy: bool = False,
336+
is_residential_proxy: bool = False,
337+
is_tor_exit_node: bool = False,
338+
network_last_seen: str | None = None,
339+
provider_name: str | None = None,
340+
**_: Any,
341+
) -> None:
342+
self.anonymizer_confidence = anonymizer_confidence
343+
self.is_anonymous = is_anonymous
344+
self.is_anonymous_vpn = is_anonymous_vpn
345+
self.is_hosting_provider = is_hosting_provider
346+
self.is_public_proxy = is_public_proxy
347+
self.is_residential_proxy = is_residential_proxy
348+
self.is_tor_exit_node = is_tor_exit_node
349+
self.network_last_seen = (
350+
datetime.date.fromisoformat(network_last_seen)
351+
if network_last_seen
352+
else None
353+
)
354+
self.provider_name = provider_name
355+
356+
264357
class Postal(Record):
265358
"""Contains data for the postal record associated with an IP address.
266359
@@ -425,10 +518,24 @@ class Traits(Record):
425518
from the City Plus and Insights web service end points and the
426519
Enterprise database.
427520
"""
521+
ip_risk_snapshot: float | None
522+
"""The risk associated with the IP address. The value ranges from 0.01 to
523+
99. A higher score indicates a higher risk.
524+
525+
Please note that the IP risk score provided in GeoIP products and services
526+
is more static than the IP risk score provided in minFraud and is not
527+
responsive to traffic on your network. If you need realtime IP risk scoring
528+
based on behavioral signals on your own network, please use minFraud.
529+
530+
This attribute is only available from the Insights end point.
531+
"""
428532
_ip_address: IPAddress | None
429533
is_anonymous: bool
430534
"""This is true if the IP address belongs to any sort of anonymous network.
431535
This attribute is only available from Insights.
536+
537+
.. deprecated:: 5.2.0
538+
Use the ``anonymizer`` object in the ``Insights`` model instead.
432539
"""
433540
is_anonymous_proxy: bool
434541
"""This is true if the IP is an anonymous proxy.
@@ -447,6 +554,9 @@ class Traits(Record):
447554
``is_hosting_provider`` attribute.
448555
449556
This attribute is only available from Insights.
557+
558+
.. deprecated:: 5.2.0
559+
Use the ``anonymizer`` object in the ``Insights`` model instead.
450560
"""
451561
is_anycast: bool
452562
"""This returns true if the IP address belongs to an
@@ -458,6 +568,9 @@ class Traits(Record):
458568
"""This is true if the IP address belongs to a hosting or VPN provider
459569
(see description of ``is_anonymous_vpn`` attribute).
460570
This attribute is only available from Insights.
571+
572+
.. deprecated:: 5.2.0
573+
Use the ``anonymizer`` object in the ``Insights`` model instead.
461574
"""
462575
is_legitimate_proxy: bool
463576
"""This attribute is true if MaxMind believes this IP address to be a
@@ -467,11 +580,17 @@ class Traits(Record):
467580
is_public_proxy: bool
468581
"""This is true if the IP address belongs to a public proxy. This attribute
469582
is only available from Insights.
583+
584+
.. deprecated:: 5.2.0
585+
Use the ``anonymizer`` object in the ``Insights`` model instead.
470586
"""
471587
is_residential_proxy: bool
472588
"""This is true if the IP address is on a suspected anonymizing network
473589
and belongs to a residential ISP. This attribute is only available from
474590
Insights.
591+
592+
.. deprecated:: 5.2.0
593+
Use the ``anonymizer`` object in the ``Insights`` model instead.
475594
"""
476595
is_satellite_provider: bool
477596
"""This is true if the IP address is from a satellite provider that
@@ -486,6 +605,9 @@ class Traits(Record):
486605
is_tor_exit_node: bool
487606
"""This is true if the IP address is a Tor exit node. This attribute is
488607
only available from Insights.
608+
609+
.. deprecated:: 5.2.0
610+
Use the ``anonymizer`` object in the ``Insights`` model instead.
489611
"""
490612
isp: str | None
491613
"""The name of the ISP associated with the IP address. This attribute is
@@ -560,6 +682,7 @@ def __init__(
560682
autonomous_system_organization: str | None = None,
561683
connection_type: str | None = None,
562684
domain: str | None = None,
685+
ip_risk_snapshot: float | None = None,
563686
is_anonymous: bool = False,
564687
is_anonymous_proxy: bool = False,
565688
is_anonymous_vpn: bool = False,
@@ -586,6 +709,7 @@ def __init__(
586709
self.autonomous_system_organization = autonomous_system_organization
587710
self.connection_type = connection_type
588711
self.domain = domain
712+
self.ip_risk_snapshot = ip_risk_snapshot
589713
self.is_anonymous = is_anonymous
590714
self.is_anonymous_proxy = is_anonymous_proxy
591715
self.is_anonymous_vpn = is_anonymous_vpn

0 commit comments

Comments
 (0)