diff --git a/HISTORY.rst b/HISTORY.rst index 9a41ae6..78f12b2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,11 @@ History * IMPORTANT: Python 3.8 or greater is required. If you are using an older version, please use an earlier release. +* The ``is_anycast`` attribute was added to ``geoip2.record.Traits``. + This returns ``True`` if the IP address belongs to an + `anycast network `_. + This is available for the GeoIP2 Country, City Plus, and Insights web services + and the GeoIP2 Country, City, and Enterprise databases. 4.7.0 (2023-05-09) ++++++++++++++++++ diff --git a/geoip2/records.py b/geoip2/records.py index 000dade..cd36f3c 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -665,6 +665,15 @@ class Traits(Record): :type: bool + .. attribute:: is_anycast + + This returns true if the IP address belongs to an + `anycast network `_. + This is available for the GeoIP2 Country, City Plus, and Insights + web services and the GeoIP2 Country, City, and Enterprise databases. + + :type: bool + .. attribute:: is_hosting_provider This is true if the IP address belongs to a hosting or VPN provider @@ -815,9 +824,11 @@ class Traits(Record): autonomous_system_organization: Optional[str] connection_type: Optional[str] domain: Optional[str] + ip_address: Optional[str] is_anonymous: bool is_anonymous_proxy: bool is_anonymous_vpn: bool + is_anycast: bool is_hosting_provider: bool is_legitimate_proxy: bool is_public_proxy: bool @@ -825,7 +836,6 @@ class Traits(Record): is_satellite_provider: bool is_tor_exit_node: bool isp: Optional[str] - ip_address: Optional[str] mobile_country_code: Optional[str] mobile_network_code: Optional[str] organization: Optional[str] @@ -860,6 +870,7 @@ def __init__( user_type: Optional[str] = None, mobile_country_code: Optional[str] = None, mobile_network_code: Optional[str] = None, + is_anycast: bool = False, **_, ) -> None: self.autonomous_system_number = autonomous_system_number @@ -869,6 +880,7 @@ def __init__( self.is_anonymous = is_anonymous self.is_anonymous_proxy = is_anonymous_proxy self.is_anonymous_vpn = is_anonymous_vpn + self.is_anycast = is_anycast self.is_hosting_provider = is_hosting_provider self.is_legitimate_proxy = is_legitimate_proxy self.is_public_proxy = is_public_proxy diff --git a/tests/data b/tests/data index 2b37923..1271107 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit 2b37923df61aa3b5fb6c7edfbf4dc5fafa10258a +Subproject commit 1271107ccad72c320bc7dc8aefd767cba550101a diff --git a/tests/database_test.py b/tests/database_test.py index 037f141..0974b74 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -135,6 +135,10 @@ def test_city(self) -> None: record.location.accuracy_radius, 100, "The accuracy_radius is populated" ) self.assertEqual(record.registered_country.is_in_european_union, False) + self.assertFalse(record.traits.is_anycast) + + record = reader.city("214.1.1.0") + self.assertTrue(record.traits.is_anycast) reader.close() @@ -150,13 +154,13 @@ def test_connection_type(self) -> None: record, eval(repr(record)), "ConnectionType repr can be eval'd" ) - self.assertEqual(record.connection_type, "Cable/DSL") + self.assertEqual(record.connection_type, "Cellular") self.assertEqual(record.ip_address, ip_address) self.assertEqual(record.network, ipaddress.ip_network("1.0.1.0/24")) self.assertRegex( str(record), - r"ConnectionType\(\{.*Cable/DSL.*\}\)", + r"ConnectionType\(\{.*Cellular.*\}\)", "ConnectionType str representation is reasonable", ) @@ -171,6 +175,11 @@ def test_country(self) -> None: self.assertEqual(record.traits.network, ipaddress.ip_network("81.2.69.160/27")) self.assertEqual(record.country.is_in_european_union, False) self.assertEqual(record.registered_country.is_in_european_union, False) + self.assertFalse(record.traits.is_anycast) + + record = reader.country("214.1.1.0") + self.assertTrue(record.traits.is_anycast) + reader.close() def test_domain(self) -> None: @@ -211,12 +220,15 @@ def test_enterprise(self) -> None: self.assertEqual( record.traits.network, ipaddress.ip_network("74.209.16.0/20") ) + self.assertFalse(record.traits.is_anycast) record = reader.enterprise("149.101.100.0") - self.assertEqual(record.traits.mobile_country_code, "310") self.assertEqual(record.traits.mobile_network_code, "004") + record = reader.enterprise("214.1.1.0") + self.assertTrue(record.traits.is_anycast) + def test_isp(self) -> None: with geoip2.database.Reader( "tests/data/test-data/GeoIP2-ISP-Test.mmdb" diff --git a/tests/models_test.py b/tests/models_test.py index ecae500..9f4fa72 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -78,6 +78,7 @@ def test_insights_full(self) -> None: "is_anonymous": True, "is_anonymous_proxy": True, "is_anonymous_vpn": True, + "is_anycast": True, "is_hosting_provider": True, "is_public_proxy": True, "is_residential_proxy": True, @@ -194,6 +195,7 @@ def test_insights_full(self) -> None: self.assertIs(model.traits.is_anonymous, True) self.assertIs(model.traits.is_anonymous_proxy, True) self.assertIs(model.traits.is_anonymous_vpn, True) + self.assertIs(model.traits.is_anycast, True) self.assertIs(model.traits.is_hosting_provider, True) self.assertIs(model.traits.is_public_proxy, True) self.assertIs(model.traits.is_residential_proxy, True) @@ -327,6 +329,11 @@ def test_city_full(self) -> None: False, "traits is_anonymous_proxy returns False by default", ) + self.assertEqual( + model.traits.is_anycast, + False, + "traits is_anycast returns False by default", + ) self.assertEqual( model.traits.is_satellite_provider, True, diff --git a/tests/webservice_test.py b/tests/webservice_test.py index f2825a2..4e8a7a7 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -44,7 +44,11 @@ class TestBaseClient(unittest.TestCase): "iso_code": "DE", "names": {"en": "Germany"}, }, - "traits": {"ip_address": "1.2.3.4", "network": "1.2.3.0/24"}, + "traits": { + "ip_address": "1.2.3.4", + "is_anycast": True, + "network": "1.2.3.0/24", + }, } # this is not a comprehensive representation of the @@ -104,6 +108,7 @@ def test_country_ok(self): self.assertEqual( country.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" ) + self.assertTrue(country.traits.is_anycast) self.assertEqual(country.raw, self.country, "raw response is correct") @httprettified @@ -328,6 +333,7 @@ def test_city_ok(self): self.assertEqual( city.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" ) + self.assertTrue(city.traits.is_anycast) @httprettified def test_insights_ok(self): @@ -345,6 +351,7 @@ def test_insights_ok(self): self.assertEqual( insights.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" ) + self.assertTrue(insights.traits.is_anycast) self.assertEqual(insights.traits.static_ip_score, 1.3, "static_ip_score is 1.3") self.assertEqual(insights.traits.user_count, 2, "user_count is 2")