From bf50f5c234ede0be3f01554a937d89b7da3b05c1 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 12:21:02 -0700 Subject: [PATCH 01/18] Drop Python 2 support --- .travis.yml | 3 --- HISTORY.rst | 6 ++++++ README.rst | 14 ++++++-------- geoip2/compat.py | 31 ------------------------------- geoip2/database.py | 2 +- geoip2/models.py | 3 +-- geoip2/records.py | 3 +-- geoip2/webservice.py | 6 ++---- tests/webservice_test.py | 10 +++++----- 9 files changed, 22 insertions(+), 56 deletions(-) delete mode 100644 geoip2/compat.py diff --git a/.travis.yml b/.travis.yml index 108e0779..f43fc204 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 diff --git a/HISTORY.rst b/HISTORY.rst index de19ae3b..ac4f072b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ History ------- +4.0.0 +++++++++++++++++++ + +* IMPORTANT: Python 2.7 and 3.5 support has been dropped. Python 3.6 or greater + is required. + 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/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..f0cdaaf6 100644 --- a/geoip2/database.py +++ b/geoip2/database.py @@ -197,7 +197,7 @@ 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) + return record, prefix_len def _model_for(self, model_class, types, ip_address): (record, prefix_len) = self._get(types, ip_address) diff --git a/geoip2/models.py b/geoip2/models.py index b0757db5..81e938ec 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -15,7 +15,6 @@ from abc import ABCMeta import geoip2.records -from geoip2.compat import compat_ip_network from geoip2.mixins import SimpleEquality @@ -331,7 +330,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 diff --git a/geoip2/records.py b/geoip2/records.py index acb29cd8..5c4a20d1 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -11,7 +11,6 @@ # pylint:disable=R0903 from abc import ABCMeta -from geoip2.compat import compat_ip_network from geoip2.mixins import SimpleEquality @@ -799,6 +798,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 diff --git a/geoip2/webservice.py b/geoip2/webservice.py index 4f43cae0..35391144 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -25,15 +25,13 @@ """ +import ipaddress import requests - from requests.utils import default_user_agent import geoip2 import geoip2.models -from .compat import compat_ip_address - from .errors import ( AddressNotFoundError, AuthenticationError, @@ -159,7 +157,7 @@ def insights(self, ip_address="me"): def _response_for(self, path, model_class, ip_address): if ip_address != "me": - ip_address = str(compat_ip_address(ip_address)) + ip_address = str(ipaddress.ip_address(ip_address)) uri = "/".join([self._base_uri, path, ip_address]) response = requests.get( uri, diff --git a/tests/webservice_test.py b/tests/webservice_test.py index c3721e2d..0a37415c 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -1,11 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import copy +import ipaddress import sys sys.path.append("..") -import copy import geoip2 import requests_mock from geoip2.errors import ( @@ -18,7 +19,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 @@ -106,7 +106,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 +315,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,7 +331,7 @@ 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") From e88d435f7b422d7bd4cdf41ecc5fc7b7a2e54a0b Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 12:34:38 -0700 Subject: [PATCH 02/18] Bump copyright year --- docs/conf.py | 2 +- docs/index.rst | 2 +- geoip2/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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..7a3cecc3 100644 --- a/geoip2/__init__.py +++ b/geoip2/__init__.py @@ -4,4 +4,4 @@ __version__ = "3.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." From 219473d1b6c46eb0ef187b117751892e5026c201 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 12:37:50 -0700 Subject: [PATCH 03/18] Set flake8 line length Although we don't currently use flake8 in CI, this is nice for anyone who has it enabled in their editor. --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) 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 From 8dc844cfc0369907b6bf56e713d88c3bf2f3a60a Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 13:29:50 -0700 Subject: [PATCH 04/18] Remove user_id support --- geoip2/webservice.py | 10 ---------- tests/webservice_test.py | 9 +++------ 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/geoip2/webservice.py b/geoip2/webservice.py index 35391144..9da32704 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -93,21 +93,11 @@ def __init__( 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, ): """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 diff --git a/tests/webservice_test.py b/tests/webservice_test.py index 0a37415c..f6e836aa 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -339,12 +339,9 @@ def test_insights_ok(self, mock): def test_named_constructor_args(self): 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, id) + self.assertEqual(client._license_key, key) def test_missing_constructor_args(self): with self.assertRaises(TypeError): From 0c55ecbe7cc801ffab2cd272055ceab6edc2b287 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 12:48:52 -0700 Subject: [PATCH 05/18] Add type annotations throughout code --- geoip2/database.py | 98 +++++++++++++++++------- geoip2/errors.py | 6 +- geoip2/mixins.py | 3 +- geoip2/models.py | 27 ++++--- geoip2/records.py | 158 +++++++++++++++++++++++---------------- geoip2/types.py | 6 ++ geoip2/webservice.py | 84 ++++++++++++++------- tests/database_test.py | 30 ++++---- tests/models_test.py | 25 ++++--- tests/webservice_test.py | 9 ++- 10 files changed, 283 insertions(+), 163 deletions(-) create mode 100644 geoip2/types.py diff --git a/geoip2/database.py b/geoip2/database.py index f0cdaaf6..6668f390 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,6 +22,17 @@ 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): @@ -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( @@ -199,27 +231,39 @@ def _get(self, database_type, ip_address): ) return record, prefix_len - def _model_for(self, model_class, types, ip_address): + 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..645b24e7 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,7 +35,9 @@ class HTTPError(GeoIP2Error): """ - def __init__(self, message, http_status=None, uri=None): + def __init__( + self, message: str, http_status: Optional[int] = None, uri: Optional[str] = None + ) -> None: super(HTTPError, self).__init__(message) self.http_status = http_status self.uri = uri diff --git a/geoip2/mixins.py b/geoip2/mixins.py index 0dbb2716..7134bde0 100644 --- a/geoip2/mixins.py +++ b/geoip2/mixins.py @@ -1,6 +1,7 @@ """This package contains utility mixins""" # pylint: disable=too-few-public-methods from abc import ABCMeta +from typing import Any class SimpleEquality(object): @@ -8,7 +9,7 @@ class SimpleEquality(object): __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 81e938ec..9f7a47cb 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -13,6 +13,7 @@ # pylint: disable=too-many-instance-attributes,too-few-public-methods import ipaddress from abc import ABCMeta +from typing import Any, Dict, List, Optional, Union import geoip2.records from geoip2.mixins import SimpleEquality @@ -66,7 +67,9 @@ class Country(SimpleEquality): """ - def __init__(self, raw_response, locales=None): + def __init__( + self, raw_response: Dict[str, Any], locales: Optional[List[str]] = None + ) -> None: if locales is None: locales = ["en"] self._locales = locales @@ -88,7 +91,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__, @@ -160,7 +163,9 @@ class City(Country): """ - def __init__(self, raw_response, locales=None): + def __init__( + self, raw_response: Dict[str, Any], locales: Optional[List[str]] = None + ) -> None: super(City, self).__init__(raw_response, locales) self.city = geoip2.records.City(locales, **raw_response.get("city", {})) self.location = geoip2.records.Location(**raw_response.get("location", {})) @@ -303,13 +308,13 @@ class SimpleModel(SimpleEquality): __metaclass__ = ABCMeta - 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") - def __repr__(self): + def __repr__(self) -> str: # pylint: disable=no-member return "{module}.{class_name}({data})".format( module=self.__module__, @@ -318,7 +323,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 @@ -391,7 +396,7 @@ class AnonymousIP(SimpleModel): :type: ipaddress.IPv4Network or ipaddress.IPv6Network """ - def __init__(self, raw): + def __init__(self, raw: Dict[str, Union[bool, str, int]]) -> None: super(AnonymousIP, self).__init__(raw) self.is_anonymous = raw.get("is_anonymous", False) self.is_anonymous_vpn = raw.get("is_anonymous_vpn", False) @@ -434,7 +439,7 @@ class ASN(SimpleModel): """ # pylint:disable=too-many-arguments - def __init__(self, raw): + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: super(ASN, self).__init__(raw) self.autonomous_system_number = raw.get("autonomous_system_number") self.autonomous_system_organization = raw.get("autonomous_system_organization") @@ -473,7 +478,7 @@ class ConnectionType(SimpleModel): :type: ipaddress.IPv4Network or ipaddress.IPv6Network """ - def __init__(self, raw): + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: super(ConnectionType, self).__init__(raw) self.connection_type = raw.get("connection_type") @@ -505,7 +510,7 @@ class Domain(SimpleModel): """ - def __init__(self, raw): + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: super(Domain, self).__init__(raw) self.domain = raw.get("domain") @@ -556,7 +561,7 @@ class ISP(ASN): """ # pylint:disable=too-many-arguments - def __init__(self, raw): + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: super(ISP, self).__init__(raw) self.isp = raw.get("isp") self.organization = raw.get("organization") diff --git a/geoip2/records.py b/geoip2/records.py index 5c4a20d1..05b04313 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -10,6 +10,7 @@ # pylint:disable=R0903 from abc import ABCMeta +from typing import Dict, List, Optional, Type, Union from geoip2.mixins import SimpleEquality @@ -19,7 +20,7 @@ class Record(SimpleEquality): __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 @@ -30,8 +31,13 @@ class PlaceRecord(Record): """All records with :py:attr:`names` subclass :py:class:`PlaceRecord`.""" __metaclass__ = ABCMeta + names: Dict[str, 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 @@ -40,7 +46,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) @@ -85,7 +91,14 @@ class City(PlaceRecord): """ - def __init__(self, locales=None, confidence=None, geoname_id=None, names=None, **_): + 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) @@ -129,7 +142,14 @@ class Continent(PlaceRecord): """ - def __init__(self, locales=None, code=None, geoname_id=None, names=None, **_): + 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) @@ -189,14 +209,14 @@ class Country(PlaceRecord): 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 @@ -268,16 +288,16 @@ class RepresentedCountry(Country): 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__( locales, confidence, geoname_id, is_in_european_union, iso_code, names @@ -353,17 +373,17 @@ class Location(Record): 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, + postal_code: None = None, + postal_confidence: None = None, + time_zone: Optional[str] = None, **_ - ): + ) -> None: self.average_income = average_income self.accuracy_radius = accuracy_radius self.latitude = latitude @@ -389,7 +409,7 @@ class MaxMind(Record): """ - def __init__(self, queries_remaining=None, **_): + def __init__(self, queries_remaining: Optional[int] = None, **_) -> None: self.queries_remaining = queries_remaining @@ -421,7 +441,9 @@ class Postal(Record): """ - def __init__(self, code=None, confidence=None, **_): + def __init__( + self, code: Optional[str] = None, confidence: Optional[int] = None, **_ + ) -> None: self.code = code self.confidence = confidence @@ -476,13 +498,13 @@ class Subdivision(PlaceRecord): 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 @@ -501,17 +523,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(cls, Subdivisions).__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__() @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, @@ -740,28 +766,28 @@ class Traits(Record): 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 @@ -786,7 +812,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)): @@ -800,4 +826,4 @@ def network(self): network = "{}/{}".format(ip_address, prefix_len) 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 9da32704..0db10ca9 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -26,13 +26,15 @@ """ import ipaddress +from typing import cast, Dict, List, Optional, Type, Union + import requests +from requests.models import Response from requests.utils import default_user_agent import geoip2 import geoip2.models - -from .errors import ( +from geoip2.errors import ( AddressNotFoundError, AuthenticationError, GeoIP2Error, @@ -41,6 +43,8 @@ OutOfQueriesError, PermissionRequiredError, ) +from geoip2.models import City, Country, Insights +from geoip2.types import IPAddress class Client(object): @@ -88,12 +92,12 @@ class Client(object): def __init__( self, - account_id=None, - license_key=None, - host="geoip.maxmind.com", - locales=None, - timeout=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: @@ -109,7 +113,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 @@ -119,9 +123,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 @@ -131,9 +135,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 @@ -143,12 +149,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(ipaddress.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), @@ -160,13 +173,24 @@ 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): + def _user_agent(self) -> str: return "GeoIP2 Python Client v%s (%s)" % ( geoip2.__version__, default_user_agent(), ) - def _handle_success(self, response, uri): + def _handle_success( + self, response: Response, uri: str + ) -> Dict[ + str, + Union[ + Dict[str, Union[str, int, Dict[str, str]]], + Dict[str, int], + Dict[str, Union[int, bool, str, Dict[str, str]]], + Dict[str, Union[int, float, str]], + Dict[str, str], + ], + ]: try: return response.json() except ValueError as ex: @@ -178,7 +202,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: @@ -187,7 +211,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(), @@ -197,7 +223,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, ) @@ -221,7 +247,15 @@ def _exception_for_4xx_status(self, response, status, uri): uri, ) - def _exception_for_web_service_error(self, message, code, status, uri): + def _exception_for_web_service_error( + self, 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 ( @@ -240,14 +274,14 @@ 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): + def _exception_for_5xx_status(self, 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): + def _exception_for_non_200_status(self, status: int, uri: str) -> HTTPError: return HTTPError( "Received a very surprising HTTP status " "(%(status)i) for %(uri)s" % locals(), diff --git a/tests/database_test.py b/tests/database_test.py index b0d325b5..6615fb9b 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -3,15 +3,13 @@ from __future__ import unicode_literals +import ipaddress import sys sys.path.append("..") import geoip2.database import maxminddb -import ipaddress - -from ipaddress import IPv4Network, IPv6Network try: import maxminddb.extension @@ -29,7 +27,7 @@ class BaseTestReader(object): - def test_language_list(self): + 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 +37,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 +46,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 +55,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 +63,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 +79,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 +100,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 +115,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 +139,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 +150,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 +170,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 +189,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 +211,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: diff --git a/tests/models_test.py b/tests/models_test.py index a6e63baa..ba06b7fa 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import sys +from typing import Dict sys.path.append("..") @@ -20,7 +21,7 @@ class TestModels(unittest.TestCase): - def test_insights_full(self): + def test_insights_full(self) -> None: raw = { "city": { "confidence": 76, @@ -199,7 +200,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 +239,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 +335,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 +351,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 +385,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 +398,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 +407,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 +416,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 +425,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 f6e836aa..3f1438b2 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -4,11 +4,12 @@ import copy import ipaddress import sys +from typing import cast, Dict sys.path.append("..") import geoip2 -import requests_mock +import requests_mock # type: ignore from geoip2.errors import ( AddressNotFoundError, AuthenticationError, @@ -54,7 +55,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 @@ -337,10 +338,10 @@ def test_insights_ok(self, mock): self.assertEqual(insights.traits.user_count, 2, "user_count is 2") def test_named_constructor_args(self): - id = "47" + id = 47 key = "1234567890ab" client = Client(account_id=id, license_key=key) - self.assertEqual(client._account_id, id) + self.assertEqual(client._account_id, str(id)) self.assertEqual(client._license_key, key) def test_missing_constructor_args(self): From 88c6691b25caf71797cc3ef711579a6cbfe71dcb Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 13:46:27 -0700 Subject: [PATCH 06/18] Add missing documentation --- geoip2/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/geoip2/models.py b/geoip2/models.py index 9f7a47cb..3461b90e 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -126,12 +126,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 From bdcfd42576f06840b767b1323d5301ad2676b5b7 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 14:00:52 -0700 Subject: [PATCH 07/18] Remove invalid attributes Not sure why these are there. I think an early prototype of GeoIP2 City has the postal values in location. Perhaps they are from that. --- geoip2/records.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/geoip2/records.py b/geoip2/records.py index 05b04313..f8893e69 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -379,8 +379,6 @@ def __init__( longitude: Optional[float] = None, metro_code: Optional[int] = None, population_density: Optional[int] = None, - postal_code: None = None, - postal_confidence: None = None, time_zone: Optional[str] = None, **_ ) -> None: @@ -390,8 +388,6 @@ def __init__( 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 From 836b2e7956de4c6398c6453d4409cbb5cec22fbd Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 14:01:55 -0700 Subject: [PATCH 08/18] Add types to records/models --- geoip2/models.py | 55 ++++++++++++++++++++++++++++++++++++++--------- geoip2/records.py | 52 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/geoip2/models.py b/geoip2/models.py index 3461b90e..4ee2fd73 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -13,7 +13,7 @@ # pylint: disable=too-many-instance-attributes,too-few-public-methods import ipaddress from abc import ABCMeta -from typing import Any, Dict, List, Optional, Union +from typing import Any, cast, Dict, List, Optional, Union import geoip2.records from geoip2.mixins import SimpleEquality @@ -67,6 +67,13 @@ class Country(SimpleEquality): """ + 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: @@ -171,6 +178,11 @@ class City(Country): """ + 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: @@ -316,11 +328,14 @@ class SimpleModel(SimpleEquality): __metaclass__ = ABCMeta + raw: Dict[str, Union[bool, str, int]] + ip_address: str + 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) -> str: # pylint: disable=no-member @@ -404,8 +419,14 @@ class AnonymousIP(SimpleModel): :type: ipaddress.IPv4Network or ipaddress.IPv6Network """ - def __init__(self, raw: Dict[str, Union[bool, str, int]]) -> None: - 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(AnonymousIP, self).__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) @@ -446,11 +467,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: Dict[str, Union[str, int]]) -> None: super(ASN, self).__init__(raw) - self.autonomous_system_number = raw.get("autonomous_system_number") - self.autonomous_system_organization = raw.get("autonomous_system_organization") + 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): @@ -486,9 +514,11 @@ class ConnectionType(SimpleModel): :type: ipaddress.IPv4Network or ipaddress.IPv6Network """ + connection_type: Optional[str] + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: super(ConnectionType, self).__init__(raw) - self.connection_type = raw.get("connection_type") + self.connection_type = cast(Optional[str], raw.get("connection_type")) class Domain(SimpleModel): @@ -518,9 +548,11 @@ class Domain(SimpleModel): """ + domain: Optional[str] + def __init__(self, raw: Dict[str, Union[str, int]]) -> None: super(Domain, self).__init__(raw) - self.domain = raw.get("domain") + self.domain = cast(Optional[str], raw.get("domain")) class ISP(ASN): @@ -568,8 +600,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: Dict[str, Union[str, int]]) -> None: super(ISP, self).__init__(raw) - self.isp = raw.get("isp") - self.organization = raw.get("organization") + 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 f8893e69..1a501cd9 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -32,6 +32,7 @@ class PlaceRecord(Record): __metaclass__ = ABCMeta names: Dict[str, str] + _locales: List[str] def __init__( self, @@ -91,6 +92,9 @@ class City(PlaceRecord): """ + confidence: Optional[int] + geoname_id: Optional[int] + def __init__( self, locales: Optional[List[str]] = None, @@ -142,6 +146,9 @@ class Continent(PlaceRecord): """ + code: Optional[str] + geoname_id: Optional[int] + def __init__( self, locales: Optional[List[str]] = None, @@ -207,6 +214,11 @@ class Country(PlaceRecord): """ + confidence: Optional[int] + geoname_id: Optional[int] + is_in_european_union: bool + iso_code: Optional[str] + def __init__( self, locales: Optional[List[str]] = None, @@ -286,6 +298,8 @@ class RepresentedCountry(Country): """ + type: Optional[str] + def __init__( self, locales: Optional[List[str]] = None, @@ -371,6 +385,14 @@ 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: Optional[int] = None, @@ -405,6 +427,8 @@ class MaxMind(Record): """ + queries_remaining: Optional[int] + def __init__(self, queries_remaining: Optional[int] = None, **_) -> None: self.queries_remaining = queries_remaining @@ -437,6 +461,9 @@ class Postal(Record): """ + code: Optional[str] + confidence: Optional[int] + def __init__( self, code: Optional[str] = None, confidence: Optional[int] = None, **_ ) -> None: @@ -492,6 +519,10 @@ class Subdivision(PlaceRecord): """ + confidence: Optional[int] + geoname_id: Optional[int] + iso_code: Optional[str] + def __init__( self, locales: Optional[List[str]] = None, @@ -760,6 +791,27 @@ 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: Optional[int] = None, From a1a6c6c92a598378fae27c3cfb4e4d7fa5c6629f Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 14:06:38 -0700 Subject: [PATCH 09/18] Clean up test inheritance --- tests/database_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/database_test.py b/tests/database_test.py index 6615fb9b..4bbc8657 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -26,7 +26,7 @@ unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches -class BaseTestReader(object): +class BaseTestReader(unittest.TestCase): def test_language_list(self) -> None: reader = geoip2.database.Reader( "tests/data/test-data/GeoIP2-Country-Test.mmdb", @@ -220,19 +220,19 @@ def test_context_manager(self) -> None: @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 @@ -240,5 +240,5 @@ class TestFDReader(unittest.TestCase): mode = geoip2.database.MODE_FD -class TestAutoReader(BaseTestReader, unittest.TestCase): +class TestAutoReader(BaseTestReader): mode = geoip2.database.MODE_AUTO From 5157fa5826fc6642c0926c76d1227d0e0d42f4b5 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 14:12:04 -0700 Subject: [PATCH 10/18] Test with mypy via Travis --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f43fc204..ac9b5a7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,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: From 86111da1c8080199012f6354569e42684370da1d Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 14:14:07 -0700 Subject: [PATCH 11/18] Remove Python 2 test code --- tests/database_test.py | 12 ++---------- tests/models_test.py | 10 +--------- tests/webservice_test.py | 10 +--------- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/tests/database_test.py b/tests/database_test.py index 4bbc8657..0817bb96 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -5,6 +5,7 @@ import ipaddress import sys +import unittest sys.path.append("..") @@ -14,16 +15,7 @@ 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(unittest.TestCase): diff --git a/tests/models_test.py b/tests/models_test.py index ba06b7fa..ed43215d 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -5,20 +5,12 @@ 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) -> None: diff --git a/tests/webservice_test.py b/tests/webservice_test.py index 3f1438b2..d2600af0 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -5,6 +5,7 @@ import ipaddress import sys from typing import cast, Dict +import unittest sys.path.append("..") @@ -21,15 +22,6 @@ ) from geoip2.webservice import Client -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): def setUp(self): From d14fb690326c5d69d2fe1c931d6e5335911621db Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 14:18:46 -0700 Subject: [PATCH 12/18] Remove Python 2 compat super calls --- geoip2/errors.py | 2 +- geoip2/models.py | 12 ++++++------ geoip2/records.py | 14 +++++++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/geoip2/errors.py b/geoip2/errors.py index 645b24e7..d3d40d9b 100644 --- a/geoip2/errors.py +++ b/geoip2/errors.py @@ -38,7 +38,7 @@ class HTTPError(GeoIP2Error): def __init__( self, message: str, http_status: Optional[int] = None, uri: Optional[str] = None ) -> None: - super(HTTPError, self).__init__(message) + super().__init__(message) self.http_status = http_status self.uri = uri diff --git a/geoip2/models.py b/geoip2/models.py index 4ee2fd73..fc6ab72b 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -186,7 +186,7 @@ class City(Country): def __init__( self, raw_response: Dict[str, Any], locales: Optional[List[str]] = None ) -> None: - super(City, self).__init__(raw_response, locales) + 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", {})) @@ -426,7 +426,7 @@ class AnonymousIP(SimpleModel): is_tor_exit_node: bool def __init__(self, raw: Dict[str, bool]) -> None: - super(AnonymousIP, self).__init__(raw) # type: ignore + 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) @@ -472,7 +472,7 @@ class ASN(SimpleModel): # pylint:disable=too-many-arguments def __init__(self, raw: Dict[str, Union[str, int]]) -> None: - super(ASN, self).__init__(raw) + super().__init__(raw) self.autonomous_system_number = cast( Optional[int], raw.get("autonomous_system_number") ) @@ -517,7 +517,7 @@ class ConnectionType(SimpleModel): connection_type: Optional[str] def __init__(self, raw: Dict[str, Union[str, int]]) -> None: - super(ConnectionType, self).__init__(raw) + super().__init__(raw) self.connection_type = cast(Optional[str], raw.get("connection_type")) @@ -551,7 +551,7 @@ class Domain(SimpleModel): domain: Optional[str] def __init__(self, raw: Dict[str, Union[str, int]]) -> None: - super(Domain, self).__init__(raw) + super().__init__(raw) self.domain = cast(Optional[str], raw.get("domain")) @@ -605,6 +605,6 @@ class ISP(ASN): # pylint:disable=too-many-arguments def __init__(self, raw: Dict[str, Union[str, int]]) -> None: - super(ISP, self).__init__(raw) + 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 1a501cd9..597cb0d0 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -105,7 +105,7 @@ def __init__( ) -> None: self.confidence = confidence self.geoname_id = geoname_id - super(City, self).__init__(locales, names) + super().__init__(locales, names) class Continent(PlaceRecord): @@ -159,7 +159,7 @@ def __init__( ) -> None: self.code = code self.geoname_id = geoname_id - super(Continent, self).__init__(locales, names) + super().__init__(locales, names) class Country(PlaceRecord): @@ -233,7 +233,7 @@ def __init__( 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): @@ -313,7 +313,7 @@ def __init__( **_ ) -> None: self.type = type - super(RepresentedCountry, self).__init__( + super().__init__( locales, confidence, geoname_id, is_in_european_union, iso_code, names ) @@ -535,7 +535,7 @@ def __init__( 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): @@ -554,14 +554,14 @@ def __new__( cls: Type["Subdivisions"], locales: Optional[List[str]], *subdivisions ) -> "Subdivisions": subobjs = tuple(Subdivision(locales, **x) for x in subdivisions) - obj = super(cls, Subdivisions).__new__(cls, subobjs) # type: ignore + obj = super().__new__(cls, subobjs) # type: ignore return obj 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) -> Subdivision: From 5f4bfb10729e423542287d8e68b5e9bed583a892 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 14:35:17 -0700 Subject: [PATCH 13/18] Add additional items to the 4.0.0 change log --- HISTORY.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index ac4f072b..7ca4216a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,6 +8,12 @@ History * 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) ++++++++++++++++++ From a46d5553f6d7c0321a093b1954591bc12e642ab4 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 20:06:32 -0700 Subject: [PATCH 14/18] Switch to Python 3 metaclasses --- geoip2/mixins.py | 4 +--- geoip2/models.py | 4 +--- geoip2/records.py | 7 ++----- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/geoip2/mixins.py b/geoip2/mixins.py index 7134bde0..2209b7bd 100644 --- a/geoip2/mixins.py +++ b/geoip2/mixins.py @@ -4,11 +4,9 @@ from typing import Any -class SimpleEquality(object): +class SimpleEquality(metaclass=ABCMeta): """Naive __dict__ equality mixin""" - __metaclass__ = ABCMeta - def __eq__(self, other: Any) -> bool: return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ diff --git a/geoip2/models.py b/geoip2/models.py index fc6ab72b..ab9a6953 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -323,11 +323,9 @@ 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 diff --git a/geoip2/records.py b/geoip2/records.py index 597cb0d0..ce33676a 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -15,11 +15,9 @@ 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) -> str: args = ", ".join("%s=%r" % x for x in self.__dict__.items()) return "{module}.{class_name}({data})".format( @@ -27,10 +25,9 @@ def __repr__(self) -> str: ) -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] From 319a60f2126a1c5175341ae285fb68d0a8307063 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 20:07:54 -0700 Subject: [PATCH 15/18] Do not subclass object explicitly --- geoip2/database.py | 2 +- geoip2/webservice.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/geoip2/database.py b/geoip2/database.py index 6668f390..10c1bdf0 100644 --- a/geoip2/database.py +++ b/geoip2/database.py @@ -35,7 +35,7 @@ ) -class Reader(object): +class Reader: """GeoIP2 database Reader object. Instances of this class provide a reader for the GeoIP2 database format. diff --git a/geoip2/webservice.py b/geoip2/webservice.py index 0db10ca9..94a70dd2 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -47,7 +47,7 @@ from geoip2.types import IPAddress -class Client(object): +class Client: """Creates a new client object. It accepts the following required arguments: From 0a56acf260ae74529db5f10ad766fa66d4f6d43c Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2020 20:15:15 -0700 Subject: [PATCH 16/18] Convert methods to static methods And switch a complicated yet mislead type signature to Any. --- geoip2/webservice.py | 28 +++++++++++----------------- pylintrc | 2 +- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/geoip2/webservice.py b/geoip2/webservice.py index 94a70dd2..789f33e6 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -26,7 +26,7 @@ """ import ipaddress -from typing import cast, Dict, List, Optional, Type, Union +from typing import Any, cast, List, Optional, Type, Union import requests from requests.models import Response @@ -173,24 +173,15 @@ def _response_for( body = self._handle_success(response, uri) return model_class(body, locales=self._locales) - def _user_agent(self) -> str: + @staticmethod + def _user_agent() -> str: return "GeoIP2 Python Client v%s (%s)" % ( geoip2.__version__, default_user_agent(), ) - def _handle_success( - self, response: Response, uri: str - ) -> Dict[ - str, - Union[ - Dict[str, Union[str, int, Dict[str, str]]], - Dict[str, int], - Dict[str, Union[int, bool, str, Dict[str, str]]], - Dict[str, Union[int, float, str]], - Dict[str, str], - ], - ]: + @staticmethod + def _handle_success(response: Response, uri: str) -> Any: try: return response.json() except ValueError as ex: @@ -247,8 +238,9 @@ def _exception_for_4xx_status( uri, ) + @staticmethod def _exception_for_web_service_error( - self, message: str, code: str, status: int, uri: str + message: str, code: str, status: int, uri: str ) -> Union[ AuthenticationError, AddressNotFoundError, @@ -274,14 +266,16 @@ def _exception_for_web_service_error( return InvalidRequestError(message, code, status, uri) - def _exception_for_5xx_status(self, status: int, uri: str) -> HTTPError: + @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: int, uri: str) -> HTTPError: + @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] From d4780d8306007e7a65b84bcfbc9d9b60517aa116 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 13 Jul 2020 13:52:04 -0700 Subject: [PATCH 17/18] Bump version in module This is mostly so I can install from Git for minfraud and get 4.0.0. --- geoip2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geoip2/__init__.py b/geoip2/__init__.py index 7a3cecc3..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-2020 Maxmind, Inc." From 47227d44f4e92be2be2697d1eb1585a8b7aadaed Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 13 Jul 2020 13:53:10 -0700 Subject: [PATCH 18/18] Add missing documentation for timeout --- geoip2/webservice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/geoip2/webservice.py b/geoip2/webservice.py index 789f33e6..54d49930 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -87,6 +87,8 @@ class Client: * 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. """