File: http.py

package info (click to toggle)
python-osmapi 4.3.0%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 820 kB
  • sloc: python: 3,409; xml: 1,601; makefile: 45; sh: 14
file content (168 lines) | stat: -rw-r--r-- 6,011 bytes parent folder | download
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)