Skip to content

Commit

Permalink
Ors v5 (#34)
Browse files Browse the repository at this point in the history
* update validator with new restrictions

* All necessary changes implemented

* added self.req as a property
  • Loading branch information
nilsnolde authored and nilsnolde committed Mar 14, 2019
1 parent ec75b92 commit 2586cd3
Show file tree
Hide file tree
Showing 23 changed files with 1,655 additions and 1,085 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG
@@ -1,3 +1,10 @@
# 2.0.0

- implement all backend changes from moving to openrouteservice v5
- now all parameters are named like their backend equivalents, while keeping the old ors-py parameter names for backwards compatibility, but with deprecation warnings
- validator validates ALL parameters
- added a Client.req property, returning the actual `requests` request

### v1.1.8

- make dependencies more sensible (#32)
Expand Down
2 changes: 1 addition & 1 deletion openrouteservice/__init__.py
Expand Up @@ -17,7 +17,7 @@
# the License.
#

__version__ = "1.1.7"
__version__ = "2.0-dev"

# Make sure QGIS plugin can import openrouteservice-py

Expand Down
158 changes: 76 additions & 82 deletions openrouteservice/client.py
Expand Up @@ -25,9 +25,10 @@
from datetime import timedelta
import functools
import requests
import json
import random
import time
import collections
import warnings

from openrouteservice import exceptions, __version__

Expand All @@ -45,15 +46,15 @@
class Client(object):
"""Performs requests to the ORS API services."""

def __init__(self, key=None,
base_url=_DEFAULT_BASE_URL,
def __init__(self,
key=None,
base_url=_DEFAULT_BASE_URL,
timeout=60,
retry_timeout=60,
requests_kwargs=None,
queries_per_minute=40,
retry_over_query_limit=False):
retry_over_query_limit=True):
"""
:param key: ORS API key. Required.
:param key: ORS API key.
:type key: string
:param base_url: The base URL for the request. Defaults to the ORS API
Expand All @@ -68,52 +69,59 @@ def __init__(self, key=None,
seconds.
:type retry_timeout: int
:param requests_kwargs: Extra keyword arguments for the requests
library, which among other things allow for proxy auth to be
implemented. See the official requests docs for more info:
http://docs.python-requests.org/en/latest/api/#main-interface
:type requests_kwargs: dict
:param queries_per_minute: Number of queries per second permitted.
If the rate limit is reached, the client will sleep for the
appropriate amount of time before it runs the current query.
Note, it won't help to initiate another client. This saves you the
trouble of raised exceptions.
:type queries_per_second: int
:param requests_kwargs: Extra keyword arguments for the requests
library, which among other things allow for proxy auth to be
implemented. See the official requests docs for more info:
http://docs.python-requests.org/en/latest/api/#main-interface
:type requests_kwargs: dict
:param retry_over_query_limit: If True, the client will retry when query
limit is reached (HTTP 429). Default False.
"""

self.session = requests.Session()
self.key = key
self.base_url = base_url

self.timeout = timeout
self.retry_over_query_limit = retry_over_query_limit
self.retry_timeout = timedelta(seconds=retry_timeout)
self.requests_kwargs = requests_kwargs or {}
self.requests_kwargs.update({
self._session = requests.Session()
self._key = key
self._base_url = base_url

if self._base_url == _DEFAULT_BASE_URL and key is None:
raise ValueError("No API key was specified. Please visit https://openrouteservice.org/sign-up to create one.")

self._timeout = timeout
self._retry_over_query_limit = retry_over_query_limit
self._retry_timeout = timedelta(seconds=retry_timeout)
self._requests_kwargs = requests_kwargs or {}
self._requests_kwargs.update({
"headers": {"User-Agent": _USER_AGENT,
'Content-type': 'application/json'},
"timeout": self.timeout
'Content-type': 'application/json',
"Authorization": self._key},
"timeout": self._timeout,
})

self.queries_per_minute = queries_per_minute
self.sent_times = collections.deque("", queries_per_minute)
self._req = None

def request(self,
url, params,
first_request_time=None,
retry_counter=0,
requests_kwargs=None,
post_json=None,
dry_run=None):
def request(self,
url,
get_params=None,
first_request_time=None,
retry_counter=0,
requests_kwargs=None,
post_json=None,
dry_run=None):
"""Performs HTTP GET/POST with credentials, returning the body as
JSON.
:param url: URL path for the request. Should begin with a slash.
:type url: string
:param params: HTTP GET parameters.
:type params: dict or list of key/value tuples
:param get_params: HTTP GET parameters.
:type get_params: dict or list of key/value tuples
:param first_request_time: The time of the first request (None if no
retries have occurred).
Expand All @@ -135,8 +143,6 @@ def request(self,
:raises ApiError: when the API returns an error.
:raises Timeout: if the request timed out.
:raises TransportError: when something went wrong while trying to
execute a request.
:rtype: dict from JSON response.
"""
Expand All @@ -145,7 +151,7 @@ def request(self,
first_request_time = datetime.now()

elapsed = datetime.now() - first_request_time
if elapsed > self.retry_timeout:
if elapsed > self._retry_timeout:
raise exceptions.Timeout()

if retry_counter > 0:
Expand All @@ -158,69 +164,64 @@ def request(self,
time.sleep(delay_seconds * (random.random() + 0.5))

authed_url = self._generate_auth_url(url,
params,
get_params,
)

# Default to the client-level self.requests_kwargs, with method-level
# requests_kwargs arg overriding.
requests_kwargs = requests_kwargs or {}
final_requests_kwargs = dict(self.requests_kwargs, **requests_kwargs)

# Check if the time of the nth previous query (where n is
# queries_per_second) is under a second ago - if so, sleep for
# the difference.
if self.sent_times and len(self.sent_times) == self.queries_per_minute:
elapsed_since_earliest = time.time() - self.sent_times[0]
if elapsed_since_earliest < 60:
print("Request limit of {} per minute exceeded. Wait for {} seconds".format(self.queries_per_minute,
60 - elapsed_since_earliest))
time.sleep(60 - elapsed_since_earliest)
final_requests_kwargs = dict(self._requests_kwargs, **requests_kwargs)

# Determine GET/POST.
# post_json is so far only sent from matrix call
requests_method = self.session.get
requests_method = self._session.get

if post_json is not None:
requests_method = self.session.post
requests_method = self._session.post
final_requests_kwargs["json"] = post_json

# Only print URL and parameters for dry_run
if dry_run:
print("url:\n{}\nParameters:\n{}".format(self.base_url+authed_url,
final_requests_kwargs))
print("url:\n{}\nHeaders:\n{}".format(self._base_url + authed_url,
json.dumps(final_requests_kwargs, indent=2)))
return

try:
response = requests_method(self.base_url + authed_url,
response = requests_method(self._base_url + authed_url,
**final_requests_kwargs)
self._req = response.request

except requests.exceptions.Timeout:
raise exceptions.Timeout()
except Exception as e:
raise exceptions.TransportError(e)

if response.status_code in _RETRIABLE_STATUSES:
# Retry request.
print('Server down.\nRetrying for the {}th time.'.format(retry_counter + 1))
warnings.warn('Server down.\nRetrying for the {}th time.'.format(retry_counter + 1),
UserWarning,
stacklevel=1)

return self.request(url, params, first_request_time,
retry_counter + 1, requests_kwargs, post_json)
return self.request(url, get_params, first_request_time,
retry_counter + 1, requests_kwargs, post_json)

try:
result = self._get_body(response)
self.sent_times.append(time.time())

return result
except exceptions._RetriableRequest as e:
if isinstance(e, exceptions._OverQueryLimit) and not self.retry_over_query_limit:
if isinstance(e, exceptions._OverQueryLimit) and not self._retry_over_query_limit:
raise

print('Rate limit exceeded.\nRetrying for the {}th time.'.format(retry_counter + 1))
warnings.warn('Rate limit exceeded.\nRetrying for the {}th time.'.format(retry_counter + 1),
UserWarning,
stacklevel=1)
# Retry request.
return self.request(url, params, first_request_time,
retry_counter + 1, requests_kwargs,
post_json)
except:
raise
return self.request(url, get_params, first_request_time,
retry_counter + 1, requests_kwargs,
post_json)

@property
def req(self):
"""Returns request object. Can be used in case of request failure."""
return self._req

def _get_body(self, response):
body = response.json()
Expand All @@ -229,14 +230,17 @@ def _get_body(self, response):

if status_code == 429:
raise exceptions._OverQueryLimit(
str(status_code), body)
status_code,
body
)
if status_code != 200:
raise exceptions.ApiError(status_code,
body)
raise exceptions.ApiError(
status_code,
body
)

return body


def _generate_auth_url(self, path, params):
"""Returns the path and query string portion of the request URL, first
adding any necessary parameters.
Expand All @@ -254,17 +258,7 @@ def _generate_auth_url(self, path, params):
if type(params) is dict:
params = sorted(dict(**params).items())

# Only auto-add API key when using ORS. If own instance, API key must
# be explicitly added to params
if self.key:
params.append(("api_key", self.key))
return path + "?" + _urlencode_params(params)
elif self.base_url != _DEFAULT_BASE_URL:
return path + "?" + _urlencode_params(params)

raise ValueError("No API key specified. "
"Visit https://go.openrouteservice.org/dev-dashboard/ "
"to create one.")
return path + "?" + _urlencode_params(params)


from openrouteservice.directions import directions
Expand Down
2 changes: 1 addition & 1 deletion openrouteservice/convert.py
Expand Up @@ -129,7 +129,7 @@ def _has_method(arg, method):


def decode_polyline(polyline, is3d=False):
"""Decodes a Polyline string into a GeoJSON structure.
"""Decodes a Polyline string into a GeoJSON geometry.
:param polyline: An encoded polyline, only the geometry.
:type polyline: string
Expand Down
23 changes: 23 additions & 0 deletions openrouteservice/deprecation.py
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2018 HeiGIT, University of Heidelberg.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#

import warnings

def warning(old_name, new_name):
warnings.warn('{} will be deprecated in v2.0. Please use {} instead'.format(old_name, new_name),
DeprecationWarning,
stacklevel=2)

0 comments on commit 2586cd3

Please sign in to comment.