From 334525104e04002a2fde57397962ae53e7d5dd15 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Tue, 21 Mar 2023 13:12:41 +0200 Subject: [PATCH] Add mypy integration --- .github/workflows/test.yml | 3 +- googlemaps/addressvalidation.py | 23 ++- googlemaps/client.py | 259 +++++++++++++++++--------------- googlemaps/convert.py | 61 +++++--- googlemaps/directions.py | 44 ++++-- googlemaps/distance_matrix.py | 41 ++++- googlemaps/elevation.py | 17 ++- googlemaps/exceptions.py | 14 +- googlemaps/geocoding.py | 30 +++- googlemaps/geolocation.py | 29 +++- googlemaps/maps.py | 56 +++++-- googlemaps/places.py | 189 +++++++++++++---------- googlemaps/roads.py | 37 ++++- googlemaps/timezone.py | 15 +- googlemaps/types.py | 28 ++++ noxfile.py | 14 +- setup.cfg | 8 +- setup.py | 7 +- 18 files changed, 577 insertions(+), 298 deletions(-) create mode 100644 googlemaps/types.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 570a087a..56429890 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - name: Checkout repository uses: actions/checkout@v3 @@ -44,6 +44,7 @@ jobs: - name: Run tests run: | + python3 -m nox --session "mypy-${{ matrix.python-version }}" python3 -m nox --session "tests-${{ matrix.python-version }}" python3 -m nox -e distribution test: diff --git a/googlemaps/addressvalidation.py b/googlemaps/addressvalidation.py index 149f3b48..7e40687e 100644 --- a/googlemaps/addressvalidation.py +++ b/googlemaps/addressvalidation.py @@ -16,19 +16,28 @@ # """Performs requests to the Google Maps Address Validation API.""" +from __future__ import annotations +from typing import TYPE_CHECKING, cast, List, Optional + +import requests + from googlemaps import exceptions +from googlemaps.types import DictStrAny + +if TYPE_CHECKING: + from googlemaps.client import Client _ADDRESSVALIDATION_BASE_URL = "https://addressvalidation.googleapis.com" -def _addressvalidation_extract(response): +def _addressvalidation_extract(response: requests.Response) -> DictStrAny: """ Mimics the exception handling logic in ``client._get_body``, but for addressvalidation which uses a different response format. """ body = response.json() - return body + return cast(DictStrAny, body) # if response.status_code in (200, 404): # return body @@ -44,7 +53,13 @@ def _addressvalidation_extract(response): # raise exceptions.ApiError(response.status_code, error) -def addressvalidation(client, addressLines, regionCode=None , locality=None, enableUspsCass=None): +def addressvalidation( + client: Client, + addressLines: List[str], + regionCode: Optional[str] = None, + locality: Optional[str] = None, + enableUspsCass: Optional[bool] = None, +) -> DictStrAny: """ The Google Maps Address Validation API returns a verification of an address See https://developers.google.com/maps/documentation/address-validation/overview @@ -59,7 +74,7 @@ def addressvalidation(client, addressLines, regionCode=None , locality=None, ena :type locality: boolean """ - params = { + params: DictStrAny = { "address":{ "addressLines": addressLines } diff --git a/googlemaps/client.py b/googlemaps/client.py index d1f4ab6a..d28c9882 100644 --- a/googlemaps/client.py +++ b/googlemaps/client.py @@ -23,44 +23,129 @@ import base64 import collections import logging +from contextlib import suppress from datetime import datetime from datetime import timedelta import functools import hashlib import hmac import re +from typing import TypeVar, Callable, Optional, Dict, Any, Union, Tuple, cast, Deque, List +from typing_extensions import ParamSpec + import requests import random import time import math import sys +from urllib.parse import urlencode + import googlemaps -try: # Python 3 - from urllib.parse import urlencode -except ImportError: # Python 2 - from urllib import urlencode +from googlemaps.directions import directions as directions_method +from googlemaps.distance_matrix import distance_matrix as distance_matrix_method +from googlemaps.elevation import elevation as elevation_method +from googlemaps.elevation import elevation_along_path as elevation_along_path_method +from googlemaps.geocoding import geocode as geocode_method +from googlemaps.geocoding import reverse_geocode as reverse_geocode_method +from googlemaps.geolocation import geolocate as geolocate_method +from googlemaps.timezone import timezone as timezone_method +from googlemaps.roads import snap_to_roads as snap_to_roads_method +from googlemaps.roads import nearest_roads as nearest_roads_method +from googlemaps.roads import speed_limits as speed_limits_method +from googlemaps.roads import snapped_speed_limits as snapped_speed_limits_method +from googlemaps.places import find_place as find_place_method +from googlemaps.places import places as places_method +from googlemaps.places import places_nearby as places_nearby_method +from googlemaps.places import place as place_method +from googlemaps.places import places_photo as places_photo_method +from googlemaps.places import places_autocomplete as places_autocomplete_method +from googlemaps.places import places_autocomplete_query as places_autocomplete_query_method +from googlemaps.maps import static_map as static_map_method +from googlemaps.addressvalidation import addressvalidation as addressvalidation_method +from googlemaps.types import DictStrAny logger = logging.getLogger(__name__) _X_GOOG_MAPS_EXPERIENCE_ID = "X-Goog-Maps-Experience-ID" -_USER_AGENT = "GoogleGeoApiClientPython/%s" % googlemaps.__version__ +_USER_AGENT = f"GoogleGeoApiClientPython/{googlemaps.__version__}" _DEFAULT_BASE_URL = "https://maps.googleapis.com" _RETRIABLE_STATUSES = {500, 503, 504} +P = ParamSpec("P") +R = TypeVar("R") + + +def make_api_method(func: Callable[P, R]) -> Callable[P, R]: + """ + Provides a single entry point for modifying all API methods. + For now this is limited to allowing the client object to be modified + with an `extra_params` keyword arg to each method, that is then used + as the params for each web service request. + + Please note that this is an unsupported feature for advanced use only. + It's also currently incompatibile with multiple threads, see GH #160. + """ + + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + args[0]._extra_params = kwargs.pop("extra_params", None) # type: ignore[attr-defined] + + try: + return func(*args, **kwargs) + finally: + with suppress(AttributeError): + del args[0]._extra_params # type: ignore[attr-defined] + + return wrapper + + class Client: """Performs requests to the Google Maps API web services.""" - def __init__(self, key=None, client_id=None, client_secret=None, - timeout=None, connect_timeout=None, read_timeout=None, - retry_timeout=60, requests_kwargs=None, - queries_per_second=60, queries_per_minute=6000,channel=None, - retry_over_query_limit=True, experience_id=None, - requests_session=None, - base_url=_DEFAULT_BASE_URL): + directions = make_api_method(directions_method) + distance_matrix = make_api_method(distance_matrix_method) + elevation = make_api_method(elevation_method) + elevation_along_path = make_api_method(elevation_along_path_method) + geocode = make_api_method(geocode_method) + reverse_geocode = make_api_method(reverse_geocode_method) + geolocate = make_api_method(geolocate_method) + timezone = make_api_method(timezone_method) + snap_to_roads = make_api_method(snap_to_roads_method) + nearest_roads = make_api_method(nearest_roads_method) + speed_limits = make_api_method(speed_limits_method) + snapped_speed_limits = make_api_method(snapped_speed_limits_method) + find_place = make_api_method(find_place_method) + places = make_api_method(places_method) + places_nearby = make_api_method(places_nearby_method) + place = make_api_method(place_method) + places_photo = make_api_method(places_photo_method) + places_autocomplete = make_api_method(places_autocomplete_method) + places_autocomplete_query = make_api_method(places_autocomplete_query_method) + static_map = make_api_method(static_map_method) + addressvalidation = make_api_method(addressvalidation_method) + + def __init__( + self, + key: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + timeout: Optional[int] = None, + connect_timeout: Optional[int] = None, + read_timeout: Optional[int] = None, + retry_timeout: int = 60, + requests_kwargs: Optional[DictStrAny] = None, + queries_per_second: int = 60, + queries_per_minute: int = 6000, + channel: Optional[str] = None, + retry_over_query_limit: bool = True, + experience_id: Optional[str] = None, + requests_session: Optional[requests.Session] = None, + base_url: str = _DEFAULT_BASE_URL, + ) -> None: """ :param key: Maps API key. Required, unless "client_id" and "client_secret" are set. Most users should use an API key. @@ -157,6 +242,7 @@ def __init__(self, key=None, client_id=None, client_secret=None, raise ValueError("Specify either timeout, or connect_timeout " "and read_timeout") + self.timeout: Optional[Union[int, Tuple[int, int]]] if connect_timeout and read_timeout: # Check that the version of requests is >= 2.4.0 chunks = requests.__version__.split(".") @@ -197,11 +283,11 @@ def __init__(self, key=None, client_id=None, client_secret=None, sys.exit("MISSING VALUE for queries_per_second or queries_per_minute") self.retry_over_query_limit = retry_over_query_limit - self.sent_times = collections.deque("", self.queries_quota) + self.sent_times: Deque[float] = collections.deque(maxlen=self.queries_quota) self.set_experience_id(experience_id) self.base_url = base_url - def set_experience_id(self, *experience_id_args): + def set_experience_id(self, *experience_id_args: Optional[str]) -> None: """Sets the value for the HTTP header field name 'X-Goog-Maps-Experience-ID' to be used on subsequent API calls. @@ -213,10 +299,10 @@ def set_experience_id(self, *experience_id_args): return headers = self.requests_kwargs.pop("headers", {}) - headers[_X_GOOG_MAPS_EXPERIENCE_ID] = ",".join(experience_id_args) + headers[_X_GOOG_MAPS_EXPERIENCE_ID] = ",".join(exp for exp in experience_id_args if exp is not None) self.requests_kwargs["headers"] = headers - def get_experience_id(self): + def get_experience_id(self) -> Optional[str]: """Gets the experience ID for the HTTP header field name 'X-Goog-Maps-Experience-ID' @@ -224,9 +310,9 @@ def get_experience_id(self): :rtype: str """ headers = self.requests_kwargs.get("headers", {}) - return headers.get(_X_GOOG_MAPS_EXPERIENCE_ID, None) + return cast(Optional[str], headers.get(_X_GOOG_MAPS_EXPERIENCE_ID, None)) - def clear_experience_id(self): + def clear_experience_id(self) -> None: """Clears the experience ID for the HTTP header field name 'X-Goog-Maps-Experience-ID' if set. """ @@ -236,9 +322,18 @@ def clear_experience_id(self): headers.pop(_X_GOOG_MAPS_EXPERIENCE_ID, {}) self.requests_kwargs["headers"] = headers - def _request(self, url, params, first_request_time=None, retry_counter=0, - base_url=None, accepts_clientid=True, - extract_body=None, requests_kwargs=None, post_json=None): + def _request( + self, + url: str, + params: Union[DictStrAny, List[Tuple[str, Any]]], + first_request_time: Optional[datetime] = None, + retry_counter: int = 0, + base_url: Optional[str] = None, + accepts_clientid: bool = True, + extract_body: Optional[Callable[[requests.Response], Any]] = None, + requests_kwargs: Optional[DictStrAny] = None, + post_json: Optional[Any] = None, + ) -> DictStrAny: """Performs HTTP GET/POST with credentials, returning the body as JSON. @@ -339,7 +434,7 @@ def _request(self, url, params, first_request_time=None, retry_counter=0, else: result = self._get_body(response) self.sent_times.append(time.time()) - return result + return cast(DictStrAny, result) except googlemaps.exceptions._RetriableRequest as e: if isinstance(e, googlemaps.exceptions._OverQueryLimit) and not self.retry_over_query_limit: raise @@ -349,10 +444,10 @@ def _request(self, url, params, first_request_time=None, retry_counter=0, retry_counter + 1, base_url, accepts_clientid, extract_body, requests_kwargs, post_json) - def _get(self, *args, **kwargs): # Backwards compatibility. + def _get(self, *args: Any, **kwargs: Any) -> Any: # Backwards compatibility. return self._request(*args, **kwargs) - def _get_body(self, response): + def _get_body(self, response: requests.Response) -> DictStrAny: if response.status_code != 200: raise googlemaps.exceptions.HTTPError(response.status_code) @@ -360,7 +455,7 @@ def _get_body(self, response): api_status = body["status"] if api_status == "OK" or api_status == "ZERO_RESULTS": - return body + return cast(DictStrAny, body) if api_status == "OVER_QUERY_LIMIT": raise googlemaps.exceptions._OverQueryLimit( @@ -369,7 +464,12 @@ def _get_body(self, response): raise googlemaps.exceptions.ApiError(api_status, body.get("error_message")) - def _generate_auth_url(self, path, params, accepts_clientid): + def _generate_auth_url( + self, + path: str, + params: Union[DictStrAny, List[Tuple[str, Any]]], + accepts_clientid: bool, + ) -> str: """Returns the path and query string portion of the request URL, first adding any necessary parameters. @@ -388,7 +488,7 @@ def _generate_auth_url(self, path, params, accepts_clientid): if type(params) is dict: params = sorted(dict(extra_params, **params).items()) else: - params = sorted(extra_params.items()) + params[:] # Take a copy. + params = sorted(extra_params.items()) + params[:] # type: ignore # Take a copy. if accepts_clientid and self.client_id and self.client_secret: if self.channel: @@ -407,74 +507,7 @@ def _generate_auth_url(self, path, params, accepts_clientid): "enterprise credentials.") -from googlemaps.directions import directions -from googlemaps.distance_matrix import distance_matrix -from googlemaps.elevation import elevation -from googlemaps.elevation import elevation_along_path -from googlemaps.geocoding import geocode -from googlemaps.geocoding import reverse_geocode -from googlemaps.geolocation import geolocate -from googlemaps.timezone import timezone -from googlemaps.roads import snap_to_roads -from googlemaps.roads import nearest_roads -from googlemaps.roads import speed_limits -from googlemaps.roads import snapped_speed_limits -from googlemaps.places import find_place -from googlemaps.places import places -from googlemaps.places import places_nearby -from googlemaps.places import place -from googlemaps.places import places_photo -from googlemaps.places import places_autocomplete -from googlemaps.places import places_autocomplete_query -from googlemaps.maps import static_map -from googlemaps.addressvalidation import addressvalidation - -def make_api_method(func): - """ - Provides a single entry point for modifying all API methods. - For now this is limited to allowing the client object to be modified - with an `extra_params` keyword arg to each method, that is then used - as the params for each web service request. - - Please note that this is an unsupported feature for advanced use only. - It's also currently incompatibile with multiple threads, see GH #160. - """ - @functools.wraps(func) - def wrapper(*args, **kwargs): - args[0]._extra_params = kwargs.pop("extra_params", None) - result = func(*args, **kwargs) - try: - del args[0]._extra_params - except AttributeError: - pass - return result - return wrapper - - -Client.directions = make_api_method(directions) -Client.distance_matrix = make_api_method(distance_matrix) -Client.elevation = make_api_method(elevation) -Client.elevation_along_path = make_api_method(elevation_along_path) -Client.geocode = make_api_method(geocode) -Client.reverse_geocode = make_api_method(reverse_geocode) -Client.geolocate = make_api_method(geolocate) -Client.timezone = make_api_method(timezone) -Client.snap_to_roads = make_api_method(snap_to_roads) -Client.nearest_roads = make_api_method(nearest_roads) -Client.speed_limits = make_api_method(speed_limits) -Client.snapped_speed_limits = make_api_method(snapped_speed_limits) -Client.find_place = make_api_method(find_place) -Client.places = make_api_method(places) -Client.places_nearby = make_api_method(places_nearby) -Client.place = make_api_method(place) -Client.places_photo = make_api_method(places_photo) -Client.places_autocomplete = make_api_method(places_autocomplete) -Client.places_autocomplete_query = make_api_method(places_autocomplete_query) -Client.static_map = make_api_method(static_map) -Client.addressvalidation = make_api_method(addressvalidation) - - -def sign_hmac(secret, payload): +def sign_hmac(secret: str, payload: str) -> str: """Returns a base64-encoded HMAC-SHA1 signature of a given string. :param secret: The key used for the signature, base64 encoded. @@ -485,14 +518,14 @@ def sign_hmac(secret, payload): :rtype: string """ - payload = payload.encode('ascii', 'strict') - secret = secret.encode('ascii', 'strict') - sig = hmac.new(base64.urlsafe_b64decode(secret), payload, hashlib.sha1) + bpayload = payload.encode('ascii', 'strict') + bsecret = secret.encode('ascii', 'strict') + sig = hmac.new(base64.urlsafe_b64decode(bsecret), bpayload, hashlib.sha1) out = base64.urlsafe_b64encode(sig.digest()) return out.decode('utf-8') -def urlencode_params(params): +def urlencode_params(params: Any) -> str: """URL encodes the parameters. :param params: The parameters @@ -515,26 +548,10 @@ def urlencode_params(params): return requests.utils.unquote_unreserved(urlencode(extended)) -try: - unicode - # NOTE(cbro): `unicode` was removed in Python 3. In Python 3, NameError is - # raised here, and caught below. - - def normalize_for_urlencode(value): - """(Python 2) Converts the value to a `str` (raw bytes).""" - if isinstance(value, unicode): - return value.encode('utf8') - - if isinstance(value, str): - return value - - return normalize_for_urlencode(str(value)) - -except NameError: - def normalize_for_urlencode(value): - """(Python 3) No-op.""" - # urlencode in Python 3 handles all the types we are passing it. - if isinstance(value, str): - return value +def normalize_for_urlencode(value: Any) -> str: + """(Python 3) No-op.""" + # urlencode in Python 3 handles all the types we are passing it. + if isinstance(value, str): + return value - return normalize_for_urlencode(str(value)) + return normalize_for_urlencode(str(value)) diff --git a/googlemaps/convert.py b/googlemaps/convert.py index 2b3d056e..3c8445c7 100644 --- a/googlemaps/convert.py +++ b/googlemaps/convert.py @@ -27,9 +27,13 @@ convert.latlng(sydney) # '-33.8674869,151.2069902' """ +from typing import Any, Union, Dict, List, Tuple, TypeVar, Iterator, overload, cast +from typing_extensions import TypeGuard, TypeAlias +from googlemaps.types import Location, DictStrAny, LocationDict -def format_float(arg): + +def format_float(arg: float) -> str: """Formats a float value to be as short as possible. Truncates float to 8 decimal places and trims extraneous @@ -55,7 +59,7 @@ def format_float(arg): return ("%.8f" % float(arg)).rstrip("0").rstrip(".") -def latlng(arg): +def latlng(arg: Location) -> str: """Converts a lat/lon pair to a comma-separated string. For example: @@ -81,7 +85,7 @@ def latlng(arg): return "%s,%s" % (format_float(normalized[0]), format_float(normalized[1])) -def normalize_lat_lng(arg): +def normalize_lat_lng(arg: Location) -> Tuple[float, float]: """Take the various lat/lng representations and return a tuple. Accepts various representations: @@ -108,7 +112,7 @@ def normalize_lat_lng(arg): "but got %s" % type(arg).__name__) -def location_list(arg): +def location_list(arg: Union[Location, List[Location]]) -> str: """Joins a list of locations into a pipe separated string, handling the various formats supported for lat/lng values. @@ -126,10 +130,10 @@ def location_list(arg): # Handle the single-tuple lat/lng case. return latlng(arg) else: - return "|".join([latlng(location) for location in as_list(arg)]) + return "|".join([latlng(cast(Location, location)) for location in as_list(arg)]) -def join_list(sep, arg): +def join_list(sep: str, arg: Union[str, List[str]]) -> str: """If arg is list-like, then joins it with sep. :param sep: Separator string. @@ -143,7 +147,20 @@ def join_list(sep, arg): return sep.join(as_list(arg)) -def as_list(arg): +T = TypeVar("T") + + +@overload +def as_list(arg: List[T]) -> List[T]: + pass + + +@overload +def as_list(arg: T) -> List[T]: + pass + + +def as_list(arg: Union[List[T], T]) -> List[T]: """Coerces arg into a list. If arg is already list-like, returns arg. Otherwise, returns a one-element list containing arg. @@ -151,10 +168,10 @@ def as_list(arg): """ if _is_list(arg): return arg - return [arg] + return [arg] # type: ignore[list-item] -def _is_list(arg): +def _is_list(arg: Any) -> TypeGuard[List[Any]]: """Checks if arg is list-like. This excludes strings and dicts.""" if isinstance(arg, dict): return False @@ -163,16 +180,12 @@ def _is_list(arg): return _has_method(arg, "__getitem__") if not _has_method(arg, "strip") else _has_method(arg, "__iter__") -def is_string(val): +def is_string(val: Any) -> TypeGuard[str]: """Determines whether the passed value is a string, safe for 2/3.""" - try: - basestring - except NameError: - return isinstance(val, str) - return isinstance(val, basestring) + return isinstance(val, str) -def time(arg): +def time(arg: Any) -> str: """Converts the value into a unix time (seconds since unix epoch). For example: @@ -192,7 +205,7 @@ def time(arg): return str(arg) -def _has_method(arg, method): +def _has_method(arg: Any, method: str) -> bool: """Returns true if the given object has a method with the given name. :param arg: the object @@ -205,7 +218,7 @@ def _has_method(arg, method): return hasattr(arg, method) and callable(getattr(arg, method)) -def components(arg): +def components(arg: Union[DictStrAny, List[DictStrAny]]) -> str: """Converts a dict of components to the format expected by the Google Maps server. @@ -223,7 +236,7 @@ def components(arg): # Components may have multiple values per type, here we # expand them into individual key/value items, eg: # {"country": ["US", "AU"], "foo": 1} -> "country:AU", "country:US", "foo:1" - def expand(arg): + def expand(arg: DictStrAny) -> Iterator[str]: for k, v in arg.items(): for item in as_list(v): yield "%s:%s" % (k, item) @@ -236,7 +249,7 @@ def expand(arg): "but got %s" % type(arg).__name__) -def bounds(arg): +def bounds(arg: Any) -> str: """Converts a lat/lon bounds to a comma- and pipe-separated string. Accepts two representations: @@ -276,7 +289,7 @@ def bounds(arg): "but got %s" % type(arg).__name__) -def size(arg): +def size(arg: Any) -> str: if isinstance(arg, int): return "%sx%s" % (arg, arg) elif _is_list(arg): @@ -287,7 +300,7 @@ def size(arg): "but got %s" % type(arg).__name__) -def decode_polyline(polyline): +def decode_polyline(polyline: str) -> List[Dict[str, float]]: """Decodes a Polyline string into a list of lat/lng dicts. See the developer docs for a detailed description of this encoding: @@ -329,7 +342,7 @@ def decode_polyline(polyline): return points -def encode_polyline(points): +def encode_polyline(points: List[Location]) -> str: """Encodes a list of points into a polyline string. See the developer docs for a detailed description of this encoding: @@ -363,7 +376,7 @@ def encode_polyline(points): return result -def shortest_path(locations): +def shortest_path(locations: List[Location]) -> str: """Returns the shortest representation of the given locations. The Elevations API limits requests to 2000 characters, and accepts diff --git a/googlemaps/directions.py b/googlemaps/directions.py index 353145cc..d190cb36 100644 --- a/googlemaps/directions.py +++ b/googlemaps/directions.py @@ -16,15 +16,35 @@ # """Performs requests to the Google Maps Directions API.""" +from __future__ import annotations +from typing import TYPE_CHECKING, List, Optional, Union, cast from googlemaps import convert - - -def directions(client, origin, destination, - mode=None, waypoints=None, alternatives=False, avoid=None, - language=None, units=None, region=None, departure_time=None, - arrival_time=None, optimize_waypoints=False, transit_mode=None, - transit_routing_preference=None, traffic_model=None): +from googlemaps.types import DictStrAny, Location, Timestamp, Unit, DirectionsMode, TransitMode, \ + TransitRoutingPreference, TrafficMode, DestinationAvoid + +if TYPE_CHECKING: + from googlemaps.client import Client + + +def directions( + client: Client, + origin: Location, + destination: Location, + mode: Optional[DirectionsMode] = None, + waypoints: Optional[List[Location]] = None, + alternatives: bool = False, + avoid: Optional[Union[DestinationAvoid, List[DestinationAvoid]]] = None, + language: Optional[str] = None, + units: Optional[Unit] = None, + region: Optional[str] = None, + departure_time: Optional[Timestamp] = None, + arrival_time: Optional[Timestamp] = None, + optimize_waypoints: bool = False, + transit_mode: Optional[TransitMode] = None, + transit_routing_preference: Optional[TransitRoutingPreference] = None, + traffic_model: Optional[TrafficMode] = None, +) -> List[DictStrAny]: """Get directions between an origin point and a destination point. :param origin: The address or latitude/longitude value from which you wish @@ -98,7 +118,7 @@ def directions(client, origin, destination, :rtype: list of routes """ - params = { + params: DictStrAny = { "origin": convert.latlng(origin), "destination": convert.latlng(destination) } @@ -111,16 +131,16 @@ def directions(client, origin, destination, params["mode"] = mode if waypoints: - waypoints = convert.location_list(waypoints) + waypoints_val = convert.location_list(waypoints) if optimize_waypoints: - waypoints = "optimize:true|" + waypoints - params["waypoints"] = waypoints + waypoints_val = "optimize:true|" + waypoints_val + params["waypoints"] = waypoints_val if alternatives: params["alternatives"] = "true" if avoid: - params["avoid"] = convert.join_list("|", avoid) + params["avoid"] = convert.join_list("|", cast(Union[str, List[str]], avoid)) if language: params["language"] = language diff --git a/googlemaps/distance_matrix.py b/googlemaps/distance_matrix.py index a30cbe09..e2ddf2be 100755 --- a/googlemaps/distance_matrix.py +++ b/googlemaps/distance_matrix.py @@ -16,14 +16,41 @@ # """Performs requests to the Google Maps Distance Matrix API.""" +from __future__ import annotations +from typing import List, TYPE_CHECKING, Union, Optional from googlemaps import convert - - -def distance_matrix(client, origins, destinations, - mode=None, language=None, avoid=None, units=None, - departure_time=None, arrival_time=None, transit_mode=None, - transit_routing_preference=None, traffic_model=None, region=None): +from googlemaps.types import ( + DictStrAny, + Location, + DirectionsMode, + DestinationAvoid, + Unit, + Timestamp, + TransitMode, + TrafficMode, + TransitRoutingPreference, +) + +if TYPE_CHECKING: + from googlemaps.client import Client + + +def distance_matrix( + client: Client, + origins: Union[Location, List[Location]], + destinations: Union[Location, List[Location]], + mode: Optional[DirectionsMode] = None, + language: Optional[str] = None, + avoid: Optional[DestinationAvoid] = None, + units: Optional[Unit] = None, + departure_time: Optional[Timestamp] = None, + arrival_time: Optional[Timestamp] = None, + transit_mode: Optional[TransitMode] = None, + transit_routing_preference: Optional[TransitRoutingPreference] = None, + traffic_model: Optional[TrafficMode] = None, + region: Optional[str] = None, +) -> List[DictStrAny]: """ Gets travel distance and time for a matrix of origins and destinations. :param origins: One or more addresses, Place IDs, and/or latitude/longitude @@ -136,4 +163,4 @@ def distance_matrix(client, origins, destinations, if region: params["region"] = region - return client._request("/maps/api/distancematrix/json", params) + return client._request("/maps/api/distancematrix/json", params) # type: ignore diff --git a/googlemaps/elevation.py b/googlemaps/elevation.py index 8eb6b14a..1a6ca8e4 100644 --- a/googlemaps/elevation.py +++ b/googlemaps/elevation.py @@ -16,11 +16,18 @@ # """Performs requests to the Google Maps Elevation API.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Union from googlemaps import convert +from googlemaps.types import Location, DictStrAny + +if TYPE_CHECKING: + from googlemaps.client import Client -def elevation(client, locations): +def elevation(client: Client, locations: List[Location]) -> List[DictStrAny]: """ Provides elevation data for locations provided on the surface of the earth, including depth locations on the ocean floor (which return negative @@ -37,7 +44,11 @@ def elevation(client, locations): return client._request("/maps/api/elevation/json", params).get("results", []) -def elevation_along_path(client, path, samples): +def elevation_along_path( + client: Client, + path: Union[str, List[Location]], + samples: int, +) -> List[DictStrAny]: """ Provides elevation data sampled along a path on the surface of the earth. @@ -52,7 +63,7 @@ def elevation_along_path(client, path, samples): :rtype: list of elevation data responses """ - if type(path) is str: + if isinstance(path, str): path = "enc:%s" % path else: path = convert.shortest_path(path) diff --git a/googlemaps/exceptions.py b/googlemaps/exceptions.py index 0a0f116a..fc8a0981 100644 --- a/googlemaps/exceptions.py +++ b/googlemaps/exceptions.py @@ -18,14 +18,16 @@ """ Defines exceptions that are thrown by the Google Maps client. """ +from typing import Optional, Union + class ApiError(Exception): """Represents an exception returned by the remote API.""" - def __init__(self, status, message=None): + def __init__(self, status: Union[int, str], message: Optional[str] = None) -> None: self.status = status self.message = message - def __str__(self): + def __str__(self) -> str: if self.message is None: return str(self.status) else: @@ -34,10 +36,10 @@ def __str__(self): class TransportError(Exception): """Something went wrong while trying to execute the request.""" - def __init__(self, base_exception=None): + def __init__(self, base_exception: Optional[BaseException] = None) -> None: self.base_exception = base_exception - def __str__(self): + def __str__(self) -> str: if self.base_exception: return str(self.base_exception) @@ -45,10 +47,10 @@ def __str__(self): class HTTPError(TransportError): """An unexpected HTTP error occurred.""" - def __init__(self, status_code): + def __init__(self, status_code: int) -> None: self.status_code = status_code - def __str__(self): + def __str__(self) -> str: return "HTTP Error: %d" % self.status_code class Timeout(Exception): diff --git a/googlemaps/geocoding.py b/googlemaps/geocoding.py index e409a49e..3c26f290 100644 --- a/googlemaps/geocoding.py +++ b/googlemaps/geocoding.py @@ -16,11 +16,26 @@ # """Performs requests to the Google Maps Geocoding API.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Union, List + from googlemaps import convert +from googlemaps.types import DictStrAny, Location + +if TYPE_CHECKING: + from googlemaps.client import Client -def geocode(client, address=None, place_id=None, components=None, bounds=None, region=None, - language=None): +def geocode( + client: Client, + address: Optional[str] = None, + place_id: Optional[str] = None, + components: Optional[DictStrAny] = None, + bounds: Optional[Union[str, DictStrAny]] = None, + region: Optional[str] = None, + language: Optional[str] = None, +) -> List[DictStrAny]: """ Geocoding is the process of converting addresses (like ``"1600 Amphitheatre Parkway, Mountain View, CA"``) into geographic @@ -52,7 +67,7 @@ def geocode(client, address=None, place_id=None, components=None, bounds=None, r :rtype: list of geocoding results. """ - params = {} + params: DictStrAny = {} if address: params["address"] = address @@ -75,8 +90,13 @@ def geocode(client, address=None, place_id=None, components=None, bounds=None, r return client._request("/maps/api/geocode/json", params).get("results", []) -def reverse_geocode(client, latlng, result_type=None, location_type=None, - language=None): +def reverse_geocode( + client: Client, + latlng: Location, + result_type: Optional[Union[str, List[str]]] = None, + location_type: Optional[List[str]] = None, + language: Optional[str] = None, +) -> List[DictStrAny]: """ Reverse geocoding is the process of converting geographic coordinates into a human-readable address. diff --git a/googlemaps/geolocation.py b/googlemaps/geolocation.py index c8db15ec..8ff6bc01 100644 --- a/googlemaps/geolocation.py +++ b/googlemaps/geolocation.py @@ -16,18 +16,28 @@ # """Performs requests to the Google Maps Geolocation API.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, List, cast + +import requests + from googlemaps import exceptions +from googlemaps.types import DictStrAny + +if TYPE_CHECKING: + from googlemaps.client import Client _GEOLOCATION_BASE_URL = "https://www.googleapis.com" -def _geolocation_extract(response): +def _geolocation_extract(response: requests.Response) -> DictStrAny: """ Mimics the exception handling logic in ``client._get_body``, but for geolocation which uses a different response format. """ - body = response.json() + body = cast(DictStrAny, response.json()) if response.status_code in (200, 404): return body @@ -42,9 +52,16 @@ def _geolocation_extract(response): raise exceptions.ApiError(response.status_code, error) -def geolocate(client, home_mobile_country_code=None, - home_mobile_network_code=None, radio_type=None, carrier=None, - consider_ip=None, cell_towers=None, wifi_access_points=None): +def geolocate( + client: Client, + home_mobile_country_code: Optional[str] = None, + home_mobile_network_code: Optional[str] = None, + radio_type: Optional[str] = None, + carrier: Optional[str] = None, + consider_ip: Optional[bool] = None, + cell_towers: Optional[List[DictStrAny]] = None, + wifi_access_points: Optional[List[DictStrAny]] = None, +) -> DictStrAny: """ The Google Maps Geolocation API returns a location and accuracy radius based on information about cell towers and WiFi nodes given. @@ -85,7 +102,7 @@ def geolocate(client, home_mobile_country_code=None, :type wifi_access_points: list of dicts """ - params = {} + params: DictStrAny = {} if home_mobile_country_code is not None: params["homeMobileCountryCode"] = home_mobile_country_code if home_mobile_network_code is not None: diff --git a/googlemaps/maps.py b/googlemaps/maps.py index 746223d6..6fc4cde9 100644 --- a/googlemaps/maps.py +++ b/googlemaps/maps.py @@ -16,8 +16,14 @@ # """Performs requests to the Google Maps Static API.""" +from __future__ import annotations +from typing import List, Optional, Iterator, TYPE_CHECKING, Union from googlemaps import convert +from googlemaps.types import Location, DictStrAny + +if TYPE_CHECKING: + from googlemaps.client import Client MAPS_IMAGE_FORMATS = {'png8', 'png', 'png32', 'gif', 'jpg', 'jpg-baseline'} @@ -27,10 +33,10 @@ class StaticMapParam: """Base class to handle parameters for Maps Static API.""" - def __init__(self): - self.params = [] + def __init__(self) -> None: + self.params: List[str] = [] - def __str__(self): + def __str__(self) -> str: """Converts a list of parameters to the format expected by the Google Maps server. @@ -43,8 +49,13 @@ def __str__(self): class StaticMapMarker(StaticMapParam): """Handles marker parameters for Maps Static API.""" - def __init__(self, locations, - size=None, color=None, label=None): + def __init__( + self, + locations: List[Location], + size: Optional[str] = None, + color: Optional[str] = None, + label: Optional[str] = None, + ) -> None: """ :param locations: Specifies the locations of the markers on the map. @@ -80,9 +91,14 @@ def __init__(self, locations, class StaticMapPath(StaticMapParam): """Handles path parameters for Maps Static API.""" - def __init__(self, points, - weight=None, color=None, - fillcolor=None, geodesic=None): + def __init__( + self, + points: List[Location], + weight: Optional[int] = None, + color: Optional[str] = None, + fillcolor: Optional[str] = None, + geodesic: Optional[str] = None, + ) -> None: """ :param points: Specifies the point through which the path will be built. @@ -122,10 +138,21 @@ def __init__(self, points, self.params.append(convert.location_list(points)) -def static_map(client, size, - center=None, zoom=None, scale=None, - format=None, maptype=None, language=None, region=None, - markers=None, path=None, visible=None, style=None): +def static_map( + client: Client, + size: Union[int, List[int]], + center: Optional[Location] = None, + zoom: Optional[int] = None, + scale: Optional[int] = None, + format: Optional[str] = None, + maptype: Optional[str] = None, + language: Optional[str] = None, + region: Optional[str] = None, + markers: Optional[StaticMapMarker] = None, + path: Optional[StaticMapPath] = None, + visible: Optional[List[Location]] = None, + style: Optional[List[DictStrAny]] = None, +) -> Iterator[bytes]: """ Downloads a map image from the Maps Static API. @@ -192,7 +219,7 @@ def static_map(client, size, f.close() """ - params = {"size": convert.size(size)} + params: DictStrAny = {"size": convert.size(size)} if not markers: if not (center or zoom is not None): @@ -244,4 +271,5 @@ def static_map(client, size, extract_body=lambda response: response, requests_kwargs={"stream": True}, ) - return response.iter_content() + + return response.iter_content() # type: ignore diff --git a/googlemaps/places.py b/googlemaps/places.py index 269a17fa..a7a3f4d2 100644 --- a/googlemaps/places.py +++ b/googlemaps/places.py @@ -16,9 +16,17 @@ # """Performs requests to the Google Places API.""" +from __future__ import annotations + import warnings +from typing import Optional, List, TYPE_CHECKING, Union, Iterator +from typing_extensions import Literal from googlemaps import convert +from googlemaps.types import DictStrAny, Location + +if TYPE_CHECKING: + from googlemaps.client import Client PLACES_FIND_FIELDS_BASIC = {"business_status", @@ -125,8 +133,13 @@ def find_place( - client, input, input_type, fields=None, location_bias=None, language=None -): + client: Client, + input: str, + input_type: Literal["textquery", "phonenumber"], + fields: Optional[List[str]] = None, + location_bias: Optional[str] = None, + language: Optional[str] = None, +) -> DictStrAny: """ A Find Place request takes a text input, and returns a place. The text input can be any kind of Places data, for example, @@ -196,18 +209,18 @@ def find_place( def places( - client, - query=None, - location=None, - radius=None, - language=None, - min_price=None, - max_price=None, - open_now=False, - type=None, - region=None, - page_token=None, -): + client: Client, + query: Optional[str] = None, + location: Optional[Location] = None, + radius: Optional[int] = None, + language: Optional[str] = None, + min_price: Optional[int] = None, + max_price: Optional[int] = None, + open_now: bool = False, + type: Optional[str] = None, + region: Optional[str] = None, + page_token: Optional[str] = None, +) -> DictStrAny: """ Places search. @@ -273,19 +286,19 @@ def places( def places_nearby( - client, - location=None, - radius=None, - keyword=None, - language=None, - min_price=None, - max_price=None, - name=None, - open_now=False, - rank_by=None, - type=None, - page_token=None, -): + client: Client, + location: Optional[Location] = None, + radius: Optional[int] = None, + keyword: Optional[str] = None, + language: Optional[str] = None, + min_price: Optional[int] = None, + max_price: Optional[int] = None, + name: Optional[Union[str, List[str]]] = None, + open_now: bool = False, + rank_by: Optional[str] = None, + type: Optional[str] = None, + page_token: Optional[str] = None, +) -> DictStrAny: """ Performs nearby search for places. @@ -375,28 +388,28 @@ def places_nearby( def _places( - client, - url_part, - query=None, - location=None, - radius=None, - keyword=None, - language=None, - min_price=0, - max_price=4, - name=None, - open_now=False, - rank_by=None, - type=None, - region=None, - page_token=None, -): + client: Client, + url_part: str, + query: Optional[str] = None, + location: Optional[Location] = None, + radius: Optional[int] = None, + keyword: Optional[str] = None, + language: Optional[str] = None, + min_price: Optional[int] = 0, + max_price: Optional[int] = 4, + name: Optional[Union[str, List[str]]] = None, + open_now: bool = False, + rank_by: Optional[str] = None, + type: Optional[str] = None, + region: Optional[str] = None, + page_token: Optional[str] = None, +) -> DictStrAny: """ Internal handler for ``places`` and ``places_nearby``. See each method's docs for arg details. """ - params = {"minprice": min_price, "maxprice": max_price} + params: DictStrAny = {"minprice": min_price, "maxprice": max_price} if query: params["query"] = query @@ -426,14 +439,14 @@ def _places( def place( - client, - place_id, - session_token=None, - fields=None, - language=None, - reviews_no_translations=False, - reviews_sort="most_relevant", -): + client: Client, + place_id: str, + session_token: Optional[str] = None, + fields: Optional[List[str]] = None, + language: Optional[str] = None, + reviews_no_translations: bool = False, + reviews_sort: str = "most_relevant", +) -> DictStrAny: """ Comprehensive details for an individual place. @@ -496,7 +509,12 @@ def place( return client._request("/maps/api/place/details/json", params) -def places_photo(client, photo_reference, max_width=None, max_height=None): +def places_photo( + client: Client, + photo_reference: str, + max_width: Optional[int] = None, + max_height: Optional[int] = None, +) -> Iterator[bytes]: """ Downloads a photo from the Places API. @@ -525,7 +543,7 @@ def places_photo(client, photo_reference, max_width=None, max_height=None): if not (max_width or max_height): raise ValueError("a max_width or max_height arg is required") - params = {"photoreference": photo_reference} + params: DictStrAny = {"photoreference": photo_reference} if max_width: params["maxwidth"] = max_width @@ -541,22 +559,22 @@ def places_photo(client, photo_reference, max_width=None, max_height=None): extract_body=lambda response: response, requests_kwargs={"stream": True}, ) - return response.iter_content() + return response.iter_content() # type: ignore def places_autocomplete( - client, - input_text, - session_token=None, - offset=None, - origin=None, - location=None, - radius=None, - language=None, - types=None, - components=None, - strict_bounds=False, -): + client: Client, + input_text: str, + session_token: Optional[str] = None, + offset: Optional[int] = None, + origin: Optional[Location] = None, + location: Optional[Location] = None, + radius: Optional[int] = None, + language: Optional[str] = None, + types: Optional[List[str]] = None, + components: Optional[DictStrAny] = None, + strict_bounds: bool = False, +) -> List[DictStrAny]: """ Returns Place predictions given a textual search string and optional geographic bounds. @@ -624,8 +642,13 @@ def places_autocomplete( def places_autocomplete_query( - client, input_text, offset=None, location=None, radius=None, language=None -): + client: Client, + input_text: str, + offset: Optional[int] = None, + location: Optional[Location] = None, + radius: Optional[int] = None, + language: Optional[str] = None, +) -> List[DictStrAny]: """ Returns Place predictions given a textual search query, such as "pizza near New York", and optional geographic bounds. @@ -662,25 +685,25 @@ def places_autocomplete_query( def _autocomplete( - client, - url_part, - input_text, - session_token=None, - offset=None, - origin=None, - location=None, - radius=None, - language=None, - types=None, - components=None, - strict_bounds=False, -): + client: Client, + url_part: str, + input_text: str, + session_token: Optional[str] = None, + offset: Optional[int] = None, + origin: Optional[Location] = None, + location: Optional[Location] = None, + radius: Optional[int] = None, + language: Optional[str] = None, + types: Optional[List[str]] = None, + components: Optional[DictStrAny] = None, + strict_bounds: bool = False, +) -> List[DictStrAny]: """ Internal handler for ``autocomplete`` and ``autocomplete_query``. See each method's docs for arg details. """ - params = {"input": input_text} + params: DictStrAny = {"input": input_text} if session_token: params["sessiontoken"] = session_token diff --git a/googlemaps/roads.py b/googlemaps/roads.py index edfb8ecb..17548514 100644 --- a/googlemaps/roads.py +++ b/googlemaps/roads.py @@ -16,15 +16,28 @@ # """Performs requests to the Google Maps Roads API.""" +from __future__ import annotations +from typing import TYPE_CHECKING, Dict, Any, Union, List, cast + +import requests import googlemaps + from googlemaps import convert +from googlemaps.types import Location, DictStrAny + +if TYPE_CHECKING: + from googlemaps.client import Client _ROADS_BASE_URL = "https://roads.googleapis.com" -def snap_to_roads(client, path, interpolate=False): +def snap_to_roads( + client: Client, + path: Union[Location, List[Location]], + interpolate: bool = False, +) -> List[DictStrAny]: """Snaps a path to the most likely roads travelled. Takes up to 100 GPS points collected along a route, and returns a similar @@ -45,7 +58,7 @@ def snap_to_roads(client, path, interpolate=False): :rtype: A list of snapped points. """ - params = {"path": convert.location_list(path)} + params: Dict[str, Any] = {"path": convert.location_list(path)} if interpolate: params["interpolate"] = "true" @@ -55,7 +68,10 @@ def snap_to_roads(client, path, interpolate=False): accepts_clientid=False, extract_body=_roads_extract).get("snappedPoints", []) -def nearest_roads(client, points): +def nearest_roads( + client: Client, + points: Union[Location, List[Location]], +) -> List[DictStrAny]: """Find the closest road segments for each point Takes up to 100 independent coordinates, and returns the closest road @@ -77,7 +93,11 @@ def nearest_roads(client, points): accepts_clientid=False, extract_body=_roads_extract).get("snappedPoints", []) -def speed_limits(client, place_ids): + +def speed_limits( + client:Client, + place_ids: Union[str, List[str]], +) -> List[DictStrAny]: """Returns the posted speed limit (in km/h) for given road segments. :param place_ids: The Place ID of the road segment. Place IDs are returned @@ -95,7 +115,10 @@ def speed_limits(client, place_ids): extract_body=_roads_extract).get("speedLimits", []) -def snapped_speed_limits(client, path): +def snapped_speed_limits( + client: Client, + path: Union[Location, List[Location]], +) -> DictStrAny: """Returns the posted speed limit (in km/h) for given road segments. The provided points will first be snapped to the most likely roads the @@ -116,11 +139,11 @@ def snapped_speed_limits(client, path): extract_body=_roads_extract) -def _roads_extract(resp): +def _roads_extract(resp: requests.Response) -> DictStrAny: """Extracts a result from a Roads API HTTP response.""" try: - j = resp.json() + j = cast(DictStrAny, resp.json()) except: if resp.status_code != 200: raise googlemaps.exceptions.HTTPError(resp.status_code) diff --git a/googlemaps/timezone.py b/googlemaps/timezone.py index 0b6370dc..cebc5029 100644 --- a/googlemaps/timezone.py +++ b/googlemaps/timezone.py @@ -16,13 +16,26 @@ # """Performs requests to the Google Maps Directions API.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional from googlemaps import convert +from googlemaps.types import Location, Timestamp, DictStrAny from datetime import datetime -def timezone(client, location, timestamp=None, language=None): +if TYPE_CHECKING: + from googlemaps.client import Client + + +def timezone( + client: Client, + location: Location, + timestamp: Optional[Timestamp] = None, + language: Optional[str] = None, +) -> DictStrAny: """Get time zone for a location on the earth, as well as that location's time offset from UTC. diff --git a/googlemaps/types.py b/googlemaps/types.py new file mode 100644 index 00000000..7aa1d34d --- /dev/null +++ b/googlemaps/types.py @@ -0,0 +1,28 @@ +from datetime import datetime +from typing import Union, List, Tuple, Dict, Any + +from typing_extensions import TypeAlias, TypedDict, Literal + + +class LocationDict(TypedDict): + lat: float + lng: float + + +Location: TypeAlias = Union[ + str, + List[float], + Tuple[float, float], + LocationDict, +] + +Timestamp: TypeAlias = Union[int, datetime] +DictStrAny: TypeAlias = Dict[str, Any] + +Unit = Literal["metric", "imperial"] + +TrafficMode: TypeAlias = Literal["best_guess", "optimistic", "pessimistic"] +TransitRoutingPreference: TypeAlias = Literal["less_walking", "fewer_transfers"] +TransitMode: TypeAlias = Literal["bus", "subway", "train", "tram", "rail"] +DirectionsMode: TypeAlias = Literal["driving", "walking", "bicycling", "transit"] +DestinationAvoid = Literal["tolls", "highways", "ferries"] diff --git a/noxfile.py b/noxfile.py index e5b92961..a9063a10 100644 --- a/noxfile.py +++ b/noxfile.py @@ -14,7 +14,7 @@ import nox -SUPPORTED_PY_VERSIONS = ["3.7", "3.8", "3.9", "3.10"] +SUPPORTED_PY_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11"] def _install_dev_packages(session): @@ -31,6 +31,11 @@ def _install_doc_dependencies(session): session.install("sphinx") +def _install_mypy_dependencies(session): + session.install("mypy") + session.install("types-requests") + + @nox.session(python=SUPPORTED_PY_VERSIONS) def tests(session): _install_dev_packages(session) @@ -42,6 +47,13 @@ def tests(session): session.notify("cover") +@nox.session(python=SUPPORTED_PY_VERSIONS) +def mypy(session): + _install_dev_packages(session) + _install_mypy_dependencies(session) + session.run("mypy", "googlemaps") + + @nox.session def cover(session): """Coverage analysis.""" diff --git a/setup.cfg b/setup.cfg index 56320971..5af4f123 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,4 +9,10 @@ omit = exclude_lines = pragma: no cover def __repr__ - raise NotImplementedError \ No newline at end of file + raise NotImplementedError + +[mypy] +python_version = 3.7 +show_column_numbers = true +follow_imports = normal +strict = true diff --git a/setup.py b/setup.py index df9f32f1..fcac15d0 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,10 @@ from setuptools import setup -requirements = ["requests>=2.20.0,<3.0"] +requirements = [ + "requests>=2.20.0,<3.0", + "typing_extensions>=4.5.0,<5.0", +] with open("README.md") as f: readme = f.read() @@ -48,5 +51,5 @@ "Programming Language :: Python :: 3.8", "Topic :: Internet", ], - python_requires='>=3.5' + python_requires='>=3.7' )