Skip to content

Commit 069704a

Browse files
committed
Replace raw attribute with to_dict method
1 parent 8d366fd commit 069704a

File tree

6 files changed

+70
-24
lines changed

6 files changed

+70
-24
lines changed

HISTORY.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
History
55
-------
66

7-
4.9.0
7+
5.0.0
88
++++++++++++++++++
99

10+
* BREAKING: The ``raw`` attribute on the model classes has been replaced
11+
with a ``to_dict()`` method. This can be used to get a representation of
12+
the object that is suitable for serialization.
1013
* IMPORTANT: Python 3.9 or greater is required. If you are using an older
1114
version, please use an earlier release.
1215
* ``metro_code`` on ``geoip2.record.Location`` has been deprecated. The

geoip2/mixins.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,50 @@
1-
"""This package contains utility mixins"""
1+
"""This package contains internal utilities"""
22

33
# pylint: disable=too-few-public-methods
44
from abc import ABCMeta
55
from typing import Any
66

77

8-
class SimpleEquality(metaclass=ABCMeta):
9-
"""Naive __dict__ equality mixin"""
8+
class Model(metaclass=ABCMeta):
9+
"""Shared methods for MaxMind model classes"""
1010

1111
def __eq__(self, other: Any) -> bool:
1212
return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
1313

1414
def __ne__(self, other):
1515
return not self.__eq__(other)
16+
17+
# pylint: disable=too-many-branches
18+
def to_dict(self):
19+
"""Returns a dict of the object suitable for serialization"""
20+
result = {}
21+
for key, value in self.__dict__.items():
22+
if key.startswith("_"):
23+
continue
24+
if hasattr(value, "to_dict") and callable(value.to_dict):
25+
if d := value.to_dict():
26+
result[key] = d
27+
elif isinstance(value, (list, tuple)):
28+
ls = []
29+
for e in value:
30+
if hasattr(e, "to_dict") and callable(e.to_dict):
31+
if e := e.to_dict():
32+
ls.append(e)
33+
elif e is not None:
34+
ls.append(e)
35+
if ls:
36+
result[key] = ls
37+
# We only have dicts of strings currently. Do not bother with
38+
# the general case.
39+
elif isinstance(value, dict):
40+
if value:
41+
result[key] = value
42+
elif value is not None and value is not False:
43+
result[key] = value
44+
45+
# network is a property for performance reasons
46+
# pylint: disable=no-member
47+
if hasattr(self, "network") and self.network is not None:
48+
result["network"] = str(self.network)
49+
50+
return result

geoip2/models.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
from typing import Any, cast, Dict, Optional, Sequence, Union
1818

1919
import geoip2.records
20-
from geoip2.mixins import SimpleEquality
20+
from geoip2.mixins import Model
2121

2222

23-
class Country(SimpleEquality):
23+
class Country(Model):
2424
"""Model for the Country web service and Country database.
2525
2626
This class provides the following attributes:
@@ -97,12 +97,9 @@ def __init__(
9797
self.maxmind = geoip2.records.MaxMind(**raw_response.get("maxmind", {}))
9898

9999
self.traits = geoip2.records.Traits(**raw_response.get("traits", {}))
100-
self.raw = raw_response
101100

102101
def __repr__(self) -> str:
103-
return (
104-
f"{self.__module__}.{self.__class__.__name__}({self.raw}, {self._locales})"
105-
)
102+
return f"{self.__module__}.{self.__class__.__name__}({self.to_dict()}, {self._locales})"
106103

107104

108105
class City(Country):
@@ -321,22 +318,27 @@ class Enterprise(City):
321318
"""
322319

323320

324-
class SimpleModel(SimpleEquality, metaclass=ABCMeta):
321+
class SimpleModel(Model, metaclass=ABCMeta):
325322
"""Provides basic methods for non-location models"""
326323

327-
raw: Dict[str, Union[bool, str, int]]
328324
ip_address: str
329325
_network: Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]
330326
_prefix_len: int
331327

332328
def __init__(self, raw: Dict[str, Union[bool, str, int]]) -> None:
333-
self.raw = raw
334-
self._network = None
335-
self._prefix_len = cast(int, raw.get("prefix_len"))
329+
if network := raw.get("network"):
330+
self._network = ipaddress.ip_network(network, False)
331+
self._prefix_len = self._network.prefixlen
332+
else:
333+
# This case is for MMDB lookups where performance is paramount.
334+
# This is why we don't generate the network unless .network is
335+
# used.
336+
self._network = None
337+
self._prefix_len = cast(int, raw.get("prefix_len"))
336338
self.ip_address = cast(str, raw.get("ip_address"))
337339

