usse/funda-scraper/venv/lib/python3.10/site-packages/osrm.py

360 lines
10 KiB
Python

import asyncio
import collections.abc
import enum
import logging
import numbers
import random
try:
import ujson as json
except ImportError:
import json
from urllib.parse import urlencode
logger = logging.getLogger(__name__)
try:
import aiohttp
except ImportError:
aiohttp = None
try:
import requests
except ImportError:
requests = None
if not (aiohttp or requests):
logger.error('Could not import none of modules \'aiohttp\' or \'requests\'')
class overview(enum.Enum):
simplified = 'simplified'
full = 'full'
false = 'false'
# alias for avoiding name collision
osrm_overview = overview
class geometries(enum.Enum):
polyline = 'polyline'
polyline6 = 'polyline6'
geojson = 'geojson'
# alias for avoiding name collision
osrm_geometries = geometries
class gaps(enum.Enum):
split = 'split'
ignore = 'ignore'
# alias for avoiding name collision
osrm_gaps = gaps
class continue_straight(enum.Enum):
default = 'default'
true = 'true'
false = 'false'
# alias for avoiding name collision
osrm_continue_straight = continue_straight
class OSRMException(Exception):
pass
class OSRMServerException(OSRMException):
pass
class OSRMClientException(OSRMException):
pass
def _check_pairs(items):
''' checking that 'items' has format [[Number, Number], ...]'''
return (
isinstance(items, collections.abc.Iterable) and
all([isinstance(p, collections.abc.Iterable) for p in items]) and
all([
isinstance(p[0], numbers.Number) and
isinstance(p[1], numbers.Number) and
len(p) == 2
for p in items]))
class BaseRequest:
def __init__(self, coordinates, radiuses=[], bearings=[], hints=[]):
assert _check_pairs(coordinates), \
'''coordinates must be in format [[longitude, latitude],...]'''
assert all([
-180 <= lon <= 180 and -90 <= lat <= 90
for lon, lat in coordinates]), \
''''longitude' should be -180..180 and 'latitude' should be -90..90 (actual: {})'''.format(coordinates)
assert _check_pairs(bearings), \
'''bearings must be in format [[value, range],...]'''
assert all([
0 <= bvalue <= 360 and 0 <= brange <= 180
for bvalue, brange in bearings]), \
'''bearing 'value' should be 0..360 and 'range' should be 0..180 (actual: {})'''.format(bearings)
assert isinstance(radiuses, list)
assert isinstance(bearings, list)
assert isinstance(hints, list)
self.coordinates = coordinates
self.radiuses = radiuses
self.bearings = bearings
self.hints = hints
def get_coordinates(self):
return self._encode_pairs(self.coordinates)
def get_options(self):
return {
'radiuses': self._encode_array(self.radiuses),
'bearings': self._encode_pairs(self.bearings),
'hints': self._encode_array(self.hints)
}
def _encode_array(self, value):
return ';'.join(map(lambda x: str(x) if x else "", value))
def _encode_bool(self, value):
return 'true' if value else 'false'
def _encode_pairs(self, coordinates):
return ';'.join([','.join(map(str, coord)) for coord in coordinates])
def decode_response(self, url, status, response):
if status == 200:
return json.loads(response)
elif status == 400:
raise OSRMClientException(json.loads(response))
raise OSRMServerException(url, response)
class NearestRequest(BaseRequest):
service = 'nearest'
def __init__(self, number=1, **kwargs):
super().__init__(**kwargs)
self.number = number
def get_options(self):
options = super().get_options()
options['number'] = self.number
return options
class RouteRequest(BaseRequest):
service = 'route'
def __init__(
self,
alternatives=False,
steps=False, annotations=False,
geometries=geometries.geojson,
overview=overview.simplified,
continue_straight=continue_straight.default,
**kwargs):
super().__init__(**kwargs)
assert isinstance(alternatives, bool)
assert isinstance(steps, bool)
assert isinstance(annotations, bool)
assert isinstance(geometries, osrm_geometries)
assert isinstance(overview, osrm_overview)
assert isinstance(continue_straight, osrm_continue_straight)
self.alternatives = alternatives
self.steps = steps
self.annotations = annotations
self.geometries = geometries
self.overview = overview
self.continue_straight = continue_straight
def get_options(self):
options = super().get_options()
options.update({
'alternatives': self._encode_bool(self.alternatives),
'steps': self._encode_bool(self.steps),
'annotations': self._encode_bool(self.annotations),
'geometries': self.geometries.value,
'overview': self.overview.value
})
if self.continue_straight != continue_straight.default:
options['continue_straight'] = self.continue_straight.value
return options
class MatchRequest(RouteRequest):
service = 'match'
def __init__(
self,
timestamps=[],
gaps=gaps.split,
tidy=False,
**kwargs):
super().__init__(**kwargs)
assert isinstance(timestamps, list)
assert isinstance(gaps, osrm_gaps)
assert isinstance(tidy, bool)
self.timestamps = timestamps
self.gaps = gaps
self.tidy = tidy
def get_options(self):
options = super().get_options()
options.pop('alternatives', None)
options['timestamps'] = self._encode_array(self.timestamps)
# Don't send default values (for compatibility with 5.6)
if self.gaps.value != osrm_gaps.split:
options['gaps'] = self.gaps.value
if self.tidy:
options['tidy'] = self._encode_bool(self.tidy)
return options
class BaseClient:
def __init__(
self,
host='http://localhost:5000',
version='v1', profile='driving',
timeout=5, max_retries=5):
assert isinstance(host, str)
assert isinstance(version, str)
assert isinstance(profile, str)
assert isinstance(timeout, numbers.Number)
assert isinstance(max_retries, int) and max_retries >= 1
self.host = host
self.version = version
self.profile = profile
self.timeout = timeout
self.max_retries = max_retries
def _build_request(self, request):
url = '{host}/{service}/{version}/{profile}/{coordinates}'.format(
host=self.host,
service=request.service,
version=self.version,
profile=self.profile,
coordinates=request.get_coordinates())
params = {
k: v
for k, v in request.get_options().items()
if v
}
logger.debug('request url=%s; params=%s', url, params)
return (url, params)
class Client(BaseClient):
def __init__(self, *args, session=None, **kwargs):
super().__init__(*args, **kwargs)
if not requests:
raise RuntimeError('Module \'requests\' is not available')
self.session = session or requests.Session()
self.a = requests.adapters.HTTPAdapter(max_retries=self.max_retries)
self.session.mount('http://', self.a)
def nearest(self, **kwargs):
return self._request(
NearestRequest(**kwargs)
)
def route(self, **kwargs):
return self._request(
RouteRequest(**kwargs)
)
def match(self, **kwargs):
return self._request(
MatchRequest(**kwargs)
)
def _request(self, request):
if not requests:
raise RuntimeError('Module \'requests\' is not available')
url, params = self._build_request(request)
response = self.session.get(url, params=params, timeout=self.timeout)
return request.decode_response(url, response.status_code, response.text)
class AioHTTPClient(BaseClient):
BACKOFF_MAX = 120
BACKOFF_FACTOR = 0.5
def __init__(self, *args, session=None, loop=None, **kwargs):
super().__init__(*args, **kwargs)
if not aiohttp:
raise RuntimeError('Module \'aiohttp\' is not available')
if not session:
self.loop = loop or asyncio.get_event_loop()
self.session = aiohttp.ClientSession(loop=self.loop)
else:
self.session = session
async def nearest(self, **kwargs):
return await self._request(
NearestRequest(**kwargs)
)
async def route(self, **kwargs):
return await self._request(
RouteRequest(**kwargs)
)
async def match(self, **kwargs):
return await self._request(
MatchRequest(**kwargs)
)
def exp_backoff(self, attempt):
timeout = min(self.timeout * (2 ** attempt), self.BACKOFF_MAX)
return timeout + random.uniform(0, self.BACKOFF_FACTOR * timeout)
async def _request(self, request):
url, params = self._build_request(request)
attempt = 0
while attempt < self.max_retries:
try:
# This is a workaround for the https://github.com/aio-libs/aiohttp/issues/1901
request_url = "{}?{}".format(url, urlencode(params))
async with self.session.get(
request_url, timeout=self.timeout) as response:
body = await response.text()
return request.decode_response(
response.url, response.status, body)
except asyncio.TimeoutError:
timeout = self.exp_backoff(attempt)
logger.info(
'Timeout error url=%s (remaining tries %s, sleeping %.2f secs)',
url, self.max_retries - attempt, timeout)
await asyncio.sleep(timeout)
attempt += 1
raise OSRMServerException(url, 'server timeout')
async def close(self):
await self.session.close()