1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
|
import datetime
import itertools as it
import logging
import requests
import time
from . import errors
logger = logging.getLogger(__name__)
class OsmApiSession:
MAX_RETRY_LIMIT = 5
"""Maximum retries if a call to the remote API fails (default: 5)"""
def __init__(self, base_url, created_by, auth=None, session=None, timeout=30):
self._api = base_url
self._created_by = created_by
self._timeout = timeout
try:
self._auth = auth
if not auth and session.auth:
self._auth = session.auth
except AttributeError:
pass
self._http_session = session
self._session = self._get_http_session()
def close(self):
if self._session:
self._session.close()
def _http_request(self, method, path, auth, send, return_value=True): # noqa
"""
Returns the response generated by an HTTP request.
`method` is a HTTP method to be executed
with the request data. For example: 'GET' or 'POST'.
`path` is the path to the requested resource relative to the
base API address stored in self._api. Should start with a
slash character to separate the URL.
`auth` is a boolean indicating whether authentication should
be preformed on this request.
`send` contains additional data that might be sent in a
request.
`return_value` indicates wheter this request should return
any data or not.
If the username or password is missing,
`OsmApi.UsernamePasswordMissingError` is raised.
If the requested element has been deleted,
`OsmApi.ElementDeletedApiError` is raised.
If the requested element can not be found,
`OsmApi.ElementNotFoundApiError` is raised.
If the response status code indicates an error,
`OsmApi.ApiError` is raised.
"""
logger.debug(f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S} {method} {path}")
# Add API base URL to path
path = self._api + path
if auth and not self._auth:
raise errors.UsernamePasswordMissingError("Username/Password missing")
try:
response = self._session.request(
method, path, data=send, timeout=self._timeout
)
except requests.exceptions.Timeout as e:
raise errors.TimeoutApiError(
0, f"Request timed out (timeout={self._timeout})", ""
) from e
except requests.exceptions.ConnectionError as e:
raise errors.ConnectionApiError(0, f"Connection error: {str(e)}", "") from e
except requests.exceptions.RequestException as e:
raise errors.ApiError(0, str(e), "") from e
if response.status_code != 200:
payload = response.content.strip()
if response.status_code == 401:
raise errors.UnauthorizedApiError(
response.status_code, response.reason, payload
)
if response.status_code == 404:
raise errors.ElementNotFoundApiError(
response.status_code, response.reason, payload
)
elif response.status_code == 410:
raise errors.ElementDeletedApiError(
response.status_code, response.reason, payload
)
raise errors.ApiError(response.status_code, response.reason, payload)
if return_value and not response.content:
raise errors.ResponseEmptyApiError(
response.status_code, response.reason, ""
)
logger.debug(f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S} {method} {path}")
return response.content
def _http(self, cmd, path, auth, send, return_value=True): # noqa
for i in it.count(1):
try:
return self._http_request(
cmd, path, auth, send, return_value=return_value
)
except errors.ApiError as e:
if e.status >= 500:
if i == self.MAX_RETRY_LIMIT:
raise
if i != 1:
self._sleep()
self._session = self._get_http_session()
else:
logger.exception("ApiError Exception occured")
raise
except errors.UsernamePasswordMissingError:
raise
except Exception as e:
logger.exception("General exception occured")
if i == self.MAX_RETRY_LIMIT:
if isinstance(e, errors.OsmApiError):
raise
raise errors.MaximumRetryLimitReachedError(
f"Give up after {i} retries"
) from e
if i != 1:
self._sleep()
self._session = self._get_http_session()
def _get_http_session(self):
"""
Creates a requests session for connection pooling.
"""
if self._http_session:
session = self._http_session
else:
session = requests.Session()
session.auth = self._auth
session.headers.update({"user-agent": self._created_by})
return session
def _sleep(self):
time.sleep(5)
def _get(self, path):
return self._http("GET", path, False, None)
def _put(self, path, data, return_value=True):
return self._http("PUT", path, True, data, return_value=return_value)
def _post(self, path, data, optionalAuth=False, forceAuth=False):
# the Notes API allows certain POSTs by non-authenticated users
auth = optionalAuth and self._auth
if forceAuth:
auth = True
return self._http("POST", path, auth, data)
def _delete(self, path, data):
return self._http("DELETE", path, True, data)
|