diff --git a/.gitignore b/.gitignore index ebd19299..c1ac9a6c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,10 +13,8 @@ geoip2.egg-info/ MANIFEST .mypy_cache/ *.pyc -pylint.txt .pyre .pytype *.swp .tox -violations.pyflakes.txt -/venv \ No newline at end of file +/venv diff --git a/examples/benchmark.py b/examples/benchmark.py old mode 100644 new mode 100755 index b9fbc7c2..e8e478ea --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -1,5 +1,5 @@ #!/usr/bin/python - +"""Simple benchmarking script.""" import argparse import contextlib @@ -9,6 +9,7 @@ import timeit import geoip2.database +import geoip2.errors parser = argparse.ArgumentParser(description="Benchmark maxminddb.") parser.add_argument("--count", default=250000, type=int, help="number of lookups") @@ -21,6 +22,7 @@ def lookup_ip_address() -> None: + """Look up IP address.""" ip = socket.inet_ntoa(struct.pack("!L", random.getrandbits(32))) with contextlib.suppress(geoip2.errors.AddressNotFoundError): reader.city(str(ip)) @@ -32,4 +34,4 @@ def lookup_ip_address() -> None: number=args.count, ) -print(args.count / elapsed, "lookups per second") +print(args.count / elapsed, "lookups per second") # noqa: T201 diff --git a/geoip2/__init__.py b/geoip2/__init__.py index 7cd957d1..8b92c9b9 100644 --- a/geoip2/__init__.py +++ b/geoip2/__init__.py @@ -1,4 +1,4 @@ -# pylint:disable=C0111 +"""geoip2 client library.""" __title__ = "geoip2" __version__ = "5.1.0" diff --git a/geoip2/_internal.py b/geoip2/_internal.py index 0918feaa..fc083b77 100644 --- a/geoip2/_internal.py +++ b/geoip2/_internal.py @@ -1,20 +1,18 @@ """Internal utilities.""" -# pylint: disable=too-few-public-methods from abc import ABCMeta -class Model(metaclass=ABCMeta): +class Model(metaclass=ABCMeta): # noqa: B024 """Shared methods for MaxMind model classes.""" def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and self.to_dict() == other.to_dict() - def __ne__(self, other) -> bool: + def __ne__(self, other: object) -> bool: return not self.__eq__(other) - # pylint: disable=too-many-branches - def to_dict(self) -> dict: + def to_dict(self) -> dict: # noqa: C901, PLR0912 """Return a dict of the object suitable for serialization.""" result = {} for key, value in self.__dict__.items(): @@ -42,7 +40,6 @@ def to_dict(self) -> dict: result[key] = value # network and ip_address are properties for performance reasons - # pylint: disable=no-member if hasattr(self, "ip_address") and self.ip_address is not None: result["ip_address"] = str(self.ip_address) if hasattr(self, "network") and self.network is not None: diff --git a/geoip2/database.py b/geoip2/database.py index b36a0e41..2c10538b 100644 --- a/geoip2/database.py +++ b/geoip2/database.py @@ -1,9 +1,9 @@ """The database reader for MaxMind MMDB files.""" +from __future__ import annotations + import inspect -import os -from collections.abc import Sequence -from typing import IO, Any, AnyStr, Optional, Union, cast +from typing import IO, TYPE_CHECKING, AnyStr, cast import maxminddb from maxminddb import ( @@ -13,23 +13,31 @@ MODE_MEMORY, MODE_MMAP, MODE_MMAP_EXT, + InvalidDatabaseError, ) import geoip2 import geoip2.errors import geoip2.models -from geoip2.models import ( - ASN, - ISP, - AnonymousIP, - AnonymousPlus, - City, - ConnectionType, - Country, - Domain, - Enterprise, -) -from geoip2.types import IPAddress + +if TYPE_CHECKING: + import os + from collections.abc import Sequence + + from typing_extensions import Self + + from geoip2.models import ( + ASN, + ISP, + AnonymousIP, + AnonymousPlus, + City, + ConnectionType, + Country, + Domain, + Enterprise, + ) + from geoip2.types import IPAddress __all__ = [ "MODE_AUTO", @@ -67,8 +75,8 @@ class Reader: def __init__( self, - fileish: Union[AnyStr, int, os.PathLike, IO], - locales: Optional[Sequence[str]] = None, + fileish: AnyStr | int | os.PathLike | IO, + locales: Sequence[str] | None = None, mode: int = MODE_AUTO, ) -> None: """Create GeoIP2 Reader. @@ -117,10 +125,10 @@ def __init__( self._db_type = self._db_reader.metadata().database_type self._locales = locales - def __enter__(self) -> "Reader": + def __enter__(self) -> Self: return self - def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None: + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: self.close() def country(self, ip_address: IPAddress) -> Country: @@ -132,7 +140,7 @@ def country(self, ip_address: IPAddress) -> Country: """ return cast( - Country, + "Country", self._model_for(geoip2.models.Country, "Country", ip_address), ) @@ -144,7 +152,7 @@ def city(self, ip_address: IPAddress) -> City: :returns: :py:class:`geoip2.models.City` object """ - return cast(City, 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: IPAddress) -> AnonymousIP: """Get the AnonymousIP object for the IP address. @@ -155,7 +163,7 @@ def anonymous_ip(self, ip_address: IPAddress) -> AnonymousIP: """ return cast( - AnonymousIP, + "AnonymousIP", self._flat_model_for( geoip2.models.AnonymousIP, "GeoIP2-Anonymous-IP", @@ -172,7 +180,7 @@ def anonymous_plus(self, ip_address: IPAddress) -> AnonymousPlus: """ return cast( - AnonymousPlus, + "AnonymousPlus", self._flat_model_for( geoip2.models.AnonymousPlus, "GeoIP-Anonymous-Plus", @@ -189,7 +197,7 @@ def asn(self, ip_address: IPAddress) -> ASN: """ return cast( - ASN, + "ASN", self._flat_model_for(geoip2.models.ASN, "GeoLite2-ASN", ip_address), ) @@ -202,7 +210,7 @@ def connection_type(self, ip_address: IPAddress) -> ConnectionType: """ return cast( - ConnectionType, + "ConnectionType", self._flat_model_for( geoip2.models.ConnectionType, "GeoIP2-Connection-Type", @@ -219,7 +227,7 @@ def domain(self, ip_address: IPAddress) -> Domain: """ return cast( - Domain, + "Domain", self._flat_model_for(geoip2.models.Domain, "GeoIP2-Domain", ip_address), ) @@ -232,7 +240,7 @@ def enterprise(self, ip_address: IPAddress) -> Enterprise: """ return cast( - Enterprise, + "Enterprise", self._model_for(geoip2.models.Enterprise, "Enterprise", ip_address), ) @@ -245,31 +253,38 @@ def isp(self, ip_address: IPAddress) -> ISP: """ return cast( - ISP, + "ISP", self._flat_model_for(geoip2.models.ISP, "GeoIP2-ISP", ip_address), ) - def _get(self, database_type: str, ip_address: IPAddress) -> Any: + def _get(self, database_type: str, ip_address: IPAddress) -> tuple[dict, int]: if database_type not in self._db_type: caller = inspect.stack()[2][3] + msg = ( + f"The {caller} method cannot be used with the {self._db_type} database" + ) raise TypeError( - f"The {caller} method cannot be used with the {self._db_type} database", + msg, ) (record, prefix_len) = self._db_reader.get_with_prefix_len(ip_address) if record is None: + msg = f"The address {ip_address} is not in the database." raise geoip2.errors.AddressNotFoundError( - f"The address {ip_address} is not in the database.", + msg, str(ip_address), prefix_len, ) + if not isinstance(record, dict): + msg = f"Expected record to be a dict but was f{type(record)}" + raise InvalidDatabaseError(msg) return record, prefix_len def _model_for( self, - model_class: Union[type[Country], type[Enterprise], type[City]], + model_class: type[City | Country | Enterprise], types: str, ip_address: IPAddress, - ) -> Union[Country, Enterprise, City]: + ) -> City | Country | Enterprise: (record, prefix_len) = self._get(types, ip_address) return model_class( self._locales, @@ -280,28 +295,22 @@ def _model_for( def _flat_model_for( self, - model_class: Union[ - type[Domain], - type[ISP], - type[ConnectionType], - type[ASN], - type[AnonymousIP], - ], + model_class: type[Domain | ISP | ConnectionType | ASN | AnonymousIP], types: str, ip_address: IPAddress, - ) -> Union[ConnectionType, ISP, AnonymousIP, Domain, ASN]: + ) -> ConnectionType | ISP | AnonymousIP | Domain | ASN: (record, prefix_len) = self._get(types, ip_address) return model_class(ip_address, prefix_len=prefix_len, **record) def metadata( self, ) -> maxminddb.reader.Metadata: - """The metadata for the open database. + """Get the metadata for the open database. :returns: :py:class:`maxminddb.reader.Metadata` object """ return self._db_reader.metadata() def close(self) -> None: - """Closes the GeoIP2 database.""" + """Close the GeoIP2 database.""" self._db_reader.close() diff --git a/geoip2/errors.py b/geoip2/errors.py index 3e2f9bd4..893d9e3f 100644 --- a/geoip2/errors.py +++ b/geoip2/errors.py @@ -1,7 +1,8 @@ """Typed errors thrown by this library.""" +from __future__ import annotations + import ipaddress -from typing import Optional, Union class GeoIP2Error(RuntimeError): @@ -16,24 +17,33 @@ class GeoIP2Error(RuntimeError): class AddressNotFoundError(GeoIP2Error): """The address you were looking up was not found.""" - ip_address: Optional[str] + ip_address: str | None """The IP address used in the lookup. This is only available for database lookups. """ - _prefix_len: Optional[int] + _prefix_len: int | None def __init__( self, message: str, - ip_address: Optional[str] = None, - prefix_len: Optional[int] = None, + ip_address: str | None = None, + prefix_len: int | None = None, ) -> None: + """Initialize self. + + Arguments: + message: A message describing the error. + ip_address: The IP address that was not found. + prefix_len: The prefix length for the network associated with + the IP address. + + """ super().__init__(message) self.ip_address = ip_address self._prefix_len = prefix_len @property - def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: + def network(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None: """The network associated with the error. In particular, this is the largest network where no address would be @@ -42,7 +52,8 @@ def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network if self.ip_address is None or self._prefix_len is None: return None return ipaddress.ip_network( - f"{self.ip_address}/{self._prefix_len}", strict=False + f"{self.ip_address}/{self._prefix_len}", + strict=False, ) @@ -58,20 +69,29 @@ class HTTPError(GeoIP2Error): """ - http_status: Optional[int] + http_status: int | None """The HTTP status code returned""" - uri: Optional[str] + uri: str | None """The URI queried""" - decoded_content: Optional[str] + decoded_content: str | None """The decoded response content""" def __init__( self, message: str, - http_status: Optional[int] = None, - uri: Optional[str] = None, - decoded_content: Optional[str] = None, + http_status: int | None = None, + uri: str | None = None, + decoded_content: str | None = None, ) -> None: + """Initialize self. + + Arguments: + message: A descriptive message for the error. + http_status: The HTTP status code associated with the error, if any. + uri: The URI that was being accessed when the error occurred. + decoded_content: The decoded HTTP response body, if available. + + """ super().__init__(message) self.http_status = http_status self.uri = uri diff --git a/geoip2/models.py b/geoip2/models.py index e045c8da..b41280b8 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -5,17 +5,21 @@ https://dev.maxmind.com/geoip/docs/web-services?lang=en for more details. """ -# pylint: disable=too-many-instance-attributes,too-few-public-methods,too-many-arguments +from __future__ import annotations + import datetime import ipaddress from abc import ABCMeta -from collections.abc import Sequence from ipaddress import IPv4Address, IPv6Address -from typing import Optional, Union +from typing import TYPE_CHECKING import geoip2.records from geoip2._internal import Model -from geoip2.types import IPAddress + +if TYPE_CHECKING: + from collections.abc import Sequence + + from geoip2.types import IPAddress class Country(Model): @@ -49,16 +53,16 @@ class Country(Model): def __init__( self, - locales: Optional[Sequence[str]], + locales: Sequence[str] | None, *, - continent: Optional[dict] = None, - country: Optional[dict] = None, - ip_address: Optional[IPAddress] = None, - maxmind: Optional[dict] = None, - prefix_len: Optional[int] = None, - registered_country: Optional[dict] = None, - represented_country: Optional[dict] = None, - traits: Optional[dict] = None, + continent: dict | None = None, + country: dict | None = None, + ip_address: IPAddress | None = None, + maxmind: dict | None = None, + prefix_len: int | None = None, + registered_country: dict | None = None, + represented_country: dict | None = None, + traits: dict | None = None, **_, ) -> None: self._locales = locales @@ -109,20 +113,20 @@ class City(Country): def __init__( self, - locales: Optional[Sequence[str]], + locales: Sequence[str] | None, *, - city: Optional[dict] = None, - continent: Optional[dict] = None, - country: Optional[dict] = None, - location: Optional[dict] = None, - ip_address: Optional[IPAddress] = None, - maxmind: Optional[dict] = None, - postal: Optional[dict] = None, - prefix_len: Optional[int] = None, - registered_country: Optional[dict] = None, - represented_country: Optional[dict] = None, - subdivisions: Optional[list[dict]] = None, - traits: Optional[dict] = None, + city: dict | None = None, + continent: dict | None = None, + country: dict | None = None, + location: dict | None = None, + ip_address: IPAddress | None = None, + maxmind: dict | None = None, + postal: dict | None = None, + prefix_len: int | None = None, + registered_country: dict | None = None, + represented_country: dict | None = None, + subdivisions: list[dict] | None = None, + traits: dict | None = None, **_, ) -> None: super().__init__( @@ -154,14 +158,14 @@ class SimpleModel(Model, metaclass=ABCMeta): """Provides basic methods for non-location models.""" _ip_address: IPAddress - _network: Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]] - _prefix_len: Optional[int] + _network: ipaddress.IPv4Network | ipaddress.IPv6Network | None + _prefix_len: int | None def __init__( self, ip_address: IPAddress, - network: Optional[str], - prefix_len: Optional[int], + network: str | None, + prefix_len: int | None, ) -> None: if network: self._network = ipaddress.ip_network(network, strict=False) @@ -186,14 +190,14 @@ def __repr__(self) -> str: ) @property - def ip_address(self) -> Union[IPv4Address, IPv6Address]: + def ip_address(self) -> IPv4Address | IPv6Address: """The IP address for the record.""" if not isinstance(self._ip_address, (IPv4Address, IPv6Address)): self._ip_address = ipaddress.ip_address(self._ip_address) return self._ip_address @property - def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: + def network(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None: """The network associated with the record. In particular, this is the largest network where all of the fields besides @@ -254,8 +258,8 @@ def __init__( is_public_proxy: bool = False, is_residential_proxy: bool = False, is_tor_exit_node: bool = False, - network: Optional[str] = None, - prefix_len: Optional[int] = None, + network: str | None = None, + prefix_len: int | None = None, **_, ) -> None: super().__init__(ip_address, network, prefix_len) @@ -270,17 +274,17 @@ def __init__( class AnonymousPlus(AnonymousIP): """Model class for the GeoIP Anonymous Plus.""" - anonymizer_confidence: Optional[int] + anonymizer_confidence: int | None """A score ranging from 1 to 99 that is our percent confidence that the network is currently part of an actively used VPN service. """ - network_last_seen: Optional[datetime.date] + network_last_seen: datetime.date | None """The last day that the network was sighted in our analysis of anonymized networks. """ - provider_name: Optional[str] + provider_name: str | None """The name of the VPN provider (e.g., NordVPN, SurfShark, etc.) associated with the network. """ @@ -289,17 +293,17 @@ def __init__( self, ip_address: IPAddress, *, - anonymizer_confidence: Optional[int] = None, + anonymizer_confidence: int | None = None, is_anonymous: bool = False, is_anonymous_vpn: bool = False, is_hosting_provider: bool = False, is_public_proxy: bool = False, is_residential_proxy: bool = False, is_tor_exit_node: bool = False, - network: Optional[str] = None, - network_last_seen: Optional[str] = None, - prefix_len: Optional[int] = None, - provider_name: Optional[str] = None, + network: str | None = None, + network_last_seen: str | None = None, + prefix_len: int | None = None, + provider_name: str | None = None, **_, ) -> None: super().__init__( @@ -322,23 +326,22 @@ def __init__( class ASN(SimpleModel): """Model class for the GeoLite2 ASN.""" - autonomous_system_number: Optional[int] + autonomous_system_number: int | None """The autonomous system number associated with the IP address.""" - autonomous_system_organization: Optional[str] + autonomous_system_organization: str | None """The organization associated with the registered autonomous system number for the IP address. """ - # pylint:disable=too-many-arguments,too-many-positional-arguments def __init__( self, ip_address: IPAddress, *, - autonomous_system_number: Optional[int] = None, - autonomous_system_organization: Optional[str] = None, - network: Optional[str] = None, - prefix_len: Optional[int] = None, + autonomous_system_number: int | None = None, + autonomous_system_organization: str | None = None, + network: str | None = None, + prefix_len: int | None = None, **_, ) -> None: super().__init__(ip_address, network, prefix_len) @@ -349,7 +352,7 @@ def __init__( class ConnectionType(SimpleModel): """Model class for the GeoIP2 Connection-Type.""" - connection_type: Optional[str] + connection_type: str | None """The connection type may take the following values: - Dialup @@ -365,9 +368,9 @@ def __init__( self, ip_address: IPAddress, *, - connection_type: Optional[str] = None, - network: Optional[str] = None, - prefix_len: Optional[int] = None, + connection_type: str | None = None, + network: str | None = None, + prefix_len: int | None = None, **_, ) -> None: super().__init__(ip_address, network, prefix_len) @@ -377,16 +380,16 @@ def __init__( class Domain(SimpleModel): """Model class for the GeoIP2 Domain.""" - domain: Optional[str] + domain: str | None """The domain associated with the IP address.""" def __init__( self, ip_address: IPAddress, *, - domain: Optional[str] = None, - network: Optional[str] = None, - prefix_len: Optional[int] = None, + domain: str | None = None, + network: str | None = None, + prefix_len: int | None = None, **_, ) -> None: super().__init__(ip_address, network, prefix_len) @@ -396,37 +399,36 @@ def __init__( class ISP(ASN): """Model class for the GeoIP2 ISP.""" - isp: Optional[str] + isp: str | None """The name of the ISP associated with the IP address.""" - mobile_country_code: Optional[str] + mobile_country_code: str | None """The `mobile country code (MCC) `_ associated with the IP address and ISP. """ - mobile_network_code: Optional[str] + mobile_network_code: str | None """The `mobile network code (MNC) `_ associated with the IP address and ISP. """ - organization: Optional[str] + organization: str | None """The name of the organization associated with the IP address.""" - # pylint:disable=too-many-arguments,too-many-positional-arguments def __init__( self, ip_address: IPAddress, *, - autonomous_system_number: Optional[int] = None, - autonomous_system_organization: Optional[str] = None, - isp: Optional[str] = None, - mobile_country_code: Optional[str] = None, - mobile_network_code: Optional[str] = None, - organization: Optional[str] = None, - network: Optional[str] = None, - prefix_len: Optional[int] = None, + autonomous_system_number: int | None = None, + autonomous_system_organization: str | None = None, + isp: str | None = None, + mobile_country_code: str | None = None, + mobile_network_code: str | None = None, + organization: str | None = None, + network: str | None = None, + prefix_len: int | None = None, **_, ) -> None: super().__init__( diff --git a/geoip2/records.py b/geoip2/records.py index 3da901a2..1405b690 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -1,17 +1,20 @@ """Record classes used within the response models.""" -# pylint:disable=too-many-arguments,too-many-positional-arguments,too-many-instance-attributes,too-many-locals +from __future__ import annotations import ipaddress - -# pylint:disable=R0903 from abc import ABCMeta -from collections.abc import Sequence from ipaddress import IPv4Address, IPv6Address -from typing import Optional, Union +from typing import TYPE_CHECKING from geoip2._internal import Model -from geoip2.types import IPAddress + +if TYPE_CHECKING: + from collections.abc import Sequence + + from typing_extensions import Self + + from geoip2.types import IPAddress class Record(Model, metaclass=ABCMeta): @@ -31,8 +34,8 @@ class PlaceRecord(Record, metaclass=ABCMeta): def __init__( self, - locales: Optional[Sequence[str]], - names: Optional[dict[str, str]], + locales: Sequence[str] | None, + names: dict[str, str] | None, ) -> None: if locales is None: locales = ["en"] @@ -42,9 +45,8 @@ def __init__( self.names = names @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """The name based on the locales list passed to the constructor.""" - # pylint:disable=E1101 return next((self.names.get(x) for x in self._locales if x in self.names), None) @@ -56,21 +58,21 @@ class City(PlaceRecord): This record is returned by ``city``, ``enterprise``, and ``insights``. """ - confidence: Optional[int] + confidence: int | None """A value from 0-100 indicating MaxMind's confidence that the city is correct. This attribute is only available from the Insights end point and the Enterprise database. """ - geoname_id: Optional[int] + geoname_id: int | None """The GeoName ID for the city.""" def __init__( self, - locales: Optional[Sequence[str]], + locales: Sequence[str] | None, *, - confidence: Optional[int] = None, - geoname_id: Optional[int] = None, - names: Optional[dict[str, str]] = None, + confidence: int | None = None, + geoname_id: int | None = None, + names: dict[str, str] | None = None, **_, ) -> None: self.confidence = confidence @@ -85,20 +87,20 @@ class Continent(PlaceRecord): address. """ - code: Optional[str] + code: str | None """A two character continent code like "NA" (North America) or "OC" (Oceania). """ - geoname_id: Optional[int] + geoname_id: int | None """The GeoName ID for the continent.""" def __init__( self, - locales: Optional[Sequence[str]], + locales: Sequence[str] | None, *, - code: Optional[str] = None, - geoname_id: Optional[int] = None, - names: Optional[dict[str, str]] = None, + code: str | None = None, + geoname_id: int | None = None, + names: dict[str, str] | None = None, **_, ) -> None: self.code = code @@ -112,16 +114,16 @@ class Country(PlaceRecord): This class contains the country-level data associated with an IP address. """ - confidence: Optional[int] + confidence: int | None """A value from 0-100 indicating MaxMind's confidence that the country is correct. This attribute is only available from the Insights end point and the Enterprise database. """ - geoname_id: Optional[int] + geoname_id: int | None """The GeoName ID for the country.""" is_in_european_union: bool """This is true if the country is a member state of the European Union.""" - iso_code: Optional[str] + iso_code: str | None """The two-character `ISO 3166-1 `_ alpha code for the country. @@ -129,13 +131,13 @@ class Country(PlaceRecord): def __init__( self, - locales: Optional[Sequence[str]], + locales: Sequence[str] | None, *, - confidence: Optional[int] = None, - geoname_id: Optional[int] = None, + confidence: int | None = None, + geoname_id: int | None = None, is_in_european_union: bool = False, - iso_code: Optional[str] = None, - names: Optional[dict[str, str]] = None, + iso_code: str | None = None, + names: dict[str, str] | None = None, **_, ) -> None: self.confidence = confidence @@ -153,7 +155,7 @@ class RepresentedCountry(Country): represented by something like a military base. """ - type: Optional[str] + type: str | None """A string indicating the type of entity that is representing the country. Currently we only return ``military`` but this could expand to include other types in the future. @@ -161,15 +163,14 @@ class RepresentedCountry(Country): def __init__( self, - locales: Optional[Sequence[str]], + locales: Sequence[str] | None, *, - confidence: Optional[int] = None, - geoname_id: Optional[int] = None, + confidence: int | None = None, + geoname_id: int | None = None, is_in_european_union: bool = False, - iso_code: Optional[str] = None, - names: Optional[dict[str, str]] = None, - # pylint:disable=redefined-builtin - type: Optional[str] = None, + iso_code: str | None = None, + names: dict[str, str] | None = None, + type: str | None = None, # noqa: A002 **_, ) -> None: self.type = type @@ -191,37 +192,37 @@ class Location(Record): This record is returned by ``city``, ``enterprise``, and ``insights``. """ - average_income: Optional[int] + average_income: int | None """The average income in US dollars associated with the requested IP address. This attribute is only available from the Insights end point. """ - accuracy_radius: Optional[int] + accuracy_radius: int | None """The approximate accuracy radius in kilometers around the latitude and longitude for the IP address. This is the radius where we have a 67% confidence that the device using the IP address resides within the circle centered at the latitude and longitude with the provided radius. """ - latitude: Optional[float] + latitude: float | None """The approximate latitude of the location associated with the IP address. This value is not precise and should not be used to identify a particular address or household. """ - longitude: Optional[float] + longitude: float | None """The approximate longitude of the location associated with the IP address. This value is not precise and should not be used to identify a particular address or household. """ - metro_code: Optional[int] + metro_code: int | None """The metro code is a no-longer-maintained code for targeting advertisements in Google. .. deprecated:: 4.9.0 """ - population_density: Optional[int] + population_density: int | None """The estimated population per square kilometer associated with the IP address. This attribute is only available from the Insights end point. """ - time_zone: Optional[str] + time_zone: str | None """The time zone associated with location, as specified by the `IANA Time Zone Database `_, e.g., "America/New_York". @@ -230,13 +231,13 @@ class Location(Record): def __init__( self, *, - 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, + average_income: int | None = None, + accuracy_radius: int | None = None, + latitude: float | None = None, + longitude: float | None = None, + metro_code: int | None = None, + population_density: int | None = None, + time_zone: str | None = None, **_, ) -> None: self.average_income = average_income @@ -251,12 +252,12 @@ def __init__( class MaxMind(Record): """Contains data related to your MaxMind account.""" - queries_remaining: Optional[int] + queries_remaining: int | None """The number of remaining queries you have for the end point you are calling. """ - def __init__(self, *, queries_remaining: Optional[int] = None, **_) -> None: + def __init__(self, *, queries_remaining: int | None = None, **_) -> None: self.queries_remaining = queries_remaining @@ -268,12 +269,12 @@ class Postal(Record): This attribute is returned by ``city``, ``enterprise``, and ``insights``. """ - code: Optional[str] + code: str | None """The postal code of the location. Postal codes are not available for all countries. In some countries, this will only contain part of the postal code. """ - confidence: Optional[int] + confidence: int | None """A value from 0-100 indicating MaxMind's confidence that the postal code is correct. This attribute is only available from the Insights end point and the Enterprise database. @@ -282,8 +283,8 @@ class Postal(Record): def __init__( self, *, - code: Optional[str] = None, - confidence: Optional[int] = None, + code: str | None = None, + confidence: int | None = None, **_, ) -> None: self.code = code @@ -298,26 +299,26 @@ class Subdivision(PlaceRecord): This attribute is returned by ``city``, ``enterprise``, and ``insights``. """ - confidence: Optional[int] + confidence: int | None """This is a value from 0-100 indicating MaxMind's confidence that the subdivision is correct. This attribute is only available from the Insights end point and the Enterprise database. """ - geoname_id: Optional[int] + geoname_id: int | None """This is a GeoName ID for the subdivision.""" - iso_code: Optional[str] + iso_code: str | None """This is a string up to three characters long contain the subdivision portion of the `ISO 3166-2 code `_. """ def __init__( self, - locales: Optional[Sequence[str]], + locales: Sequence[str] | None, *, - confidence: Optional[int] = None, - geoname_id: Optional[int] = None, - iso_code: Optional[str] = None, - names: Optional[dict[str, str]] = None, + confidence: int | None = None, + geoname_id: int | None = None, + iso_code: str | None = None, + names: dict[str, str] | None = None, **_, ) -> None: self.confidence = confidence @@ -326,7 +327,7 @@ def __init__( super().__init__(locales, names) -class Subdivisions(tuple): +class Subdivisions(tuple): # noqa: SLOT001 """A tuple-like collection of subdivisions associated with an IP address. This class contains the subdivisions of the country associated with the @@ -339,18 +340,36 @@ class Subdivisions(tuple): """ def __new__( - cls: type["Subdivisions"], - locales: Optional[Sequence[str]], - *subdivisions, - ) -> "Subdivisions": + cls: type[Self], + locales: Sequence[str] | None, + *subdivisions: dict, + ) -> Self: + """Create a new Subdivisions instance. + + This method constructs the tuple with Subdivision objects created + from the provided dictionaries. + + Arguments: + cls: The class to instantiate (Subdivisions). + locales: A sequence of locale strings (e.g., ['en', 'fr']) + or None, passed to each Subdivision object. + *subdivisions: A variable number of dictionaries, where each + dictionary contains the data for a single :py:class:`Subdivision` + object (e.g., name, iso_code). + + Returns: + A new instance of Subdivisions containing :py:class:`Subdivision` objects. + + """ subobjs = tuple(Subdivision(locales, **x) for x in subdivisions) - return super().__new__(cls, subobjs) # type: ignore + return super().__new__(cls, subobjs) def __init__( self, - locales: Optional[Sequence[str]], - *subdivisions, # pylint:disable=W0613 + locales: Sequence[str] | None, + *_: dict, ) -> None: + """Initialize the Subdivisions instance.""" self._locales = locales super().__init__() @@ -373,19 +392,19 @@ class Traits(Record): This class contains the traits data associated with an IP address. """ - autonomous_system_number: Optional[int] + autonomous_system_number: int | None """The `autonomous system number `_ associated with the IP address. This attribute is only available from the City Plus and Insights web services and the Enterprise database. """ - autonomous_system_organization: Optional[str] + autonomous_system_organization: str | None """The organization associated with the registered `autonomous system number `_ for the IP address. This attribute is only available from the City Plus and Insights web service end points and the Enterprise database. """ - connection_type: Optional[str] + connection_type: str | None """The connection type may take the following values: - Dialup @@ -399,14 +418,14 @@ class Traits(Record): This attribute is only available from the City Plus and Insights web service end points and the Enterprise database. """ - domain: Optional[str] + domain: str | None """The second level domain associated with the IP address. This will be something like "example.com" or "example.co.uk", not "foo.example.com". This attribute is only available from the City Plus and Insights web service end points and the Enterprise database. """ - _ip_address: Optional[IPAddress] + _ip_address: IPAddress | None is_anonymous: bool """This is true if the IP address belongs to any sort of anonymous network. This attribute is only available from Insights. @@ -468,29 +487,29 @@ class Traits(Record): """This is true if the IP address is a Tor exit node. This attribute is only available from Insights. """ - isp: Optional[str] + isp: str | None """The name of the ISP associated with the IP address. This attribute is only available from the City Plus and Insights web services and the Enterprise database. """ - mobile_country_code: Optional[str] + mobile_country_code: str | None """The `mobile country code (MCC) `_ associated with the IP address and ISP. This attribute is available from the City Plus and Insights web services and the Enterprise database. """ - mobile_network_code: Optional[str] + mobile_network_code: str | None """The `mobile network code (MNC) `_ associated with the IP address and ISP. This attribute is available from the City Plus and Insights web services and the Enterprise database. """ - organization: Optional[str] + organization: str | None """The name of the organization associated with the IP address. This attribute is only available from the City Plus and Insights web services and the Enterprise database. """ - static_ip_score: Optional[float] + static_ip_score: float | None """An indicator of how static or dynamic an IP address is. The value ranges from 0 to 99.99 with higher values meaning a greater static association. For example, many IP addresses with a user_type of cellular have a @@ -501,13 +520,13 @@ class Traits(Record): the same user over time. This attribute is only available from Insights. """ - user_count: Optional[int] + user_count: int | None """The estimated number of users sharing the IP/network during the past 24 hours. For IPv4, the count is for the individual IP. For IPv6, the count is for the /64 network. This attribute is only available from Insights. """ - user_type: Optional[str] + user_type: str | None """The user type associated with the IP address. This can be one of the following values: @@ -531,16 +550,16 @@ class Traits(Record): This attribute is only available from the Insights end point and the Enterprise database. """ - _network: Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]] - _prefix_len: Optional[int] + _network: ipaddress.IPv4Network | ipaddress.IPv6Network | None + _prefix_len: int | None def __init__( self, *, - autonomous_system_number: Optional[int] = None, - autonomous_system_organization: Optional[str] = None, - connection_type: Optional[str] = None, - domain: Optional[str] = None, + autonomous_system_number: int | None = None, + autonomous_system_organization: str | None = None, + connection_type: str | None = None, + domain: str | None = None, is_anonymous: bool = False, is_anonymous_proxy: bool = False, is_anonymous_vpn: bool = False, @@ -550,16 +569,16 @@ def __init__( is_residential_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, - mobile_country_code: Optional[str] = None, - mobile_network_code: Optional[str] = None, + isp: str | None = None, + ip_address: str | None = None, + network: str | None = None, + organization: str | None = None, + prefix_len: int | None = None, + static_ip_score: float | None = None, + user_count: int | None = None, + user_type: str | None = None, + mobile_country_code: str | None = None, + mobile_network_code: str | None = None, is_anycast: bool = False, **_, ) -> None: @@ -595,7 +614,7 @@ def __init__( self._prefix_len = prefix_len @property - def ip_address(self) -> Optional[Union[IPv4Address, IPv6Address]]: + def ip_address(self) -> IPv4Address | IPv6Address | None: """The IP address that the data in the model is for. If you performed a "me" lookup against the web service, this will be @@ -613,7 +632,7 @@ def ip_address(self) -> Optional[Union[IPv4Address, IPv6Address]]: return ip_address @property - def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: + def network(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None: """The network associated with the record. In particular, this is the largest network where all of the fields besides diff --git a/geoip2/webservice.py b/geoip2/webservice.py index f910fcae..0316116c 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -21,10 +21,11 @@ """ +from __future__ import annotations + import ipaddress import json -from collections.abc import Sequence -from typing import Any, Optional, Union, cast +from typing import TYPE_CHECKING, cast import aiohttp import aiohttp.http @@ -42,8 +43,14 @@ OutOfQueriesError, PermissionRequiredError, ) -from geoip2.models import City, Country, Insights -from geoip2.types import IPAddress + +if TYPE_CHECKING: + from collections.abc import Sequence + + from typing_extensions import Self + + from geoip2.models import City, Country, Insights + from geoip2.types import IPAddress _AIOHTTP_UA = ( f"GeoIP2-Python-Client/{geoip2.__version__} {aiohttp.http.SERVER_SOFTWARE}" @@ -54,7 +61,9 @@ ) -class BaseClient: # pylint: disable=missing-class-docstring, too-few-public-methods +class BaseClient: + """Base class for AsyncClient and Client.""" + _account_id: str _host: str _license_key: str @@ -66,11 +75,10 @@ def __init__( account_id: int, license_key: str, host: str, - locales: Optional[Sequence[str]], + locales: Sequence[str] | None, timeout: float, ) -> None: """Construct a Client.""" - # pylint: disable=too-many-arguments,too-many-positional-arguments if locales is None: locales = ["en"] @@ -90,7 +98,7 @@ def _uri(self, path: str, ip_address: IPAddress) -> str: return "/".join([self._base_uri, path, str(ip_address)]) @staticmethod - def _handle_success(body: str, uri: str) -> Any: + def _handle_success(body: str, uri: str) -> dict: try: return json.loads(body) except ValueError as ex: @@ -140,9 +148,10 @@ def _exception_for_4xx_status( decoded_body = json.loads(body) except ValueError as ex: return HTTPError( - f"Received a {status} error for {uri} but it did not include " - + "the expected JSON body: " - + ", ".join(ex.args), + ( + f"Received a {status} error for {uri} but it did not include " + f"the expected JSON body: {', '.join(ex.args)}" + ), status, uri, body, @@ -168,13 +177,13 @@ def _exception_for_web_service_error( code: str, status: int, uri: str, - ) -> Union[ - AuthenticationError, - AddressNotFoundError, - PermissionRequiredError, - OutOfQueriesError, - InvalidRequestError, - ]: + ) -> ( + AuthenticationError + | AddressNotFoundError + | PermissionRequiredError + | OutOfQueriesError + | InvalidRequestError + ): if code in ("IP_ADDRESS_NOT_FOUND", "IP_ADDRESS_RESERVED"): return AddressNotFoundError(message) if code in ( @@ -197,7 +206,7 @@ def _exception_for_web_service_error( def _exception_for_5xx_status( status: int, uri: str, - body: Optional[str], + body: str | None, ) -> HTTPError: return HTTPError( f"Received a server error ({status}) for {uri}", @@ -210,7 +219,7 @@ def _exception_for_5xx_status( def _exception_for_non_200_status( status: int, uri: str, - body: Optional[str], + body: str | None, ) -> HTTPError: return HTTPError( f"Received a very surprising HTTP status ({status}) for {uri}", @@ -273,17 +282,18 @@ class AsyncClient(BaseClient): """ _existing_session: aiohttp.ClientSession - _proxy: Optional[str] + _proxy: str | None - def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments + def __init__( # noqa: PLR0913 self, account_id: int, license_key: str, host: str = "geoip.maxmind.com", - locales: Optional[Sequence[str]] = None, + locales: Sequence[str] | None = None, timeout: float = 60, - proxy: Optional[str] = None, + proxy: str | None = None, ) -> None: + """Initialize AsyncClient.""" super().__init__( account_id, license_key, @@ -304,7 +314,7 @@ async def city(self, ip_address: IPAddress = "me") -> City: """ return cast( - City, + "City", await self._response_for("city", geoip2.models.City, ip_address), ) @@ -319,7 +329,7 @@ async def country(self, ip_address: IPAddress = "me") -> Country: """ return cast( - Country, + "Country", await self._response_for("country", geoip2.models.Country, ip_address), ) @@ -337,7 +347,7 @@ async def insights(self, ip_address: IPAddress = "me") -> Insights: """ return cast( - Insights, + "Insights", await self._response_for("insights", geoip2.models.Insights, ip_address), ) @@ -354,9 +364,9 @@ async def _session(self) -> aiohttp.ClientSession: async def _response_for( self, path: str, - model_class: Union[type[Insights], type[City], type[Country]], + model_class: type[City | Country | Insights], ip_address: IPAddress, - ) -> Union[Country, City, Insights]: + ) -> Country | City | Insights: uri = self._uri(path, ip_address) session = await self._session() async with await session.get(uri, proxy=self._proxy) as response: @@ -376,10 +386,15 @@ async def close(self) -> None: if hasattr(self, "_existing_session"): await self._existing_session.close() - async def __aenter__(self) -> "AsyncClient": + async def __aenter__(self) -> Self: return self - async def __aexit__(self, exc_type: None, exc_value: None, traceback: None) -> None: + async def __aexit__( + self, + exc_type: object, + exc_value: object, + traceback: object, + ) -> None: await self.close() @@ -437,17 +452,18 @@ class Client(BaseClient): """ _session: requests.Session - _proxies: Optional[dict[str, str]] + _proxies: dict[str, str] | None - def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments + def __init__( # noqa: PLR0913 self, account_id: int, license_key: str, host: str = "geoip.maxmind.com", - locales: Optional[Sequence[str]] = None, + locales: Sequence[str] | None = None, timeout: float = 60, - proxy: Optional[str] = None, + proxy: str | None = None, ) -> None: + """Initialize Client.""" super().__init__(account_id, license_key, host, locales, timeout) self._session = requests.Session() self._session.auth = (self._account_id, self._license_key) @@ -468,7 +484,7 @@ def city(self, ip_address: IPAddress = "me") -> City: :returns: :py:class:`geoip2.models.City` object """ - return cast(City, 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: IPAddress = "me") -> Country: """Call the GeoIP2 Country endpoint with the specified IP. @@ -481,7 +497,7 @@ def country(self, ip_address: IPAddress = "me") -> Country: """ return cast( - Country, + "Country", self._response_for("country", geoip2.models.Country, ip_address), ) @@ -499,16 +515,16 @@ def insights(self, ip_address: IPAddress = "me") -> Insights: """ return cast( - Insights, + "Insights", self._response_for("insights", geoip2.models.Insights, ip_address), ) def _response_for( self, path: str, - model_class: Union[type[Insights], type[City], type[Country]], + model_class: type[City | Country | Insights], ip_address: IPAddress, - ) -> Union[Country, City, Insights]: + ) -> Country | City | Insights: uri = self._uri(path, ip_address) response = self._session.get(uri, proxies=self._proxies, timeout=self._timeout) status = response.status_code @@ -526,8 +542,8 @@ def close(self) -> None: """ self._session.close() - def __enter__(self) -> "Client": + def __enter__(self) -> Self: return self - def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None: + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: self.close() diff --git a/pyproject.toml b/pyproject.toml index 6be12846..0961762e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,7 @@ dev = [ "types-requests>=2.32.0.20250328", ] lint = [ - "black>=25.1.0", - "flake8>=7.2.0", "mypy>=1.15.0", - "pylint>=3.3.6", "ruff>=0.11.6", ] @@ -56,28 +53,23 @@ Documentation = "https://geoip2.readthedocs.org/" "Source Code" = "https://github.com/maxmind/GeoIP2-python" "Issue Tracker" = "https://github.com/maxmind/GeoIP2-python/issues" -[tool.black] -# src is showing up in our GitHub linting builds. It seems to -# contain deps. -extend-exclude = '^/src/' - -[tool.pylint."MESSAGES CONTROL"] -disable = "duplicate-code" - [tool.ruff.lint] select = ["ALL"] ignore = [ # Skip type annotation on **_ "ANN003", + # Redundant as the formatter handles missing trailing commas. + "COM812", + # documenting magic methods "D105", - # Line length. We let black handle this for now. - "E501", + # Conflicts with D211 + "D203", - # Don't bother with future imports for type annotations - "FA100", + # Conflicts with D212 + "D213", # Magic numbers for HTTP status codes seem ok most of the time. "PLR2004", @@ -88,6 +80,7 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] +"docs/*" = ["ALL"] "geoip2/{models,records}.py" = [ "D107", "PLR0913" ] "tests/*" = ["ANN201", "D"] @@ -121,10 +114,9 @@ dependency_groups = [ "lint", ] commands = [ - ["black", "--check", "--diff", "."], - ["flake8", "geoip2"], ["mypy", "geoip2", "tests"], - ["pylint", "geoip2"], + ["ruff", "check"], + ["ruff", "format", "--check", "--diff", "."], ] [tool.tox.gh.python] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a8d46884..00000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -# black uses 88 : ¯\_(ツ)_/¯ -max-line-length = 88 diff --git a/tests/database_test.py b/tests/database_test.py index 00d54a26..2c831dff 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python - - import datetime import ipaddress import sys @@ -17,7 +14,7 @@ try: import maxminddb.extension except ImportError: - maxminddb.extension = None # type: ignore + maxminddb.extension = None # type: ignore[assignment] class TestReader(unittest.TestCase): @@ -47,7 +44,7 @@ def test_unknown_address_network(self) -> None: self.fail("Expected AddressNotFoundError") except geoip2.errors.AddressNotFoundError as e: self.assertEqual(e.network, ipaddress.ip_network("10.0.0.0/8")) - except Exception as e: + except Exception as e: # noqa: BLE001 self.fail(f"Expected AddressNotFoundError, got {type(e)}: {e!s}") finally: reader.close() @@ -91,7 +88,6 @@ def test_anonymous_plus(self) -> None: with geoip2.database.Reader( "tests/data/test-data/GeoIP-Anonymous-Plus-Test.mmdb", ) as reader: - ip_address = "1.2.0.1" record = reader.anonymous_plus(ip_address) @@ -131,7 +127,11 @@ def test_asn(self) -> None: ip_address = "1.128.0.0" record = reader.asn(ip_address) - self.assertEqual(record, eval(repr(record)), "ASN repr can be eval'd") + self.assertEqual( + record, + eval(repr(record)), # noqa: S307 + "ASN repr can be eval'd", + ) self.assertEqual(record.autonomous_system_number, 1221) self.assertEqual(record.autonomous_system_organization, "Telstra Pty Ltd") @@ -179,7 +179,7 @@ def test_connection_type(self) -> None: self.assertEqual( record, - eval(repr(record)), + eval(repr(record)), # noqa: S307 "ConnectionType repr can be eval'd", ) @@ -219,7 +219,11 @@ def test_domain(self) -> None: ip_address = "1.2.0.0" record = reader.domain(ip_address) - self.assertEqual(record, eval(repr(record)), "Domain repr can be eval'd") + self.assertEqual( + record, + eval(repr(record)), # noqa: S307 + "Domain repr can be eval'd", + ) self.assertEqual(record.domain, "maxmind.com") self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address)) @@ -267,7 +271,11 @@ def test_isp(self) -> None: ) as reader: ip_address = "1.128.0.0" record = reader.isp(ip_address) - self.assertEqual(record, eval(repr(record)), "ISP repr can be eval'd") + self.assertEqual( + record, + eval(repr(record)), # noqa: S307 + "ISP repr can be eval'd", + ) self.assertEqual(record.autonomous_system_number, 1221) self.assertEqual(record.autonomous_system_organization, "Telstra Pty Ltd") @@ -298,7 +306,7 @@ def test_context_manager(self) -> None: ) @patch("maxminddb.open_database") - def test_modes(self, mock_open) -> None: + def test_modes(self, mock_open: MagicMock) -> None: mock_open.return_value = MagicMock() path = "tests/data/test-data/GeoIP2-Country-Test.mmdb" diff --git a/tests/models_test.py b/tests/models_test.py index 58bff6e8..7cce9a8d 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python - - import ipaddress import sys import unittest +from typing import ClassVar sys.path.append("..") @@ -93,7 +91,7 @@ def test_insights_full(self) -> None: }, } - model = geoip2.models.Insights(["en"], **raw) # type: ignore + model = geoip2.models.Insights(["en"], **raw) # type: ignore[arg-type] self.assertEqual( type(model), geoip2.models.Insights, @@ -196,7 +194,11 @@ def test_insights_full(self) -> None: "Insights str representation looks reasonable", ) - self.assertEqual(model, eval(repr(model)), "Insights repr can be eval'd") + self.assertEqual( + model, + eval(repr(model)), # noqa: S307 + "Insights repr can be eval'd", + ) self.assertRegex( str(model.location), @@ -206,23 +208,29 @@ def test_insights_full(self) -> None: self.assertEqual( model.location, - eval(repr(model.location)), + eval(repr(model.location)), # noqa: S307 "Location repr can be eval'd", ) - self.assertIs(model.country.is_in_european_union, False) - self.assertIs(model.registered_country.is_in_european_union, False) - self.assertIs(model.represented_country.is_in_european_union, True) + self.assertIs(model.country.is_in_european_union, False) # noqa: FBT003 + self.assertIs( + model.registered_country.is_in_european_union, + False, # noqa: FBT003 + ) + self.assertIs( + model.represented_country.is_in_european_union, + True, # noqa: FBT003 + ) - self.assertIs(model.traits.is_anonymous, True) - self.assertIs(model.traits.is_anonymous_proxy, True) - self.assertIs(model.traits.is_anonymous_vpn, True) - self.assertIs(model.traits.is_anycast, True) - self.assertIs(model.traits.is_hosting_provider, True) - self.assertIs(model.traits.is_public_proxy, True) - self.assertIs(model.traits.is_residential_proxy, True) - self.assertIs(model.traits.is_satellite_provider, True) - self.assertIs(model.traits.is_tor_exit_node, True) + self.assertIs(model.traits.is_anonymous, True) # noqa: FBT003 + self.assertIs(model.traits.is_anonymous_proxy, True) # noqa: FBT003 + self.assertIs(model.traits.is_anonymous_vpn, True) # noqa: FBT003 + self.assertIs(model.traits.is_anycast, True) # noqa: FBT003 + self.assertIs(model.traits.is_hosting_provider, True) # noqa: FBT003 + self.assertIs(model.traits.is_public_proxy, True) # noqa: FBT003 + self.assertIs(model.traits.is_residential_proxy, True) # noqa: FBT003 + self.assertIs(model.traits.is_satellite_provider, True) # noqa: FBT003 + self.assertIs(model.traits.is_tor_exit_node, True) # noqa: FBT003 self.assertEqual(model.traits.user_count, 2) self.assertEqual(model.traits.static_ip_score, 1.3) @@ -296,7 +304,7 @@ def test_city_full(self) -> None: "is_satellite_provider": True, }, } - model = geoip2.models.City(["en"], **raw) # type: ignore + model = geoip2.models.City(["en"], **raw) # type: ignore[arg-type] self.assertEqual(type(model), geoip2.models.City, "geoip2.models.City object") self.assertEqual( type(model.city), @@ -395,7 +403,7 @@ def test_city_full(self) -> None: r"^geoip2.models.City\(\[.*en.*\], .*geoname_id.*\)", ) - self.assertFalse(model == True, "__eq__ does not blow up on weird input") + self.assertFalse(model is True, "__eq__ does not blow up on weird input") def test_unknown_keys(self) -> None: model = geoip2.models.City( @@ -435,9 +443,9 @@ def test_unknown_keys(self) -> None: unk_base={"blah": 1}, ) with self.assertRaises(AttributeError): - model.unk_base # type: ignore + model.unk_base # type: ignore[attr-defined] # noqa: B018 with self.assertRaises(AttributeError): - model.traits.invalid # type: ignore + model.traits.invalid # type: ignore[attr-defined] # noqa: B018 self.assertEqual( model.traits.ip_address, ipaddress.ip_address("1.2.3.4"), @@ -446,7 +454,7 @@ def test_unknown_keys(self) -> None: class TestNames(unittest.TestCase): - raw: dict = { + raw: ClassVar[dict] = { "continent": { "code": "NA", "geoname_id": 42, diff --git a/tests/webservice_test.py b/tests/webservice_test.py index c17f86b8..f9187538 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +from __future__ import annotations import asyncio import copy @@ -7,7 +7,7 @@ import unittest from abc import ABC, abstractmethod from collections import defaultdict -from typing import cast, Callable, Union +from typing import Callable, ClassVar, cast import pytest import pytest_httpserver @@ -28,10 +28,10 @@ class TestBaseClient(unittest.TestCase, ABC): - client: Union[AsyncClient, Client] - client_class: Callable[[int, str], Union[AsyncClient, Client]] + client: AsyncClient | Client + client_class: Callable[[int, str], AsyncClient | Client] - country = { + country: ClassVar = { "continent": {"code": "NA", "geoname_id": 42, "names": {"en": "North America"}}, "country": { "geoname_id": 1, @@ -54,14 +54,14 @@ class TestBaseClient(unittest.TestCase, ABC): # this is not a comprehensive representation of the # JSON from the server - insights = cast(dict, copy.deepcopy(country)) + insights = cast("dict", copy.deepcopy(country)) insights["traits"]["user_count"] = 2 insights["traits"]["static_ip_score"] = 1.3 @abstractmethod - def run_client(self, v): ... + def run_client(self, v): ... # noqa: ANN001 - def _content_type(self, endpoint): + def _content_type(self, endpoint: str) -> str: return ( "application/vnd.maxmind.com-" + endpoint @@ -97,7 +97,7 @@ def test_country_ok(self) -> None: self.assertEqual(country.country.geoname_id, 1, "country geoname_id is 1") self.assertIs( country.country.is_in_european_union, - False, + False, # noqa: FBT003 "country is_in_european_union is False", ) self.assertEqual(country.country.iso_code, "US", "country iso_code is US") @@ -118,7 +118,7 @@ def test_country_ok(self) -> None: ) self.assertIs( country.registered_country.is_in_european_union, - True, + True, # noqa: FBT003 "registered_country is_in_european_union is True", ) self.assertEqual( @@ -271,16 +271,21 @@ def test_account_id_required(self) -> None: def test_user_id_required(self) -> None: self._test_error(401, "USER_ID_REQUIRED", AuthenticationError) - def test_account_id_unkown(self) -> None: + def test_account_id_unknown(self) -> None: self._test_error(401, "ACCOUNT_ID_UNKNOWN", AuthenticationError) - def test_user_id_unkown(self) -> None: + def test_user_id_unknown(self) -> None: self._test_error(401, "USER_ID_UNKNOWN", AuthenticationError) def test_out_of_queries_error(self) -> None: self._test_error(402, "OUT_OF_QUERIES", OutOfQueriesError) - def _test_error(self, status, error_code, error_class) -> None: + def _test_error( + self, + status: int, + error_code: str, + error_class: type[Exception], + ) -> None: msg = "Some error message" body = {"error": msg, "code": error_code} self.httpserver.expect_request( @@ -310,7 +315,7 @@ def test_unknown_error(self) -> None: self.run_client(self.client.country(ip)) def test_request(self) -> None: - def user_agent_compare(actual: str, expected: str) -> bool: + def user_agent_compare(actual: str, _: str) -> bool: if actual is None: return False return actual.startswith("GeoIP2-Python-Client/") @@ -379,19 +384,18 @@ def test_insights_ok(self) -> None: self.assertEqual(insights.traits.user_count, 2, "user_count is 2") def test_named_constructor_args(self) -> None: - id = 47 + account_id = 47 key = "1234567890ab" - client = self.client_class(id, key) - self.assertEqual(client._account_id, str(id)) - self.assertEqual(client._license_key, key) + client = self.client_class(account_id, key) + self.assertEqual(client._account_id, str(account_id)) # noqa: SLF001 + self.assertEqual(client._license_key, key) # noqa: SLF001 def test_missing_constructor_args(self) -> None: with self.assertRaises(TypeError): - self.client_class(license_key="1234567890ab") # type: ignore[call-arg] with self.assertRaises(TypeError): - self.client_class("47") # type: ignore + self.client_class("47") # type: ignore[call-arg,arg-type,misc] class TestClient(TestBaseClient): @@ -400,10 +404,10 @@ class TestClient(TestBaseClient): def setUp(self) -> None: self.client_class = Client self.client = Client(42, "abcdef123456") - self.client._base_uri = self.httpserver.url_for("/geoip/v2.1") + self.client._base_uri = self.httpserver.url_for("/geoip/v2.1") # noqa: SLF001 self.maxDiff = 20_000 - def run_client(self, v): + def run_client(self, v): # noqa: ANN001 return v @@ -414,14 +418,14 @@ def setUp(self) -> None: self._loop = asyncio.new_event_loop() self.client_class = AsyncClient self.client = AsyncClient(42, "abcdef123456") - self.client._base_uri = self.httpserver.url_for("/geoip/v2.1") + self.client._base_uri = self.httpserver.url_for("/geoip/v2.1") # noqa: SLF001 self.maxDiff = 20_000 def tearDown(self) -> None: self._loop.run_until_complete(self.client.close()) self._loop.close() - def run_client(self, v): + def run_client(self, v): # noqa: ANN001 return self._loop.run_until_complete(v) diff --git a/uv.lock b/uv.lock index e50b608e..4c7e4b4e 100644 --- a/uv.lock +++ b/uv.lock @@ -126,18 +126,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, ] -[[package]] -name = "astroid" -version = "3.3.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/33/536530122a22a7504b159bccaf30a1f76aa19d23028bd8b5009eb9b2efea/astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550", size = 398731, upload-time = "2025-03-09T11:54:36.388Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/80/c749efbd8eef5ea77c7d6f1956e8fbfb51963b7f93ef79647afd4d9886e3/astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248", size = 275339, upload-time = "2025-03-09T11:54:34.489Z" }, -] - [[package]] name = "async-timeout" version = "5.0.1" @@ -156,44 +144,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] -[[package]] -name = "black" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, - { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, - { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, -] - [[package]] name = "certifi" version = "2025.1.31" @@ -277,18 +227,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -298,15 +236,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "dill" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, -] - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -429,7 +358,7 @@ wheels = [ [[package]] name = "geoip2" -version = "5.0.1" +version = "5.1.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -444,9 +373,7 @@ dev = [ { name = "types-requests" }, ] lint = [ - { name = "black" }, { name = "mypy" }, - { name = "pylint" }, { name = "ruff" }, ] @@ -464,9 +391,7 @@ dev = [ { name = "types-requests", specifier = ">=2.32.0.20250328" }, ] lint = [ - { name = "black", specifier = ">=25.1.0" }, { name = "mypy", specifier = ">=1.15.0" }, - { name = "pylint", specifier = ">=3.3.6" }, { name = "ruff", specifier = ">=0.11.6" }, ] @@ -488,15 +413,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] -[[package]] -name = "isort" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, -] - [[package]] name = "markupsafe" version = "3.0.2" @@ -661,15 +577,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/ab/6677aed7f7b1132ff1fbc2a499d75d1c7243b34839973df61384ac550991/maxminddb-2.7.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:15b178a5e1af71d7ea8e5374b3ae92d4ccbe52998dc57a737793d6dd029ec97c", size = 36647, upload-time = "2025-05-05T19:31:42.275Z" }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - [[package]] name = "multidict" version = "6.4.3" @@ -846,24 +753,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, -] - [[package]] name = "pluggy" version = "1.5.0" @@ -978,26 +867,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" }, ] -[[package]] -name = "pylint" -version = "3.3.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "astroid" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "dill" }, - { name = "isort" }, - { name = "mccabe" }, - { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tomlkit" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/a7/113d02340afb9dcbb0c8b25454e9538cd08f0ebf3e510df4ed916caa1a89/pylint-3.3.6.tar.gz", hash = "sha256:b634a041aac33706d56a0d217e6587228c66427e20ec21a019bc4cdee48c040a", size = 1519586, upload-time = "2025-03-20T11:25:38.207Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/21/9537fc94aee9ec7316a230a49895266cf02d78aa29b0a2efbc39566e0935/pylint-3.3.6-py3-none-any.whl", hash = "sha256:8b7c2d3e86ae3f94fb27703d521dd0b9b6b378775991f504d7c3a6275aa0a6a6", size = 522462, upload-time = "2025-03-20T11:25:36.13Z" }, -] - [[package]] name = "pytest" version = "8.3.5" @@ -1106,15 +975,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] -[[package]] -name = "tomlkit" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload-time = "2024-08-14T08:19:41.488Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, -] - [[package]] name = "types-requests" version = "2.32.0.20250328"