diff --git a/.travis.yml b/.travis.yml index 108e0779..ac9b5a7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: python matrix: include: - - python: 2.7 - - python: 3.5 - python: 3.6 - python: 3.7 - python: 3.8 @@ -11,7 +9,6 @@ matrix: - RUN_SNYK=1 - RUN_LINTER=1 - python: nightly - - python: pypy allow_failures: - python: nightly @@ -32,11 +29,12 @@ install: - pip install requests_mock coveralls - | if [[ $RUN_LINTER ]]; then - pip install --upgrade pylint black + pip install --upgrade pylint black mypy fi - "if [[ $RUN_SNYK && $SNYK_TOKEN ]]; then snyk test --org=maxmind; fi" script: - coverage run --source=geoip2 setup.py test + - "if [[ $RUN_LINTER ]]; then mypy geoip2 tests; fi" - "if [[ $RUN_LINTER ]]; then ./.travis-pylint.sh; fi" - "if [[ $RUN_LINTER ]]; then ./.travis-black.sh; fi" after_success: diff --git a/HISTORY.rst b/HISTORY.rst index de19ae3b..7ca4216a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,18 @@ History ------- +4.0.0 +++++++++++++++++++ + +* IMPORTANT: Python 2.7 and 3.5 support has been dropped. Python 3.6 or greater + is required. +* Type hints have been added. +* The attributes ``postal_code`` and ``postal_confidence`` have been removed + from ``geoip2.record.Location``. These would previously always be ``None``. +* ``user_id`` is no longer supported as a named argument for the constructor + on ``geoip2.webservice.Client``. Use ``account_id`` or a positional + parameter instead. + 3.0.0 (2019-12-20) ++++++++++++++++++ diff --git a/README.rst b/README.rst index 95425491..fda5e05b 100644 --- a/README.rst +++ b/README.rst @@ -331,12 +331,11 @@ Database Reader Exceptions -------------------------- If the database file does not exist or is not readable, the constructor will -raise a ``FileNotFoundError`` on Python 3 or an ``IOError`` on Python 2. -If the IP address passed to a method is invalid, a ``ValueError`` will be -raised. If the file is invalid or there is a bug in the reader, a -``maxminddb.InvalidDatabaseError`` will be raised with a description of the -problem. If an IP address is not in the database, a ``AddressNotFoundError`` -will be raised. +raise a ``FileNotFoundError``. If the IP address passed to a method is +invalid, a ``ValueError`` will be raised. If the file is invalid or there is a +bug in the reader, a ``maxminddb.InvalidDatabaseError`` will be raised with a +description of the problem. If an IP address is not in the database, a +``AddressNotFoundError`` will be raised. Values to use for Database or Dictionary Keys --------------------------------------------- @@ -402,8 +401,7 @@ correction, please `contact MaxMind support Requirements ------------ -This code requires Python 2.7+ or 3.5+. Older versions are not supported. -This library has been tested with CPython and PyPy. +Python 3.6 or greater is required. Older versions are not supported. The Requests HTTP library is also required. See for details. diff --git a/docs/conf.py b/docs/conf.py index 043aac71..650b33d1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,7 +49,7 @@ # General information about the project. project = "geoip2" -copyright = "2013-2019, MaxMind, Inc." +copyright = "2013-2020, MaxMind, Inc." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/index.rst b/docs/index.rst index ee0b1870..a1e03231 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,6 @@ Indices and tables * :ref:`modindex` * :ref:`search` -:copyright: (c) 2013-2019 by MaxMind, Inc. +:copyright: (c) 2013-2020 by MaxMind, Inc. :license: Apache License, Version 2.0 diff --git a/geoip2/__init__.py b/geoip2/__init__.py index 2d5ab987..ce64915a 100644 --- a/geoip2/__init__.py +++ b/geoip2/__init__.py @@ -1,7 +1,7 @@ # pylint:disable=C0111 __title__ = "geoip2" -__version__ = "3.0.0" +__version__ = "4.0.0" __author__ = "Gregory Oschwald" __license__ = "Apache License, Version 2.0" -__copyright__ = "Copyright (c) 2013-2019 Maxmind, Inc." +__copyright__ = "Copyright (c) 2013-2020 Maxmind, Inc." diff --git a/geoip2/compat.py b/geoip2/compat.py deleted file mode 100644 index 7467fcf6..00000000 --- a/geoip2/compat.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Intended for internal use only.""" -import sys - -import ipaddress - -# pylint: skip-file - -if sys.version_info[0] == 2: - - def compat_ip_address(address): - """Intended for internal use only.""" - if isinstance(address, bytes): - address = address.decode() - return ipaddress.ip_address(address) - - def compat_ip_network(network, strict=True): - """Intended for internal use only.""" - if isinstance(network, bytes): - network = network.decode() - return ipaddress.ip_network(network, strict) - - -else: - - def compat_ip_address(address): - """Intended for internal use only.""" - return ipaddress.ip_address(address) - - def compat_ip_network(network, strict=True): - """Intended for internal use only.""" - return ipaddress.ip_network(network, strict) diff --git a/geoip2/database.py b/geoip2/database.py index 43767042..10c1bdf0 100644 --- a/geoip2/database.py +++ b/geoip2/database.py @@ -5,11 +5,12 @@ """ import inspect +from typing import Any, cast, List, Optional, Type, Union import maxminddb # pylint: disable=unused-import -from maxminddb import ( +from maxminddb import ( # type: ignore MODE_AUTO, MODE_MMAP, MODE_MMAP_EXT, @@ -21,9 +22,20 @@ import geoip2 import geoip2.models import geoip2.errors +from geoip2.types import IPAddress +from geoip2.models import ( + ASN, + AnonymousIP, + City, + ConnectionType, + Country, + Domain, + Enterprise, + ISP, +) -class Reader(object): +class Reader: """GeoIP2 database Reader object. Instances of this class provide a reader for the GeoIP2 database format. @@ -47,7 +59,9 @@ class Reader(object): """ - def __init__(self, fileish, locales=None, mode=MODE_AUTO): + def __init__( + self, fileish: str, locales: Optional[List[str]] = None, mode: int = MODE_AUTO + ) -> None: """Create GeoIP2 Reader. :param fileish: The string path to the GeoIP2 database, or an existing @@ -94,13 +108,13 @@ def __init__(self, fileish, locales=None, mode=MODE_AUTO): self._db_type = self._db_reader.metadata().database_type self._locales = locales - def __enter__(self): + def __enter__(self) -> "Reader": return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None: self.close() - def country(self, ip_address): + def country(self, ip_address: IPAddress) -> Country: """Get the Country object for the IP address. :param ip_address: IPv4 or IPv6 address as a string. @@ -109,9 +123,11 @@ def country(self, ip_address): """ - return self._model_for(geoip2.models.Country, "Country", ip_address) + return cast( + Country, self._model_for(geoip2.models.Country, "Country", ip_address) + ) - def city(self, ip_address): + def city(self, ip_address: IPAddress) -> City: """Get the City object for the IP address. :param ip_address: IPv4 or IPv6 address as a string. @@ -119,9 +135,9 @@ def city(self, ip_address): :returns: :py:class:`geoip2.models.City` object """ - return self._model_for(geoip2.models.City, "City", ip_address) + return cast(City, self._model_for(geoip2.models.City, "City", ip_address)) - def anonymous_ip(self, ip_address): + def anonymous_ip(self, ip_address: IPAddress) -> AnonymousIP: """Get the AnonymousIP object for the IP address. :param ip_address: IPv4 or IPv6 address as a string. @@ -129,11 +145,14 @@ def anonymous_ip(self, ip_address): :returns: :py:class:`geoip2.models.AnonymousIP` object """ - return self._flat_model_for( - geoip2.models.AnonymousIP, "GeoIP2-Anonymous-IP", ip_address + return cast( + AnonymousIP, + self._flat_model_for( + geoip2.models.AnonymousIP, "GeoIP2-Anonymous-IP", ip_address + ), ) - def asn(self, ip_address): + def asn(self, ip_address: IPAddress) -> ASN: """Get the ASN object for the IP address. :param ip_address: IPv4 or IPv6 address as a string. @@ -141,9 +160,11 @@ def asn(self, ip_address): :returns: :py:class:`geoip2.models.ASN` object """ - return self._flat_model_for(geoip2.models.ASN, "GeoLite2-ASN", ip_address) + return cast( + ASN, self._flat_model_for(geoip2.models.ASN, "GeoLite2-ASN", ip_address) + ) - def connection_type(self, ip_address): + def connection_type(self, ip_address: IPAddress) -> ConnectionType: """Get the ConnectionType object for the IP address. :param ip_address: IPv4 or IPv6 address as a string. @@ -151,11 +172,14 @@ def connection_type(self, ip_address): :returns: :py:class:`geoip2.models.ConnectionType` object """ - return self._flat_model_for( - geoip2.models.ConnectionType, "GeoIP2-Connection-Type", ip_address + return cast( + ConnectionType, + self._flat_model_for( + geoip2.models.ConnectionType, "GeoIP2-Connection-Type", ip_address + ), ) - def domain(self, ip_address): + def domain(self, ip_address: IPAddress) -> Domain: """Get the Domain object for the IP address. :param ip_address: IPv4 or IPv6 address as a string. @@ -163,9 +187,12 @@ def domain(self, ip_address): :returns: :py:class:`geoip2.models.Domain` object """ - return self._flat_model_for(geoip2.models.Domain, "GeoIP2-Domain", ip_address) + return cast( + Domain, + self._flat_model_for(geoip2.models.Domain, "GeoIP2-Domain", ip_address), + ) - def enterprise(self, ip_address): + def enterprise(self, ip_address: IPAddress) -> Enterprise: """Get the Enterprise object for the IP address. :param ip_address: IPv4 or IPv6 address as a string. @@ -173,9 +200,12 @@ def enterprise(self, ip_address): :returns: :py:class:`geoip2.models.Enterprise` object """ - return self._model_for(geoip2.models.Enterprise, "Enterprise", ip_address) + return cast( + Enterprise, + self._model_for(geoip2.models.Enterprise, "Enterprise", ip_address), + ) - def isp(self, ip_address): + def isp(self, ip_address: IPAddress) -> ISP: """Get the ISP object for the IP address. :param ip_address: IPv4 or IPv6 address as a string. @@ -183,9 +213,11 @@ def isp(self, ip_address): :returns: :py:class:`geoip2.models.ISP` object """ - return self._flat_model_for(geoip2.models.ISP, "GeoIP2-ISP", ip_address) + return cast( + ISP, self._flat_model_for(geoip2.models.ISP, "GeoIP2-ISP", ip_address) + ) - def _get(self, database_type, ip_address): + def _get(self, database_type: str, ip_address: IPAddress) -> Any: if database_type not in self._db_type: caller = inspect.stack()[2][3] raise TypeError( @@ -197,29 +229,41 @@ def _get(self, database_type, ip_address): raise geoip2.errors.AddressNotFoundError( "The address %s is not in the database." % ip_address ) - return (record, prefix_len) - - def _model_for(self, model_class, types, ip_address): + return record, prefix_len + + def _model_for( + self, + model_class: Union[Type[Country], Type[Enterprise], Type[City]], + types: str, + ip_address: IPAddress, + ) -> Union[Country, Enterprise, City]: (record, prefix_len) = self._get(types, ip_address) traits = record.setdefault("traits", {}) traits["ip_address"] = ip_address traits["prefix_len"] = prefix_len return model_class(record, locales=self._locales) - def _flat_model_for(self, model_class, types, ip_address): + def _flat_model_for( + self, + model_class: Union[ + Type[Domain], Type[ISP], Type[ConnectionType], Type[ASN], Type[AnonymousIP] + ], + types: str, + ip_address: IPAddress, + ) -> Union[ConnectionType, ISP, AnonymousIP, Domain, ASN]: (record, prefix_len) = self._get(types, ip_address) record["ip_address"] = ip_address record["prefix_len"] = prefix_len return model_class(record) - def metadata(self): + def metadata(self) -> maxminddb.reader.Metadata: """The metadata for the open database. :returns: :py:class:`maxminddb.reader.Metadata` object """ return self._db_reader.metadata() - def close(self): + def close(self) -> None: """Closes the GeoIP2 database.""" self._db_reader.close() diff --git a/geoip2/errors.py b/geoip2/errors.py index 468b5858..d3d40d9b 100644 --- a/geoip2/errors.py +++ b/geoip2/errors.py @@ -4,6 +4,8 @@ """ +from typing import Optional + class GeoIP2Error(RuntimeError): """There was a generic error in GeoIP2. @@ -33,8 +35,10 @@ class HTTPError(GeoIP2Error): """ - def __init__(self, message, http_status=None, uri=None): - super(HTTPError, self).__init__(message) + def __init__( + self, message: str, http_status: Optional[int] = None, uri: Optional[str] = None + ) -> None: + super().__init__(message) self.http_status = http_status self.uri = uri diff --git a/geoip2/mixins.py b/geoip2/mixins.py index 0dbb2716..2209b7bd 100644 --- a/geoip2/mixins.py +++ b/geoip2/mixins.py @@ -1,14 +1,13 @@ """This package contains utility mixins""" # pylint: disable=too-few-public-methods from abc import ABCMeta +from typing import Any -class SimpleEquality(object): +class SimpleEquality(metaclass=ABCMeta): """Naive __dict__ equality mixin""" - __metaclass__ = ABCMeta - - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ def __ne__(self, other): diff --git a/geoip2/models.py b/geoip2/models.py index b0757db5..ab9a6953 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -13,9 +13,9 @@ # pylint: disable=too-many-instance-attributes,too-few-public-methods import ipaddress from abc import ABCMeta +from typing import Any, cast, Dict, List, Optional, Union import geoip2.records -from geoip2.compat import compat_ip_network from geoip2.mixins import SimpleEquality @@ -67,7 +67,16 @@ class Country(SimpleEquality): """ - def __init__(self, raw_response, locales=None): + continent: geoip2.records.Continent + country: geoip2.records.Country + maxmind: geoip2.records.MaxMind + registered_country: geoip2.records.Country + represented_country: geoip2.records.RepresentedCountry + traits: geoip2.records.Traits + + def __init__( + self, raw_response: Dict[str, Any], locales: Optional[List[str]] = None + ) -> None: if locales is None: locales = ["en"] self._locales = locales @@ -89,7 +98,7 @@ def __init__(self, raw_response, locales=None): self.traits = geoip2.records.Traits(**raw_response.get("traits", {})) self.raw = raw_response - def __repr__(self): + def __repr__(self) -> str: return "{module}.{class_name}({data}, {locales})".format( module=self.__module__, class_name=self.__class__.__name__, @@ -124,12 +133,20 @@ class City(Country): Location object for the requested IP address. + :type: :py:class:`geoip2.records.Location` + .. attribute:: maxmind Information related to your MaxMind account. :type: :py:class:`geoip2.records.MaxMind` + .. attribute:: postal + + Postal object for the requested IP address. + + :type: :py:class:`geoip2.records.Postal` + .. attribute:: registered_country The registered country object for the requested IP address. This record @@ -161,8 +178,15 @@ class City(Country): """ - def __init__(self, raw_response, locales=None): - super(City, self).__init__(raw_response, locales) + city: geoip2.records.City + location: geoip2.records.Location + postal: geoip2.records.Postal + subdivisions: geoip2.records.Subdivisions + + def __init__( + self, raw_response: Dict[str, Any], locales: Optional[List[str]] = None + ) -> None: + super().__init__(raw_response, locales) self.city = geoip2.records.City(locales, **raw_response.get("city", {})) self.location = geoip2.records.Location(**raw_response.get("location", {})) self.postal = geoip2.records.Postal(**raw_response.get("postal", {})) @@ -299,18 +323,19 @@ class Enterprise(City): """ -class SimpleModel(SimpleEquality): +class SimpleModel(SimpleEquality, metaclass=ABCMeta): """Provides basic methods for non-location models""" - __metaclass__ = ABCMeta + raw: Dict[str, Union[bool, str, int]] + ip_address: str - def __init__(self, raw): + def __init__(self, raw: Dict[str, Union[bool, str, int]]) -> None: self.raw = raw self._network = None self._prefix_len = raw.get("prefix_len") - self.ip_address = raw.get("ip_address") + self.ip_address = cast(str, raw.get("ip_address")) - def __repr__(self): + def __repr__(self) -> str: # pylint: disable=no-member return "{module}.{class_name}({data})".format( module=self.__module__, @@ -319,7 +344,7 @@ def __repr__(self): ) @property - def network(self): + def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: """The network for the record""" # This code is duplicated for performance reasons # pylint: disable=duplicate-code @@ -331,7 +356,7 @@ def network(self): prefix_len = self._prefix_len if ip_address is None or prefix_len is None: return None - network = compat_ip_network("{}/{}".format(ip_address, prefix_len), False) + network = ipaddress.ip_network("{}/{}".format(ip_address, prefix_len), False) self._network = network return network @@ -392,8 +417,14 @@ class AnonymousIP(SimpleModel): :type: ipaddress.IPv4Network or ipaddress.IPv6Network """ - def __init__(self, raw): - super(AnonymousIP, self).__init__(raw) + is_anonymous: bool + is_anonymous_vpn: bool + is_hosting_provider: bool + is_public_proxy: bool + is_tor_exit_node: bool + + def __init__(self, raw: Dict[str, bool]) -> None: + super().__init__(raw) # type: ignore self.is_anonymous = raw.get("is_anonymous", False) self.is_anonymous_vpn = raw.get("is_anonymous_vpn", False) self.is_hosting_provider = raw.get("is_hosting_provider", False) @@ -434,11 +465,18 @@ class ASN(SimpleModel): :type: ipaddress.IPv4Network or ipaddress.IPv6Network """ + autonomous_system_number: Optional[int] + autonomous_system_organization: Optional[str] + # pylint:disable=too-many-arguments - def __init__(self, raw): - super(ASN, self).__init__(raw) - self.autonomous_system_number = raw.get("autonomous_system_number") - self.autonomous_system_organization = raw.get("autonomous_system_organization") + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: + super().__init__(raw) + self.autonomous_system_number = cast( + Optional[int], raw.get("autonomous_system_number") + ) + self.autonomous_system_organization = cast( + Optional[str], raw.get("autonomous_system_organization") + ) class ConnectionType(SimpleModel): @@ -474,9 +512,11 @@ class ConnectionType(SimpleModel): :type: ipaddress.IPv4Network or ipaddress.IPv6Network """ - def __init__(self, raw): - super(ConnectionType, self).__init__(raw) - self.connection_type = raw.get("connection_type") + connection_type: Optional[str] + + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: + super().__init__(raw) + self.connection_type = cast(Optional[str], raw.get("connection_type")) class Domain(SimpleModel): @@ -506,9 +546,11 @@ class Domain(SimpleModel): """ - def __init__(self, raw): - super(Domain, self).__init__(raw) - self.domain = raw.get("domain") + domain: Optional[str] + + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: + super().__init__(raw) + self.domain = cast(Optional[str], raw.get("domain")) class ISP(ASN): @@ -556,8 +598,11 @@ class ISP(ASN): :type: ipaddress.IPv4Network or ipaddress.IPv6Network """ + isp: Optional[str] + organization: Optional[str] + # pylint:disable=too-many-arguments - def __init__(self, raw): - super(ISP, self).__init__(raw) - self.isp = raw.get("isp") - self.organization = raw.get("organization") + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: + super().__init__(raw) + self.isp = cast(Optional[str], raw.get("isp")) + self.organization = cast(Optional[str], raw.get("organization")) diff --git a/geoip2/records.py b/geoip2/records.py index acb29cd8..ce33676a 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -10,29 +10,32 @@ # pylint:disable=R0903 from abc import ABCMeta +from typing import Dict, List, Optional, Type, Union -from geoip2.compat import compat_ip_network from geoip2.mixins import SimpleEquality -class Record(SimpleEquality): +class Record(SimpleEquality, metaclass=ABCMeta): """All records are subclasses of the abstract class ``Record``.""" - __metaclass__ = ABCMeta - - def __repr__(self): + def __repr__(self) -> str: args = ", ".join("%s=%r" % x for x in self.__dict__.items()) return "{module}.{class_name}({data})".format( module=self.__module__, class_name=self.__class__.__name__, data=args ) -class PlaceRecord(Record): +class PlaceRecord(Record, metaclass=ABCMeta): """All records with :py:attr:`names` subclass :py:class:`PlaceRecord`.""" - __metaclass__ = ABCMeta + names: Dict[str, str] + _locales: List[str] - def __init__(self, locales=None, names=None): + def __init__( + self, + locales: Optional[List[str]] = None, + names: Optional[Dict[str, str]] = None, + ) -> None: if locales is None: locales = ["en"] self._locales = locales @@ -41,7 +44,7 @@ def __init__(self, locales=None, names=None): self.names = names @property - def name(self): + def name(self) -> Optional[str]: """Dict with locale codes as keys and localized name as value.""" # pylint:disable=E1101 return next((self.names.get(x) for x in self._locales if x in self.names), None) @@ -86,10 +89,20 @@ class City(PlaceRecord): """ - def __init__(self, locales=None, confidence=None, geoname_id=None, names=None, **_): + confidence: Optional[int] + geoname_id: Optional[int] + + def __init__( + self, + locales: Optional[List[str]] = None, + confidence: Optional[int] = None, + geoname_id: Optional[int] = None, + names: Optional[Dict[str, str]] = None, + **_ + ) -> None: self.confidence = confidence self.geoname_id = geoname_id - super(City, self).__init__(locales, names) + super().__init__(locales, names) class Continent(PlaceRecord): @@ -130,10 +143,20 @@ class Continent(PlaceRecord): """ - def __init__(self, locales=None, code=None, geoname_id=None, names=None, **_): + code: Optional[str] + geoname_id: Optional[int] + + def __init__( + self, + locales: Optional[List[str]] = None, + code: Optional[str] = None, + geoname_id: Optional[int] = None, + names: Optional[Dict[str, str]] = None, + **_ + ) -> None: self.code = code self.geoname_id = geoname_id - super(Continent, self).__init__(locales, names) + super().__init__(locales, names) class Country(PlaceRecord): @@ -188,21 +211,26 @@ class Country(PlaceRecord): """ + confidence: Optional[int] + geoname_id: Optional[int] + is_in_european_union: bool + iso_code: Optional[str] + def __init__( self, - locales=None, - confidence=None, - geoname_id=None, - is_in_european_union=False, - iso_code=None, - names=None, + locales: Optional[List[str]] = None, + confidence: Optional[int] = None, + geoname_id: Optional[int] = None, + is_in_european_union: bool = False, + iso_code: Optional[str] = None, + names: Optional[Dict[str, str]] = None, **_ - ): + ) -> None: self.confidence = confidence self.geoname_id = geoname_id self.is_in_european_union = is_in_european_union self.iso_code = iso_code - super(Country, self).__init__(locales, names) + super().__init__(locales, names) class RepresentedCountry(Country): @@ -267,20 +295,22 @@ class RepresentedCountry(Country): """ + type: Optional[str] + def __init__( self, - locales=None, - confidence=None, - geoname_id=None, - is_in_european_union=False, - iso_code=None, - names=None, + locales: Optional[List[str]] = None, + confidence: Optional[int] = None, + geoname_id: Optional[int] = None, + is_in_european_union: bool = False, + iso_code: Optional[str] = None, + names: Optional[Dict[str, str]] = None, # pylint:disable=redefined-builtin - type=None, + type: Optional[str] = None, **_ - ): + ) -> None: self.type = type - super(RepresentedCountry, self).__init__( + super().__init__( locales, confidence, geoname_id, is_in_european_union, iso_code, names ) @@ -352,27 +382,31 @@ class Location(Record): """ + average_income: Optional[int] + accuracy_radius: Optional[int] + latitude: Optional[float] + longitude: Optional[float] + metro_code: Optional[int] + population_density: Optional[int] + time_zone: Optional[str] + def __init__( self, - average_income=None, - accuracy_radius=None, - latitude=None, - longitude=None, - metro_code=None, - population_density=None, - postal_code=None, - postal_confidence=None, - time_zone=None, + average_income: Optional[int] = None, + accuracy_radius: Optional[int] = None, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + metro_code: Optional[int] = None, + population_density: Optional[int] = None, + time_zone: Optional[str] = None, **_ - ): + ) -> None: self.average_income = average_income self.accuracy_radius = accuracy_radius self.latitude = latitude self.longitude = longitude self.metro_code = metro_code self.population_density = population_density - self.postal_code = postal_code - self.postal_confidence = postal_confidence self.time_zone = time_zone @@ -390,7 +424,9 @@ class MaxMind(Record): """ - def __init__(self, queries_remaining=None, **_): + queries_remaining: Optional[int] + + def __init__(self, queries_remaining: Optional[int] = None, **_) -> None: self.queries_remaining = queries_remaining @@ -422,7 +458,12 @@ class Postal(Record): """ - def __init__(self, code=None, confidence=None, **_): + code: Optional[str] + confidence: Optional[int] + + def __init__( + self, code: Optional[str] = None, confidence: Optional[int] = None, **_ + ) -> None: self.code = code self.confidence = confidence @@ -475,19 +516,23 @@ class Subdivision(PlaceRecord): """ + confidence: Optional[int] + geoname_id: Optional[int] + iso_code: Optional[str] + def __init__( self, - locales=None, - confidence=None, - geoname_id=None, - iso_code=None, - names=None, + locales: Optional[List[str]] = None, + confidence: Optional[int] = None, + geoname_id: Optional[int] = None, + iso_code: Optional[str] = None, + names: Optional[Dict[str, str]] = None, **_ - ): + ) -> None: self.confidence = confidence self.geoname_id = geoname_id self.iso_code = iso_code - super(Subdivision, self).__init__(locales, names) + super().__init__(locales, names) class Subdivisions(tuple): @@ -502,17 +547,21 @@ class Subdivisions(tuple): This attribute is returned by ``city``, ``enterprise``, and ``insights``. """ - def __new__(cls, locales, *subdivisions): - subdivisions = [Subdivision(locales, **x) for x in subdivisions] - obj = super(cls, Subdivisions).__new__(cls, subdivisions) + def __new__( + cls: Type["Subdivisions"], locales: Optional[List[str]], *subdivisions + ) -> "Subdivisions": + subobjs = tuple(Subdivision(locales, **x) for x in subdivisions) + obj = super().__new__(cls, subobjs) # type: ignore return obj - def __init__(self, locales, *subdivisions): # pylint:disable=W0613 + def __init__( + self, locales: Optional[List[str]], *subdivisions # pylint:disable=W0613 + ) -> None: self._locales = locales - super(Subdivisions, self).__init__() + super().__init__() @property - def most_specific(self): + def most_specific(self) -> Subdivision: """The most specific (smallest) subdivision available. If there are no :py:class:`Subdivision` objects for the response, @@ -739,30 +788,51 @@ class Traits(Record): """ + autonomous_system_number: Optional[int] + autonomous_system_organization: Optional[str] + connection_type: Optional[str] + domain: Optional[str] + is_anonymous: bool + is_anonymous_proxy: bool + is_anonymous_vpn: bool + is_hosting_provider: bool + is_legitimate_proxy: bool + is_public_proxy: bool + is_satellite_provider: bool + is_tor_exit_node: bool + isp: Optional[str] + ip_address: Optional[str] + organization: Optional[str] + static_ip_score: Optional[float] + user_count: Optional[int] + user_type: Optional[str] + _network: Optional[str] + _prefix_len: Optional[int] + def __init__( self, - autonomous_system_number=None, - autonomous_system_organization=None, - connection_type=None, - domain=None, - is_anonymous=False, - is_anonymous_proxy=False, - is_anonymous_vpn=False, - is_hosting_provider=False, - is_legitimate_proxy=False, - is_public_proxy=False, - is_satellite_provider=False, - is_tor_exit_node=False, - isp=None, - ip_address=None, - network=None, - organization=None, - prefix_len=None, - static_ip_score=None, - user_count=None, - user_type=None, + autonomous_system_number: Optional[int] = None, + autonomous_system_organization: Optional[str] = None, + connection_type: Optional[str] = None, + domain: Optional[str] = None, + is_anonymous: bool = False, + is_anonymous_proxy: bool = False, + is_anonymous_vpn: bool = False, + is_hosting_provider: bool = False, + is_legitimate_proxy: bool = False, + is_public_proxy: bool = False, + is_satellite_provider: bool = False, + is_tor_exit_node: bool = False, + isp: Optional[str] = None, + ip_address: Optional[str] = None, + network: Optional[str] = None, + organization: Optional[str] = None, + prefix_len: Optional[int] = None, + static_ip_score: Optional[float] = None, + user_count: Optional[int] = None, + user_type: Optional[str] = None, **_ - ): + ) -> None: self.autonomous_system_number = autonomous_system_number self.autonomous_system_organization = autonomous_system_organization self.connection_type = connection_type @@ -787,7 +857,7 @@ def __init__( # This code is duplicated for performance reasons # pylint: disable=duplicate-code @property - def network(self): + def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: """The network for the record""" network = self._network if isinstance(network, (ipaddress.IPv4Network, ipaddress.IPv6Network)): @@ -799,6 +869,6 @@ def network(self): if ip_address is None or prefix_len is None: return None network = "{}/{}".format(ip_address, prefix_len) - network = compat_ip_network(network, False) + network = ipaddress.ip_network(network, False) self._network = network - return network + return network # type: ignore diff --git a/geoip2/types.py b/geoip2/types.py new file mode 100644 index 00000000..ba6d2b52 --- /dev/null +++ b/geoip2/types.py @@ -0,0 +1,6 @@ +"""Provides types used internally""" + +from ipaddress import IPv4Address, IPv6Address +from typing import Union + +IPAddress = Union[str, IPv6Address, IPv4Address] diff --git a/geoip2/webservice.py b/geoip2/webservice.py index 4f43cae0..54d49930 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -25,16 +25,16 @@ """ -import requests +import ipaddress +from typing import Any, cast, List, Optional, Type, Union +import requests +from requests.models import Response from requests.utils import default_user_agent import geoip2 import geoip2.models - -from .compat import compat_ip_address - -from .errors import ( +from geoip2.errors import ( AddressNotFoundError, AuthenticationError, GeoIP2Error, @@ -43,9 +43,11 @@ OutOfQueriesError, PermissionRequiredError, ) +from geoip2.models import City, Country, Insights +from geoip2.types import IPAddress -class Client(object): +class Client: """Creates a new client object. It accepts the following required arguments: @@ -85,31 +87,23 @@ class Client(object): * pt-BR -- Brazilian Portuguese * ru -- Russian * zh-CN -- Simplified Chinese. + :param timeout: The timeout to use when waiting on the request. This sets + both the connect timeout and the read timeout. """ def __init__( self, - account_id=None, - license_key=None, - host="geoip.maxmind.com", - locales=None, - timeout=None, - # This is deprecated and not documented for that reason. - # It can be removed if we do a major release in the future. - user_id=None, - ): + account_id: int, + license_key: str, + host: str = "geoip.maxmind.com", + locales: Optional[List[str]] = None, + timeout: Optional[float] = None, + ) -> None: """Construct a Client.""" # pylint: disable=too-many-arguments if locales is None: locales = ["en"] - if account_id is None: - account_id = user_id - - if account_id is None: - raise TypeError("The account_id is a required parameter") - if license_key is None: - raise TypeError("The license_key is a required parameter") self._locales = locales # requests 2.12.2 requires that the username passed to auth be bytes @@ -121,7 +115,7 @@ def __init__( self._base_uri = "https://%s/geoip/v2.1" % host self._timeout = timeout - def city(self, ip_address="me"): + def city(self, ip_address: IPAddress = "me") -> City: """Call GeoIP2 Precision City endpoint with the specified IP. :param ip_address: IPv4 or IPv6 address as a string. If no @@ -131,9 +125,9 @@ def city(self, ip_address="me"): :returns: :py:class:`geoip2.models.City` object """ - return self._response_for("city", geoip2.models.City, ip_address) + return cast(City, self._response_for("city", geoip2.models.City, ip_address)) - def country(self, ip_address="me"): + def country(self, ip_address: IPAddress = "me") -> Country: """Call the GeoIP2 Country endpoint with the specified IP. :param ip_address: IPv4 or IPv6 address as a string. If no address @@ -143,9 +137,11 @@ def country(self, ip_address="me"): :returns: :py:class:`geoip2.models.Country` object """ - return self._response_for("country", geoip2.models.Country, ip_address) + return cast( + Country, self._response_for("country", geoip2.models.Country, ip_address) + ) - def insights(self, ip_address="me"): + def insights(self, ip_address: IPAddress = "me") -> Insights: """Call the GeoIP2 Precision: Insights endpoint with the specified IP. :param ip_address: IPv4 or IPv6 address as a string. If no address @@ -155,12 +151,19 @@ def insights(self, ip_address="me"): :returns: :py:class:`geoip2.models.Insights` object """ - return self._response_for("insights", geoip2.models.Insights, ip_address) + return cast( + Insights, self._response_for("insights", geoip2.models.Insights, ip_address) + ) - def _response_for(self, path, model_class, ip_address): + def _response_for( + self, + path: str, + model_class: Union[Type[Insights], Type[City], Type[Country]], + ip_address: IPAddress, + ) -> Union[Country, City, Insights]: if ip_address != "me": - ip_address = str(compat_ip_address(ip_address)) - uri = "/".join([self._base_uri, path, ip_address]) + ip_address = ipaddress.ip_address(ip_address) + uri = "/".join([self._base_uri, path, str(ip_address)]) response = requests.get( uri, auth=(self._account_id, self._license_key), @@ -172,13 +175,15 @@ def _response_for(self, path, model_class, ip_address): body = self._handle_success(response, uri) return model_class(body, locales=self._locales) - def _user_agent(self): + @staticmethod + def _user_agent() -> str: return "GeoIP2 Python Client v%s (%s)" % ( geoip2.__version__, default_user_agent(), ) - def _handle_success(self, response, uri): + @staticmethod + def _handle_success(response: Response, uri: str) -> Any: try: return response.json() except ValueError as ex: @@ -190,7 +195,7 @@ def _handle_success(self, response, uri): uri, ) - def _exception_for_error(self, response, uri): + def _exception_for_error(self, response: Response, uri: str) -> GeoIP2Error: status = response.status_code if 400 <= status < 500: @@ -199,7 +204,9 @@ def _exception_for_error(self, response, uri): return self._exception_for_5xx_status(status, uri) return self._exception_for_non_200_status(status, uri) - def _exception_for_4xx_status(self, response, status, uri): + def _exception_for_4xx_status( + self, response: Response, status: int, uri: str + ) -> GeoIP2Error: if not response.content: return HTTPError( "Received a %(status)i error for %(uri)s " "with no body." % locals(), @@ -209,7 +216,7 @@ def _exception_for_4xx_status(self, response, status, uri): if response.headers["Content-Type"].find("json") == -1: return HTTPError( "Received a %i for %s with the following " - "body: %s" % (status, uri, response.content), + "body: %s" % (status, uri, str(response.content)), status, uri, ) @@ -233,7 +240,16 @@ def _exception_for_4xx_status(self, response, status, uri): uri, ) - def _exception_for_web_service_error(self, message, code, status, uri): + @staticmethod + def _exception_for_web_service_error( + message: str, code: str, status: int, uri: str + ) -> Union[ + AuthenticationError, + AddressNotFoundError, + PermissionRequiredError, + OutOfQueriesError, + InvalidRequestError, + ]: if code in ("IP_ADDRESS_NOT_FOUND", "IP_ADDRESS_RESERVED"): return AddressNotFoundError(message) if code in ( @@ -252,14 +268,16 @@ def _exception_for_web_service_error(self, message, code, status, uri): return InvalidRequestError(message, code, status, uri) - def _exception_for_5xx_status(self, status, uri): + @staticmethod + def _exception_for_5xx_status(status: int, uri: str) -> HTTPError: return HTTPError( "Received a server error (%(status)i) for " "%(uri)s" % locals(), status, uri, ) - def _exception_for_non_200_status(self, status, uri): + @staticmethod + def _exception_for_non_200_status(status: int, uri: str) -> HTTPError: return HTTPError( "Received a very surprising HTTP status " "(%(status)i) for %(uri)s" % locals(), diff --git a/pylintrc b/pylintrc index bab00808..53a1201f 100644 --- a/pylintrc +++ b/pylintrc @@ -1,5 +1,5 @@ [MESSAGES CONTROL] -disable=C0330,R0201,R0205,W0105 +disable=C0330 [BASIC] diff --git a/setup.cfg b/setup.cfg index f038699d..cdfe10f1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,5 +2,9 @@ build_html = build_sphinx -b html --build-dir docs sdist = build_html sdist +[flake8] +# black uses 88 : ¯\_(ツ)_/¯ +max-line-length = 88 + [wheel] universal = 1 diff --git a/tests/database_test.py b/tests/database_test.py index b0d325b5..0817bb96 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -3,33 +3,23 @@ from __future__ import unicode_literals +import ipaddress import sys +import unittest sys.path.append("..") import geoip2.database import maxminddb -import ipaddress - -from ipaddress import IPv4Network, IPv6Network try: import maxminddb.extension except ImportError: - maxminddb.extension = None - -if sys.version_info[:2] == (2, 6): - import unittest2 as unittest -else: - import unittest - -if sys.version_info[0] == 2: - unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp - unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches + maxminddb.extension = None # type: ignore -class BaseTestReader(object): - def test_language_list(self): +class BaseTestReader(unittest.TestCase): + def test_language_list(self) -> None: reader = geoip2.database.Reader( "tests/data/test-data/GeoIP2-Country-Test.mmdb", ["xx", "ru", "pt-BR", "es", "en"], @@ -39,7 +29,7 @@ def test_language_list(self): self.assertEqual(record.country.name, "Великобритания") reader.close() - def test_unknown_address(self): + def test_unknown_address(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") with self.assertRaisesRegex( geoip2.errors.AddressNotFoundError, @@ -48,7 +38,7 @@ def test_unknown_address(self): reader.city("10.10.10.10") reader.close() - def test_wrong_database(self): + def test_wrong_database(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") with self.assertRaisesRegex( TypeError, @@ -57,7 +47,7 @@ def test_wrong_database(self): reader.country("1.1.1.1") reader.close() - def test_invalid_address(self): + def test_invalid_address(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") with self.assertRaisesRegex( ValueError, "u?'invalid' does not appear to be an " "IPv4 or IPv6 address" @@ -65,7 +55,7 @@ def test_invalid_address(self): reader.city("invalid") reader.close() - def test_anonymous_ip(self): + def test_anonymous_ip(self) -> None: reader = geoip2.database.Reader( "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb" ) @@ -81,7 +71,7 @@ def test_anonymous_ip(self): self.assertEqual(record.network, ipaddress.ip_network("1.2.0.0/16")) reader.close() - def test_asn(self): + def test_asn(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoLite2-ASN-Test.mmdb") ip_address = "1.128.0.0" @@ -102,7 +92,7 @@ def test_asn(self): reader.close() - def test_city(self): + def test_city(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") record = reader.city("81.2.69.160") @@ -117,7 +107,7 @@ def test_city(self): reader.close() - def test_connection_type(self): + def test_connection_type(self) -> None: reader = geoip2.database.Reader( "tests/data/test-data/GeoIP2-Connection-Type-Test.mmdb" ) @@ -141,7 +131,7 @@ def test_connection_type(self): reader.close() - def test_country(self): + def test_country(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-Country-Test.mmdb") record = reader.country("81.2.69.160") self.assertEqual( @@ -152,7 +142,7 @@ def test_country(self): self.assertEqual(record.registered_country.is_in_european_union, False) reader.close() - def test_domain(self): + def test_domain(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-Domain-Test.mmdb") ip_address = "1.2.0.0" @@ -172,7 +162,7 @@ def test_domain(self): reader.close() - def test_enterprise(self): + def test_enterprise(self) -> None: with geoip2.database.Reader( "tests/data/test-data/GeoIP2-Enterprise-Test.mmdb" ) as reader: @@ -191,7 +181,7 @@ def test_enterprise(self): record.traits.network, ipaddress.ip_network("74.209.16.0/20") ) - def test_isp(self): + def test_isp(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-ISP-Test.mmdb") ip_address = "1.128.0.0" @@ -213,7 +203,7 @@ def test_isp(self): reader.close() - def test_context_manager(self): + def test_context_manager(self) -> None: with geoip2.database.Reader( "tests/data/test-data/GeoIP2-Country-Test.mmdb" ) as reader: @@ -222,19 +212,19 @@ def test_context_manager(self): @unittest.skipUnless(maxminddb.extension, "No C extension module found. Skipping tests") -class TestExtensionReader(BaseTestReader, unittest.TestCase): +class TestExtensionReader(BaseTestReader): mode = geoip2.database.MODE_MMAP_EXT -class TestMMAPReader(BaseTestReader, unittest.TestCase): +class TestMMAPReader(BaseTestReader): mode = geoip2.database.MODE_MMAP -class TestFileReader(BaseTestReader, unittest.TestCase): +class TestFileReader(BaseTestReader): mode = geoip2.database.MODE_FILE -class TestMemoryReader(BaseTestReader, unittest.TestCase): +class TestMemoryReader(BaseTestReader): mode = geoip2.database.MODE_MEMORY @@ -242,5 +232,5 @@ class TestFDReader(unittest.TestCase): mode = geoip2.database.MODE_FD -class TestAutoReader(BaseTestReader, unittest.TestCase): +class TestAutoReader(BaseTestReader): mode = geoip2.database.MODE_AUTO diff --git a/tests/models_test.py b/tests/models_test.py index a6e63baa..ed43215d 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -4,23 +4,16 @@ from __future__ import unicode_literals import sys +from typing import Dict +import unittest sys.path.append("..") import geoip2.models -if sys.version_info[:2] == (2, 6): - import unittest2 as unittest -else: - import unittest - -if sys.version_info[0] == 2: - unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp - unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches - class TestModels(unittest.TestCase): - def test_insights_full(self): + def test_insights_full(self) -> None: raw = { "city": { "confidence": 76, @@ -199,7 +192,7 @@ def test_insights_full(self): self.assertEqual(model.traits.user_count, 2) self.assertEqual(model.traits.static_ip_score, 1.3) - def test_insights_min(self): + def test_insights_min(self) -> None: model = geoip2.models.Insights({"traits": {"ip_address": "5.6.7.8"}}) self.assertEqual( type(model), geoip2.models.Insights, "geoip2.models.Insights object" @@ -238,7 +231,7 @@ def test_insights_min(self): model.subdivisions.most_specific.names, {}, "Empty names hash returned" ) - def test_city_full(self): + def test_city_full(self) -> None: raw = { "continent": { "code": "NA", @@ -334,7 +327,7 @@ def test_city_full(self): self.assertFalse(model == True, "__eq__ does not blow up on weird input") - def test_unknown_keys(self): + def test_unknown_keys(self) -> None: model = geoip2.models.City( { "city": {"invalid": 0}, @@ -350,15 +343,15 @@ def test_unknown_keys(self): } ) with self.assertRaises(AttributeError): - model.unk_base + model.unk_base # type: ignore with self.assertRaises(AttributeError): - model.traits.invalid + model.traits.invalid # type: ignore self.assertEqual(model.traits.ip_address, "1.2.3.4", "correct ip") class TestNames(unittest.TestCase): - raw = { + raw: Dict = { "continent": { "code": "NA", "geoname_id": 42, @@ -384,7 +377,7 @@ class TestNames(unittest.TestCase): "traits": {"ip_address": "1.2.3.4",}, } - def test_names(self): + def test_names(self) -> None: model = geoip2.models.Country(self.raw, locales=["sq", "ar"]) self.assertEqual( model.continent.names, @@ -397,7 +390,7 @@ def test_names(self): "Correct names dict for country", ) - def test_three_locales(self): + def test_three_locales(self) -> None: model = geoip2.models.Country(self.raw, locales=["fr", "zh-CN", "en"]) self.assertEqual( model.continent.name, @@ -406,7 +399,7 @@ def test_three_locales(self): ) self.assertEqual(model.country.name, "États-Unis", "country name is in French") - def test_two_locales(self): + def test_two_locales(self) -> None: model = geoip2.models.Country(self.raw, locales=["ak", "fr"]) self.assertEqual( model.continent.name, @@ -415,7 +408,7 @@ def test_two_locales(self): ) self.assertEqual(model.country.name, "États-Unis", "country name is in French") - def test_unknown_locale(self): + def test_unknown_locale(self) -> None: model = geoip2.models.Country(self.raw, locales=["aa"]) self.assertEqual( model.continent.name, None, "continent name is undef (no Afar available)" @@ -424,7 +417,7 @@ def test_unknown_locale(self): model.country.name, None, "country name is in None (no Afar available)" ) - def test_german(self): + def test_german(self) -> None: model = geoip2.models.Country(self.raw, locales=["de"]) self.assertEqual( model.continent.name, "Nordamerika", "Correct german name for continent" diff --git a/tests/webservice_test.py b/tests/webservice_test.py index c3721e2d..d2600af0 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -1,13 +1,16 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import copy +import ipaddress import sys +from typing import cast, Dict +import unittest sys.path.append("..") -import copy import geoip2 -import requests_mock +import requests_mock # type: ignore from geoip2.errors import ( AddressNotFoundError, AuthenticationError, @@ -18,16 +21,6 @@ PermissionRequiredError, ) from geoip2.webservice import Client -from geoip2.compat import compat_ip_network - -if sys.version_info[:2] == (2, 6): - import unittest2 as unittest -else: - import unittest - -if sys.version_info[0] == 2: - unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp - unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches class TestClient(unittest.TestCase): @@ -54,7 +47,7 @@ def setUp(self): # this is not a comprehensive representation of the # JSON from the server - insights = copy.deepcopy(country) + insights = cast(Dict, copy.deepcopy(country)) insights["traits"]["user_count"] = 2 insights["traits"]["static_ip_score"] = 1.3 @@ -106,7 +99,7 @@ def test_country_ok(self, mock): "registered_country is_in_european_union is True", ) self.assertEqual( - country.traits.network, compat_ip_network("1.2.3.0/24"), "network" + country.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" ) self.assertEqual(country.raw, self.country, "raw response is correct") @@ -315,7 +308,7 @@ def test_city_ok(self, mock): city = self.client.city("1.2.3.4") self.assertEqual(type(city), geoip2.models.City, "return value of client.city") self.assertEqual( - city.traits.network, compat_ip_network("1.2.3.0/24"), "network" + city.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" ) @requests_mock.mock() @@ -331,20 +324,17 @@ def test_insights_ok(self, mock): type(insights), geoip2.models.Insights, "return value of client.insights" ) self.assertEqual( - insights.traits.network, compat_ip_network("1.2.3.0/24"), "network" + insights.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" ) 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") def test_named_constructor_args(self): - id = "47" + id = 47 key = "1234567890ab" - for client in ( - Client(account_id=id, license_key=key), - Client(user_id=id, license_key=key), - ): - self.assertEqual(client._account_id, id) - self.assertEqual(client._license_key, key) + client = Client(account_id=id, license_key=key) + self.assertEqual(client._account_id, str(id)) + self.assertEqual(client._license_key, key) def test_missing_constructor_args(self): with self.assertRaises(TypeError):