Skip to content

Commit 6e47815

Browse files
committed
Add support for the new network attribute
1 parent ba112f1 commit 6e47815

File tree

9 files changed

+143
-23
lines changed

9 files changed

+143
-23
lines changed

HISTORY.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ History
66
2.10.0
77
++++++++++++++++++
88

9+
* The ``network`` attribute was added to ``geoip2.record.Traits``,
10+
``geoip2.model.AnonymousIP``, ``geoip2.model.ASN``,
11+
``geoip2.model.ConnectionType``, ``geoip2.model.Domain``,
12+
and ``geoip2.model.ISP``. This is an ``ipaddress.IPv4Network`` or an
13+
``ipaddress.IPv6Network``. This is the largest network where all of the
14+
fields besides ``ip_address`` have the same value. GitHub #79.
915
* Python 3.3 and 3.4 are no longer supported.
1016
* Updated documentation of anonymizer attributes - ``is_anonymous_vpn`` and
1117
``is_hosting_provider`` - to be more descriptive.

README.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ Web Service Example
9595
44.9733
9696
>>> response.location.longitude
9797
-93.2323
98+
>>>
99+
>>> response.traits.network
100+
IPv4Network('128.101.101.101/32')
98101
99102
Web Service Client Exceptions
100103
-----------------------------
@@ -156,6 +159,10 @@ City Database
156159
44.9733
157160
>>> response.location.longitude
158161
-93.2323
162+
>>>
163+
>>> response.traits.network
164+
IPv4Network('128.101.101.0/24')
165+
>>>
159166
>>> reader.close()
160167
161168
Anonymous IP Database
@@ -182,7 +189,9 @@ Anonymous IP Database
182189
>>> response.is_tor_exit_node
183190
True
184191
>>> response.ip_address
185-
'128.101.101.101'
192+
'85.25.43.84'
193+
>>> response.network
194+
IPv4Network('85.25.43.0/24')
186195
>>> reader.close()
187196
188197
ASN Database
@@ -218,6 +227,8 @@ Connection-Type Database
218227
'Corporate'
219228
>>> response.ip_address
220229
'128.101.101.101'
230+
>>> response.network
231+
IPv4Network('128.101.101.101/24')
221232
>>> reader.close()
222233
223234
@@ -284,6 +295,10 @@ Enterprise Database
284295
44.9733
285296
>>> response.location.longitude
286297
-93.2323
298+
>>>
299+
>>> response.traits.network
300+
IPv4Network('128.101.101.0/24')
301+
287302
288303
ISP Database
289304
^^^^^^^^^^^^
@@ -307,7 +322,9 @@ ISP Database
307322
>>> response.organization
308323
'Telstra Internet'
309324
>>> response.ip_address
310-
'128.101.101.101'
325+
'1.128.0.0'
326+
>>> response.network
327+
IPv4Network('1.128.0.0/16')
311328
>>> reader.close()
312329
313330
Database Reader Exceptions

geoip2/compat.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,18 @@ def compat_ip_address(address):
1212
if isinstance(address, bytes):
1313
address = address.decode()
1414
return ipaddress.ip_address(address)
15+
16+
def compat_ip_network(network, strict=True):
17+
"""Intended for internal use only."""
18+
if isinstance(network, bytes):
19+
network = network.decode()
20+
return ipaddress.ip_network(network, strict)
1521
else:
1622

1723
def compat_ip_address(address):
1824
"""Intended for internal use only."""
1925
return ipaddress.ip_address(address)
26+
27+
def compat_ip_network(network, strict=True):
28+
"""Intended for internal use only."""
29+
return ipaddress.ip_network(network, strict)

geoip2/database.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import geoip2
1515
import geoip2.models
1616
import geoip2.errors
17+
from geoip2.compat import compat_ip_network
1718

1819

1920
class Reader(object):
@@ -184,20 +185,26 @@ def _get(self, database_type, ip_address):
184185
raise TypeError("The %s method cannot be used with the "
185186
"%s database" %
186187
(caller, self.metadata().database_type))
187-
record = self._db_reader.get(ip_address)
188+
(record, prefix_len) = self._db_reader.get_with_prefix_len(ip_address)
188189
if record is None:
189190
raise geoip2.errors.AddressNotFoundError(
190191
"The address %s is not in the database." % ip_address)
191-
return record
192+
193+
network = compat_ip_network('{}/{}'.format(ip_address, prefix_len),
194+
False)
195+
return (record, network)
192196

193197
def _model_for(self, model_class, types, ip_address):
194-
record = self._get(types, ip_address)
195-
record.setdefault('traits', {})['ip_address'] = ip_address
198+
(record, network) = self._get(types, ip_address)
199+
traits = record.setdefault('traits', {})
200+
traits['ip_address'] = ip_address
201+
traits['network'] = network
196202
return model_class(record, locales=self._locales)
197203