338340
def __repr__(self) -> str:
339-
return f"{self.__module__}.{self.__class__.__name__}({self.raw})"
341+
return f"{self.__module__}.{self.__class__.__name__}({self.to_dict()})"
340342

341343
@property
342344
def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]:

geoip2/records.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
from abc import ABCMeta
1414
from typing import Dict, Optional, Type, Sequence, Union
1515

16-
from geoip2.mixins import SimpleEquality
16+
from geoip2.mixins import Model
1717

1818

19-
class Record(SimpleEquality, metaclass=ABCMeta):
19+
class Record(Model, metaclass=ABCMeta):
2020
"""All records are subclasses of the abstract class ``Record``."""
2121

2222
def __repr__(self) -> str:
23-
args = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
23+
args = ", ".join(f"{k}={v!r}" for k, v in self.to_dict().items())
2424
return f"{self.__module__}.{self.__class__.__name__}({args})"
2525

2626

tests/models_test.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414

1515
class TestModels(unittest.TestCase):
16+
def setUp(self):
17+
self.maxDiff = 20_000
18+
1619
def test_insights_full(self) -> None:
1720
raw = {
1821
"city": {
@@ -85,7 +88,6 @@ def test_insights_full(self) -> None:
8588
"is_satellite_provider": True,
8689
"is_tor_exit_node": True,
8790
"isp": "Comcast",
88-
"network_speed": "cable/DSL",
8991
"organization": "Blorg",
9092
"static_ip_score": 1.3,
9193
"user_count": 2,
@@ -131,7 +133,7 @@ def test_insights_full(self) -> None:
131133
self.assertEqual(
132134
type(model.traits), geoip2.records.Traits, "geoip2.records.Traits object"
133135
)
134-
self.assertEqual(model.raw, raw, "raw method returns raw input")
136+
self.assertEqual(model.to_dict(), raw, "to_dict() method matches raw input")
135137
self.assertEqual(
136138
model.subdivisions[0].iso_code, "MN", "div 1 has correct iso_code"
137139
)
@@ -290,7 +292,9 @@ def test_city_full(self) -> None:
290292
self.assertEqual(
291293
type(model.traits), geoip2.records.Traits, "geoip2.records.Traits object"
292294
)
293-
self.assertEqual(model.raw, raw, "raw method returns raw input")
295+
self.assertEqual(
296+
model.to_dict(), raw, "to_dict method output matches raw input"
297+
)
294298
self.assertEqual(model.continent.geoname_id, 42, "continent geoname_id is 42")
295299
self.assertEqual(model.continent.code, "NA", "continent code is NA")
296300
self.assertEqual(
@@ -338,7 +342,7 @@ def test_city_full(self) -> None:
338342
True,
339343
"traits is_setellite_provider is True",
340344
)
341-
self.assertEqual(model.raw, raw, "raw method produces raw output")
345+
self.assertEqual(model.to_dict(), raw, "to_dict method matches raw input")
342346

343347
self.assertRegex(
344348
str(model), r"^geoip2.models.City\(\{.*geoname_id.*\}, \[.*en.*\]\)"

tests/webservice_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def test_country_ok(self):
110110
country.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network"
111111
)
112112
self.assertTrue(country.traits.is_anycast)
113-
self.assertEqual(country.raw, self.country, "raw response is correct")
113+
self.assertEqual(country.to_dict(), self.country, "raw response is correct")
114114

115115
def test_me(self):
116116
self.httpserver.expect_request(
@@ -357,6 +357,7 @@ def setUp(self):
357357
self.client_class = Client
358358
self.client = Client(42, "abcdef123456")
359359
self.client._base_uri = self.httpserver.url_for("/geoip/v2.1")
360+
self.maxDiff = 20_000
360361

361362
def run_client(self, v):
362363
return v
@@ -368,6 +369,7 @@ def setUp(self):
368369
self.client_class = AsyncClient
369370
self.client = AsyncClient(42, "abcdef123456")
370371
self.client._base_uri = self.httpserver.url_for("/geoip/v2.1")
372+
self.maxDiff = 20_000
371373

372374
def tearDown(self):
373375
self._loop.run_until_complete(self.client.close())

0 commit comments

Comments
 (0)