cs-service-api 20260531__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: cs-service_api
3
+ Version: 20260531
4
+ Summary: ServiceAPI, a base class for APIs which talk to a service, typically a web service via HTTP.
5
+ Keywords: python3
6
+ Author-email: Cameron Simpson <cs@cskk.id.au>
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Requires-Dist: cs.deco>=20260525
16
+ Requires-Dist: cs.fstags>=20260531
17
+ Requires-Dist: cs.logutils>=20250323
18
+ Requires-Dist: cs.pfx>=20250914
19
+ Requires-Dist: cs.resources>=20250915
20
+ Requires-Dist: cs.sqltags>=20260531
21
+ Requires-Dist: cs.tagset>=20260531
22
+ Requires-Dist: cs.upd>=20260526
23
+ Requires-Dist: icontract
24
+ Requires-Dist: requests
25
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
26
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
27
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
28
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/service_api.py
29
+
30
+ ServiceAPI, a base class for APIs which talk to a service,
31
+ typically a web service via HTTP.
32
+
33
+ *Latest release 20260531*:
34
+ * HTTPServiceAPI: add __truediv__(suburl) which does a GET.
35
+ * HTTPServiceAPI: default .API_HOSTNAME comes from the subclass.
36
+ * HTTPServiceAPI.json: catch requests.exceptions.JSONDecodeError also.
37
+ * HTTPServiceAPI: new mode ("response", "json", "data", callable) parameter.
38
+ * HTTPServiceAPI: new check_json optional dict of expected fields and values in JSON responses.
39
+
40
+ An instance of a `ServiceAPI` embodies some basic features
41
+ that feel common to web based services:
42
+ - a notion of a login
43
+ - local state, an `SQLTags` for data about entities of the service
44
+ - downloads, if that is a thing, with `FSTags` for file annotations
45
+
46
+ Short summary:
47
+ * `HTTPServiceAPI`: `HTTPServiceAPI` base class for other APIs talking to HTTP services.
48
+ * `RequestsNoAuth`: This is a special purpose subclass of `requests.auth.AuthBase` to apply no authorisation at all. This is for services with their own special purpose authorisation and avoids things like automatic netrc based auth.
49
+ * `ServiceAPI`: `SewrviceAPI` base class for other APIs talking to services.
50
+
51
+ Module contents:
52
+ - <a name="HTTPServiceAPI"></a>`class HTTPServiceAPI(ServiceAPI)`: `HTTPServiceAPI` base class for other APIs talking to HTTP services.
53
+
54
+ Subclasses must define:
55
+ * `API_BASE`: the base URL of API calls.
56
+ For example, the `PlayOnAPI` defines this as `f'https://{API_HOSTNAME}/v3/'`.
57
+
58
+ *`HTTPServiceAPI.get(self, suburl, **kw) -> requests.models.Response`*:
59
+ Call `slef.suburl` with `method="GET"`.
60
+
61
+ *`HTTPServiceAPI.post(self, suburl, **kw) -> requests.models.Response`*:
62
+ Call `slef.suburl` with `method="POST"`.
63
+
64
+ *`HTTPServiceAPI.response_as_json(self, rsp: requests.models.Response) -> dict`*:
65
+ Return `rsp.json()`.
66
+
67
+ *`HTTPServiceAPI.response_as_json_data(self, rsp: requests.models.Response) -> dict`*:
68
+ Return the `"data"` element from a JSON response.
69
+
70
+ *`HTTPServiceAPI.suburl(self, suburl, *, base_url=None, method='GET', mode=None, check=True, cookies=None, headers=None, runstate: Optional[cs.resources.RunState] = <function uses_runstate.<locals>.<lambda> at 0x111844fe0>, verbose: bool, **rqkw) -> Union[requests.models.Response, dict]`*:
71
+ Request `suburl` from the service, by default using a `GET`.
72
+ The `suburl` must be a URL subpath not commencing with `'/'`.
73
+
74
+ Return:
75
+ - `mode(Response)` if `mode` is callable
76
+ - the `Response` if `mode=="response"`
77
+ - the `Response.json()` if `mode=="json"`
78
+ - the `Response.json()["data"]` if `mode=="data"`
79
+
80
+ Keyword parameters:
81
+ * `base_url`: the base request domain, default from `self.API_BASE`
82
+ * `method`: optional request method, default `'GET'`
83
+ * `check`: if true, raise an HTTP error if the response
84
+ status is not 200; default `True`
85
+ * `cookies`: optional cookie jar, default from `self.cookies`
86
+ * `mode`: optional result mode, default from `self.mode`
87
+ Other keyword parameters are passed to the requests method.
88
+ - <a name="RequestsNoAuth"></a>`class RequestsNoAuth(requests.auth.AuthBase)`: This is a special purpose subclass of `requests.auth.AuthBase`
89
+ to apply no authorisation at all.
90
+ This is for services with their own special purpose authorisation
91
+ and avoids things like automatic netrc based auth.
92
+ - <a name="ServiceAPI"></a>`class ServiceAPI(cs.resources.MultiOpenMixin, cs.sqltags.UsesSQLTags)`: `SewrviceAPI` base class for other APIs talking to services.
93
+
94
+ *`ServiceAPI.available(self) -> Set[cs.sqltags.SQLTagSet]`*:
95
+ Return a set of the `SQLTagSet` instances representing available
96
+ items at the service, for example purchased books
97
+ available to your login.
98
+
99
+ *`ServiceAPI.get_login_state(self, do_refresh=False) -> cs.tagset.HasTags`*:
100
+ The login state, a `HasTags`, stored as `login.state.`*login_userid*.
101
+ This performs a login if necessary or if `do_refresh` is true
102
+ (default `False`).
103
+
104
+ *`ServiceAPI.login(self) -> Mapping`*:
105
+ Do a login: authenticate to the service, return a mapping of related information.
106
+
107
+ Not all services require this and we expect such subclasses
108
+ to avoid use of login-based methods.
109
+
110
+ *`ServiceAPI.login_expiry`*:
111
+ Expiry UNIX time for the login state.
112
+ This implementation returns `None`.
113
+
114
+ *`ServiceAPI.startup_shutdown(self)`*:
115
+ Open/close the `FSTags` and `UsesTagSets`.
116
+
117
+ # Release Log
118
+
119
+
120
+
121
+ *Release 20260531*:
122
+ * HTTPServiceAPI: add __truediv__(suburl) which does a GET.
123
+ * HTTPServiceAPI: default .API_HOSTNAME comes from the subclass.
124
+ * HTTPServiceAPI.json: catch requests.exceptions.JSONDecodeError also.
125
+ * HTTPServiceAPI: new mode ("response", "json", "data", callable) parameter.
126
+ * HTTPServiceAPI: new check_json optional dict of expected fields and values in JSON responses.
127
+
128
+ *Release 20241007*:
129
+ HTTPServiceAPI.suburl: support interruption by RunState.cancel.
130
+
131
+ *Release 20240723*:
132
+ ServiceAPI: acquire the fstags automatically at init.
133
+
134
+ *Release 20230703*:
135
+ Retry logic for requests.
136
+
137
+ *Release 20230217*:
138
+ Initial release.
@@ -0,0 +1,162 @@
1
+ [project]
2
+ name = "cs-service_api"
3
+ description = "ServiceAPI, a base class for APIs which talk to a service, typically a web service via HTTP."
4
+ authors = [
5
+ { name = "Cameron Simpson", email = "cs@cskk.id.au" },
6
+ ]
7
+ keywords = [
8
+ "python3",
9
+ ]
10
+ dependencies = [
11
+ "cs.deco>=20260525",
12
+ "cs.fstags>=20260531",
13
+ "cs.logutils>=20250323",
14
+ "cs.pfx>=20250914",
15
+ "cs.resources>=20250915",
16
+ "cs.sqltags>=20260531",
17
+ "cs.tagset>=20260531",
18
+ "cs.upd>=20260526",
19
+ "icontract",
20
+ "requests",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Programming Language :: Python",
25
+ "Programming Language :: Python :: 3",
26
+ "Intended Audience :: Developers",
27
+ "Operating System :: OS Independent",
28
+ "Topic :: Software Development :: Libraries :: Python Modules",
29
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
30
+ ]
31
+ version = "20260531"
32
+
33
+ [project.license]
34
+ text = "GNU General Public License v3 or later (GPLv3+)"
35
+
36
+ [project.urls]
37
+ "Monorepo Hg/Mercurial Mirror" = "https://hg.sr.ht/~cameron-simpson/css"
38
+ "Monorepo Git Mirror" = "https://github.com/cameron-simpson/css"
39
+ "MonoRepo Commits" = "https://bitbucket.org/cameron_simpson/css/commits/branch/main"
40
+ Source = "https://github.com/cameron-simpson/css/blob/main/lib/python/cs/service_api.py"
41
+
42
+ [project.readme]
43
+ text = """
44
+ ServiceAPI, a base class for APIs which talk to a service,
45
+ typically a web service via HTTP.
46
+
47
+ *Latest release 20260531*:
48
+ * HTTPServiceAPI: add __truediv__(suburl) which does a GET.
49
+ * HTTPServiceAPI: default .API_HOSTNAME comes from the subclass.
50
+ * HTTPServiceAPI.json: catch requests.exceptions.JSONDecodeError also.
51
+ * HTTPServiceAPI: new mode (\"response\", \"json\", \"data\", callable) parameter.
52
+ * HTTPServiceAPI: new check_json optional dict of expected fields and values in JSON responses.
53
+
54
+ An instance of a `ServiceAPI` embodies some basic features
55
+ that feel common to web based services:
56
+ - a notion of a login
57
+ - local state, an `SQLTags` for data about entities of the service
58
+ - downloads, if that is a thing, with `FSTags` for file annotations
59
+
60
+ Short summary:
61
+ * `HTTPServiceAPI`: `HTTPServiceAPI` base class for other APIs talking to HTTP services.
62
+ * `RequestsNoAuth`: This is a special purpose subclass of `requests.auth.AuthBase` to apply no authorisation at all. This is for services with their own special purpose authorisation and avoids things like automatic netrc based auth.
63
+ * `ServiceAPI`: `SewrviceAPI` base class for other APIs talking to services.
64
+
65
+ Module contents:
66
+ - <a name=\"HTTPServiceAPI\"></a>`class HTTPServiceAPI(ServiceAPI)`: `HTTPServiceAPI` base class for other APIs talking to HTTP services.
67
+
68
+ Subclasses must define:
69
+ * `API_BASE`: the base URL of API calls.
70
+ For example, the `PlayOnAPI` defines this as `f'https://{API_HOSTNAME}/v3/'`.
71
+
72
+ *`HTTPServiceAPI.get(self, suburl, **kw) -> requests.models.Response`*:
73
+ Call `slef.suburl` with `method=\"GET\"`.
74
+
75
+ *`HTTPServiceAPI.post(self, suburl, **kw) -> requests.models.Response`*:
76
+ Call `slef.suburl` with `method=\"POST\"`.
77
+
78
+ *`HTTPServiceAPI.response_as_json(self, rsp: requests.models.Response) -> dict`*:
79
+ Return `rsp.json()`.
80
+
81
+ *`HTTPServiceAPI.response_as_json_data(self, rsp: requests.models.Response) -> dict`*:
82
+ Return the `\"data\"` element from a JSON response.
83
+
84
+ *`HTTPServiceAPI.suburl(self, suburl, *, base_url=None, method='GET', mode=None, check=True, cookies=None, headers=None, runstate: Optional[cs.resources.RunState] = <function uses_runstate.<locals>.<lambda> at 0x111844fe0>, verbose: bool, **rqkw) -> Union[requests.models.Response, dict]`*:
85
+ Request `suburl` from the service, by default using a `GET`.
86
+ The `suburl` must be a URL subpath not commencing with `'/'`.
87
+
88
+ Return:
89
+ - `mode(Response)` if `mode` is callable
90
+ - the `Response` if `mode==\"response\"`
91
+ - the `Response.json()` if `mode==\"json\"`
92
+ - the `Response.json()[\"data\"]` if `mode==\"data\"`
93
+
94
+ Keyword parameters:
95
+ * `base_url`: the base request domain, default from `self.API_BASE`
96
+ * `method`: optional request method, default `'GET'`
97
+ * `check`: if true, raise an HTTP error if the response
98
+ status is not 200; default `True`
99
+ * `cookies`: optional cookie jar, default from `self.cookies`
100
+ * `mode`: optional result mode, default from `self.mode`
101
+ Other keyword parameters are passed to the requests method.
102
+ - <a name=\"RequestsNoAuth\"></a>`class RequestsNoAuth(requests.auth.AuthBase)`: This is a special purpose subclass of `requests.auth.AuthBase`
103
+ to apply no authorisation at all.
104
+ This is for services with their own special purpose authorisation
105
+ and avoids things like automatic netrc based auth.
106
+ - <a name=\"ServiceAPI\"></a>`class ServiceAPI(cs.resources.MultiOpenMixin, cs.sqltags.UsesSQLTags)`: `SewrviceAPI` base class for other APIs talking to services.
107
+
108
+ *`ServiceAPI.available(self) -> Set[cs.sqltags.SQLTagSet]`*:
109
+ Return a set of the `SQLTagSet` instances representing available
110
+ items at the service, for example purchased books
111
+ available to your login.
112
+
113
+ *`ServiceAPI.get_login_state(self, do_refresh=False) -> cs.tagset.HasTags`*:
114
+ The login state, a `HasTags`, stored as `login.state.`*login_userid*.
115
+ This performs a login if necessary or if `do_refresh` is true
116
+ (default `False`).
117
+
118
+ *`ServiceAPI.login(self) -> Mapping`*:
119
+ Do a login: authenticate to the service, return a mapping of related information.
120
+
121
+ Not all services require this and we expect such subclasses
122
+ to avoid use of login-based methods.
123
+
124
+ *`ServiceAPI.login_expiry`*:
125
+ Expiry UNIX time for the login state.
126
+ This implementation returns `None`.
127
+
128
+ *`ServiceAPI.startup_shutdown(self)`*:
129
+ Open/close the `FSTags` and `UsesTagSets`.
130
+
131
+ # Release Log
132
+
133
+
134
+
135
+ *Release 20260531*:
136
+ * HTTPServiceAPI: add __truediv__(suburl) which does a GET.
137
+ * HTTPServiceAPI: default .API_HOSTNAME comes from the subclass.
138
+ * HTTPServiceAPI.json: catch requests.exceptions.JSONDecodeError also.
139
+ * HTTPServiceAPI: new mode (\"response\", \"json\", \"data\", callable) parameter.
140
+ * HTTPServiceAPI: new check_json optional dict of expected fields and values in JSON responses.
141
+
142
+ *Release 20241007*:
143
+ HTTPServiceAPI.suburl: support interruption by RunState.cancel.
144
+
145
+ *Release 20240723*:
146
+ ServiceAPI: acquire the fstags automatically at init.
147
+
148
+ *Release 20230703*:
149
+ Retry logic for requests.
150
+
151
+ *Release 20230217*:
152
+ Initial release."""
153
+ content-type = "text/markdown"
154
+
155
+ [build-system]
156
+ build-backend = "flit_core.buildapi"
157
+ requires = [
158
+ "flit_core >=3.2,<4",
159
+ ]
160
+
161
+ [tool.flit.module]
162
+ name = "cs.service_api"
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env python3
2
+
3
+ ''' ServiceAPI, a base class for APIs which talk to a service,
4
+ typically a web service via HTTP.
5
+
6
+ An instance of a `ServiceAPI` embodies some basic features
7
+ that feel common to web based services:
8
+ - a notion of a login
9
+ - local state, an `SQLTags` for data about entities of the service
10
+ - downloads, if that is a thing, with `FSTags` for file annotations
11
+ '''
12
+
13
+ from contextlib import contextmanager
14
+ from functools import cached_property
15
+ from json import JSONDecodeError
16
+ from threading import RLock
17
+ import time
18
+ from typing import Mapping, Set, Union
19
+
20
+ from icontract import require
21
+ import requests
22
+ from requests import Response
23
+ try:
24
+ from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
25
+ except ImportError:
26
+ RequestsJSONDecodeError = JSONDecodeError
27
+
28
+ from cs.deco import uses_verbose
29
+ from cs.fstags import FSTags, uses_fstags
30
+ from cs.logutils import warning
31
+ from cs.pfx import pfx_call
32
+ from cs.resources import MultiOpenMixin, RunState, uses_runstate
33
+ from cs.tagset import HasTags, UsesTagSets
34
+ from cs.sqltags import SQLTagSet, UsesSQLTags
35
+ from cs.upd import run_task
36
+
37
+ __version__ = '20260531'
38
+
39
+ DISTINFO = {
40
+ 'keywords': ["python3"],
41
+ 'classifiers': [
42
+ "Development Status :: 3 - Alpha",
43
+ "Programming Language :: Python",
44
+ "Programming Language :: Python :: 3",
45
+ ],
46
+ 'install_requires': [
47
+ 'cs.deco',
48
+ 'cs.fstags',
49
+ 'cs.logutils',
50
+ 'cs.pfx',
51
+ 'cs.resources',
52
+ 'cs.tagset',
53
+ 'cs.sqltags',
54
+ 'cs.upd',
55
+ 'icontract',
56
+ 'requests',
57
+ ],
58
+ }
59
+
60
+ class ServiceAPI(MultiOpenMixin, UsesSQLTags):
61
+ ''' `SewrviceAPI` base class for other APIs talking to services.
62
+ '''
63
+
64
+ API_AUTH_GRACETIME = None
65
+ API_RETRY_COUNT = 3 # number of request attempts
66
+ API_RETRY_DELAY = 5 # interval between request retries
67
+
68
+ @uses_fstags
69
+ def __init__(
70
+ self,
71
+ *,
72
+ fstags: FSTags,
73
+ tagsets: UsesTagSets = None,
74
+ ):
75
+ super().__init__(tagsets=tagsets)
76
+ self.fstags = fstags
77
+ self._lock = RLock()
78
+ self.login_state_mapping = None
79
+
80
+ @contextmanager
81
+ def startup_shutdown(self):
82
+ ''' Open/close the `FSTags` and `UsesTagSets`.
83
+ '''
84
+ with self.tagsets:
85
+ with self.fstags:
86
+ yield
87
+
88
+ def login(self) -> Mapping:
89
+ ''' Do a login: authenticate to the service, return a mapping of related information.
90
+
91
+ Not all services require this and we expect such subclasses
92
+ to avoid use of login-based methods.
93
+ '''
94
+ raise NotImplementedError
95
+
96
+ @property
97
+ def login_expiry(self):
98
+ ''' Expiry UNIX time for the login state.
99
+ This implementation returns `None`.
100
+ '''
101
+ return None
102
+
103
+ def get_login_state(self, do_refresh=False) -> HasTags:
104
+ ''' The login state, a `HasTags`, stored as `login.state.`*login_userid*.
105
+ This performs a login if necessary or if `do_refresh` is true
106
+ (default `False`).
107
+ '''
108
+ with self._lock:
109
+ state = self['login.state', self.login_userid.replace('.', '_')]
110
+ if do_refresh or (self.API_AUTH_GRACETIME is not None and
111
+ time.time() + self.API_AUTH_GRACETIME >= state.expiry):
112
+ for k, v in self.login().items():
113
+ if k not in ('id', 'name'):
114
+ state[k] = v
115
+ return state
116
+
117
+ @cached_property
118
+ def login_state(self) -> HasTags:
119
+ ''' The login state, a mapping. Performs a login if necessary.
120
+ '''
121
+ return self.get_login_state()
122
+
123
+ def available(self) -> Set[SQLTagSet]:
124
+ ''' Return a set of the `SQLTagSet` instances representing available
125
+ items at the service, for example purchased books
126
+ available to your login.
127
+ '''
128
+ raise NotImplementedError
129
+
130
+ class HTTPServiceAPI(ServiceAPI):
131
+ ''' `HTTPServiceAPI` base class for other APIs talking to HTTP services.
132
+
133
+ Subclasses must define:
134
+ * `API_BASE`: the base URL of API calls.
135
+ For example, the `PlayOnAPI` defines this as `f'https://{API_HOSTNAME}/v3/'`.
136
+ '''
137
+
138
+ def __init__(
139
+ self,
140
+ api_hostname=None,
141
+ *,
142
+ default_headers=None,
143
+ mode='response',
144
+ check_json=None,
145
+ **service_api_kw
146
+ ):
147
+ if api_hostname is None:
148
+ api_hostname = type(self).API_HOSTNAME
149
+ else:
150
+ self.API_HOSTNAME = api_hostname
151
+ self.API_BASE = f'https://{api_hostname}/'
152
+ if default_headers is None:
153
+ default_headers = {}
154
+ super().__init__(**service_api_kw)
155
+ session = self.session = requests.Session()
156
+ # mapping of method names to requests convenience calls
157
+ self.REQUESTS_METHOD_CALLS = {
158
+ 'GET': session.get,
159
+ 'POST': session.post,
160
+ 'HEAD': session.head,
161
+ }
162
+ self.cookies = session.cookies
163
+ self.default_headers = default_headers
164
+ self.mode = mode
165
+ self.check_json = check_json
166
+
167
+ def response_as_json(self, rsp: Response) -> dict:
168
+ ''' Return `rsp.json()`.
169
+ '''
170
+ try:
171
+ js = rsp.json()
172
+ except (JSONDecodeError, RequestsJSONDecodeError) as e:
173
+ warning("response is not JSON: %s\n%r", e, rsp)
174
+ raise
175
+ if self.check_json:
176
+ for field, value in self.check_json.items():
177
+ js_value = js.get(field)
178
+ if js_value != value:
179
+ warning(f'expected {field}={value!r}, got {js_value!r} from {js!r}')
180
+ return js
181
+
182
+ def response_as_json_data(self, rsp: Response) -> dict:
183
+ ''' Return the `"data"` element from a JSON response.
184
+ '''
185
+ js = self.response_as_json(rsp)
186
+ return js["data"]
187
+
188
+ @uses_runstate
189
+ @uses_verbose
190
+ @require(lambda suburl: not suburl.startswith('/'))
191
+ def suburl(
192
+ self,
193
+ suburl,
194
+ *,
195
+ base_url=None,
196
+ method='GET',
197
+ mode=None,
198
+ check=True,
199
+ cookies=None,
200
+ headers=None,
201
+ runstate: RunState,
202
+ verbose: bool,
203
+ **rqkw,
204
+ ) -> Union[Response, dict]:
205
+ ''' Request `suburl` from the service, by default using a `GET`.
206
+ The `suburl` must be a URL subpath not commencing with `'/'`.
207
+
208
+ Return:
209
+ - `mode(Response)` if `mode` is callable
210
+ - the `Response` if `mode=="response"`
211
+ - the `Response.json()` if `mode=="json"`
212
+ - the `Response.json()["data"]` if `mode=="data"`
213
+
214
+ Keyword parameters:
215
+ * `base_url`: the base request domain, default from `self.API_BASE`
216
+ * `method`: optional request method, default `'GET'`
217
+ * `check`: if true, raise an HTTP error if the response
218
+ status is not 200; default `True`
219
+ * `cookies`: optional cookie jar, default from `self.cookies`
220
+ * `mode`: optional result mode, default from `self.mode`
221
+ Other keyword parameters are passed to the requests method.
222
+ '''
223
+ rqm = self.REQUESTS_METHOD_CALLS[method]
224
+ if base_url is None:
225
+ base_url = self.API_BASE
226
+ if cookies is None:
227
+ cookies = self.cookies
228
+ if mode is None:
229
+ mode = self.mode
230
+ url = base_url + suburl
231
+ rq_headers = {}
232
+ rq_headers.update(self.default_headers)
233
+ if headers is not None:
234
+ rq_headers.update(headers)
235
+ with run_task(f'{method} {url}', report_print=verbose):
236
+ for retry in range(self.API_RETRY_COUNT, 0, -1):
237
+ runstate.raiseif()
238
+ try:
239
+ rsp = pfx_call(
240
+ rqm,
241
+ url,
242
+ cookies=cookies,
243
+ headers=rq_headers,
244
+ **rqkw,
245
+ )
246
+ break
247
+ except requests.ConnectionError as e:
248
+ if retry <= 1:
249
+ # last retry
250
+ raise
251
+ warning("%s: %s, retrying in %ds", url, e, self.API_RETRY_DELAY)
252
+ runstate.sleep(self.API_RETRY_DELAY)
253
+ if check:
254
+ rsp.raise_for_status()
255
+ if callable(mode):
256
+ return mode(rsp)
257
+ if mode == 'response':
258
+ return rsp
259
+ if mode == 'json':
260
+ return self.response_as_json(rsp)
261
+ if mode == 'data':
262
+ return self.response_as_json_data(rsp)
263
+ raise ValueError(
264
+ f'unsupported {mode=}, expected "data", "json", "response" or a callable'
265
+ )
266
+
267
+ def get(self, suburl, **kw) -> Response:
268
+ ''' Call `slef.suburl` with `method="GET"`.
269
+ '''
270
+ return self, suburl(suburl, method='GET', **kw)
271
+
272
+ def post(self, suburl, **kw) -> Response:
273
+ ''' Call `slef.suburl` with `method="POST"`.
274
+ '''
275
+ return self.suburl(suburl, method='POST', **kw)
276
+
277
+ def __truediv__(self, suburl):
278
+ return self.suburl(suburl)
279
+
280
+ # pylint: disable=too-few-public-methods
281
+ class RequestsNoAuth(requests.auth.AuthBase):
282
+ ''' This is a special purpose subclass of `requests.auth.AuthBase`
283
+ to apply no authorisation at all.
284
+ This is for services with their own special purpose authorisation
285
+ and avoids things like automatic netrc based auth.
286
+ '''
287
+
288
+ def __call__(self, r):
289
+ return r