198204
def _flat_model_for(self, model_class, types, ip_address):
199-
record = self._get(types, ip_address)
205+
(record, network) = self._get(types, ip_address)
200206
record['ip_address'] = ip_address
207+
record['network'] = network
201208
return model_class(record)
202209

203210
def metadata(self):

geoip2/models.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ class SimpleModel(SimpleEquality):
305305

306306
__metaclass__ = ABCMeta
307307

308+
def __init__(self, raw):
309+
self.ip_address = raw.get('ip_address')
310+
self.network = raw.get('network')
311+
self.raw = raw
312+
308313
def __repr__(self):
309314
# pylint: disable=no-member
310315
return '{module}.{class_name}({data})'.format(
@@ -359,17 +364,23 @@ class AnonymousIP(SimpleModel):
359364
The IP address used in the lookup.
360365
361366
:type: unicode
367+
368+
.. attribute:: network
369+
370+
The network associated with the record. In particular, this is the
371+
largest network where all of the fields besides ip_address have the same
372+
value.
373+
374+
:type: ipaddress.IPv4Network or ipaddress.IPv6Network
362375
"""
363376
def __init__(self, raw):
377+
super(AnonymousIP, self).__init__(raw)
364378
self.is_anonymous = raw.get('is_anonymous', False)
365379
self.is_anonymous_vpn = raw.get('is_anonymous_vpn', False)
366380
self.is_hosting_provider = raw.get('is_hosting_provider', False)
367381
self.is_public_proxy = raw.get('is_public_proxy', False)
368382
self.is_tor_exit_node = raw.get('is_tor_exit_node', False)
369383

370-
self.ip_address = raw.get('ip_address')
371-
self.raw = raw
372-
373384

374385
class ASN(SimpleModel):
375386
"""Model class for the GeoLite2 ASN.
@@ -394,15 +405,22 @@ class ASN(SimpleModel):
394405
The IP address used in the lookup.
395406
396407
:type: unicode
408+
409+
.. attribute:: network
410+
411+
The network associated with the record. In particular, this is the
412+
largest network where all of the fields besides ip_address have the same
413+
value.
414+
415+
:type: ipaddress.IPv4Network or ipaddress.IPv6Network
397416
"""
398417

399418
# pylint:disable=too-many-arguments
400419
def __init__(self, raw):
420+
super(ASN, self).__init__(raw)
401421
self.autonomous_system_number = raw.get('autonomous_system_number')
402422
self.autonomous_system_organization = raw.get(
403423
'autonomous_system_organization')
404-
self.ip_address = raw.get('ip_address')
405-
self.raw = raw
406424

407425

408426
class ConnectionType(SimpleModel):
@@ -428,11 +446,18 @@ class ConnectionType(SimpleModel):
428446
The IP address used in the lookup.
429447
430448
:type: unicode
449+
450+
.. attribute:: network
451+
452+
The network associated with the record. In particular, this is the
453+
largest network where all of the fields besides ip_address have the same
454+
value.
455+
456+
:type: ipaddress.IPv4Network or ipaddress.IPv6Network
431457
"""
432458
def __init__(self, raw):
459+
super(ConnectionType, self).__init__(raw)
433460
self.connection_type = raw.get('connection_type')
434-
self.ip_address = raw.get('ip_address')
435-
self.raw = raw
436461

437462

438463
class Domain(SimpleModel):
@@ -452,11 +477,18 @@ class Domain(SimpleModel):
452477
453478
:type: unicode
454479
480+
.. attribute:: network
481+
482+
The network associated with the record. In particular, this is the
483+
largest network where all of the fields besides ip_address have the same
484+
value.
485+
486+
:type: ipaddress.IPv4Network or ipaddress.IPv6Network
487+
455488
"""
456489
def __init__(self, raw):
490+
super(Domain, self).__init__(raw)
457491
self.domain = raw.get('domain')
458-
self.ip_address = raw.get('ip_address')
459-
self.raw = raw
460492

461493

462494
class ISP(ASN):
@@ -494,6 +526,14 @@ class ISP(ASN):
494526
The IP address used in the lookup.
495527
496528
:type: unicode
529+
530+
.. attribute:: network
531+
532+
The network associated with the record. In particular, this is the
533+
largest network where all of the fields besides ip_address have the same
534+
value.
535+
536+
:type: ipaddress.IPv4Network or ipaddress.IPv6Network
497537
"""
498538

499539
# pylint:disable=too-many-arguments

geoip2/records.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
66
"""
77

8+
import ipaddress
9+
810
# pylint:disable=R0903
911
from abc import ABCMeta
1012

13+
from geoip2.compat import compat_ip_network
1114
from geoip2.mixins import SimpleEquality
1215

1316

@@ -621,6 +624,14 @@ class Traits(Record):
621624
622625
:type: unicode
623626
627+
.. attribute:: network
628+
629+
The network associated with the record. In particular, this is the
630+
largest network where all of the fields besides ip_address have the same
631+
value.
632+
633+
:type: ipaddress.IPv4Network or ipaddress.IPv6Network
634+
624635
.. attribute:: organization
625636
626637
The name of the organization associated with the IP address. This
@@ -662,8 +673,8 @@ class Traits(Record):
662673
'connection_type', 'domain', 'is_anonymous', 'is_anonymous_proxy',
663674
'is_anonymous_vpn', 'is_hosting_provider', 'is_legitimate_proxy',
664675
'is_public_proxy', 'is_satellite_provider', 'is_tor_exit_node',
665-
'is_satellite_provider', 'isp', 'ip_address', 'organization',
666-
'user_type'
676+
'is_satellite_provider', 'isp', 'ip_address', 'network',
677+
'organization', 'user_type'
667678
])
668679

669680
def __init__(self, **kwargs):
@@ -678,4 +689,13 @@ def __init__(self, **kwargs):
678689
'is_tor_exit_node',
679690
]:
680691
kwargs[k] = bool(kwargs.get(k, False))
692+
693+
try:
694+
network = kwargs['network']
695+
if not isinstance(network,
696+
(ipaddress.IPv4Network, ipaddress.IPv6Network)):
697+
kwargs['network'] = compat_ip_network(network)
698+
except KeyError:
699+
pass
700+
681701
super(Traits, self).__init__(**kwargs)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
maxminddb>=1.4.0
1+
maxminddb>=1.5.0
22
requests>=2.22.0
33
urllib3>=1.25.2

tests/database_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
import geoip2.database
1010
import maxminddb
11+
import ipaddress
12+
13+
from ipaddress import IPv4Network, IPv6Network
1114

1215
try:
1316
import maxminddb.extension
@@ -74,6 +77,7 @@ def test_anonymous_ip(self):
7477
self.assertEqual(record.is_public_proxy, False)
7578
self.assertEqual(record.is_tor_exit_node, False)
7679
self.assertEqual(record.ip_address, ip_address)
80+
self.assertEqual(record.network, ipaddress.ip_network('1.2.0.0/16'))
7781
reader.close()
7882

7983
def test_asn(self):
@@ -86,6 +90,7 @@ def test_asn(self):
8690
self.assertEqual(record.autonomous_system_organization,
8791
'Telstra Pty Ltd')
8892
self.assertEqual(record.ip_address, ip_address)
93+
self.assertEqual(record.network, ipaddress.ip_network('1.128.0.0/11'))
8994

9095
self.assertRegex(str(record), r'geoip2.models.ASN\(.*1\.128\.0\.0.*\)',
9196
'str representation is correct')
@@ -116,6 +121,7 @@ def test_connection_type(self):
116121
record = reader.connection_type(ip_address)
117122
self.assertEqual(record.connection_type, 'Cable/DSL')
118123
self.assertEqual(record.ip_address, ip_address)
124+
self.assertEqual(record.network, ipaddress.ip_network('1.0.1.0/24'))
119125

120126
self.assertRegex(str(record), r'ConnectionType\(\{.*Cable/DSL.*\}\)',
121127
'ConnectionType str representation is reasonable')
@@ -131,6 +137,8 @@ def test_country(self):
131137
record = reader.country('81.2.69.160')
132138
self.assertEqual(record.traits.ip_address, '81.2.69.160',
133139
'IP address is added to model')
140+
self.assertEqual(record.traits.network,
141+
ipaddress.ip_network('81.2.69.160/27'))
134142
self.assertEqual(record.country.is_in_european_union, True)
135143
self.assertEqual(record.registered_country.is_in_european_union, False)
136144
reader.close()
@@ -143,6 +151,7 @@ def test_domain(self):
143151
record = reader.domain(ip_address)
144152
self.assertEqual(record.domain, 'maxmind.com')
145153
self.assertEqual(record.ip_address, ip_address)
154+
self.assertEqual(record.network, ipaddress.ip_network('1.2.0.0/16'))
146155

147156
self.assertRegex(str(record), r'Domain\(\{.*maxmind.com.*\}\)',
148157
'Domain str representation is reasonable')
@@ -167,6 +176,8 @@ def test_enterprise(self):
167176
self.assertEqual(record.traits.connection_type, 'Cable/DSL')
168177
self.assertTrue(record.traits.is_legitimate_proxy)
169178
self.assertEqual(record.traits.ip_address, ip_address)
179+
self.assertEqual(record.traits.network,
180+
ipaddress.ip_network('74.209.16.0/20'))
170181

171182
def test_isp(self):
172183
reader = geoip2.database.Reader(
@@ -180,6 +191,7 @@ def test_isp(self):
180191
self.assertEqual(record.isp, 'Telstra Internet')
181192
self.assertEqual(record.organization, 'Telstra Internet')
182193
self.assertEqual(record.ip_address, ip_address)
194+
self.assertEqual(record.network, ipaddress.ip_network('1.128.0.0/11'))
183195

184196
self.assertRegex(str(record), r'ISP\(\{.*Telstra.*\}\)',
185197
'ISP str representation is reasonable')

0 commit comments

Comments
 (0)