codecraftai-restclient-gitlab 1.2.0__py3-none-any.whl
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.
- ccai/gitlab/py.typed +0 -0
- ccai/gitlab/rest/aspectsrest.py +120 -0
- ccai/gitlab/rest/clients/__init__.py +0 -0
- ccai/gitlab/rest/clients/gitlabrestclient.py +118 -0
- ccai/gitlab/rest/clients/groups/__init__.py +0 -0
- ccai/gitlab/rest/clients/groups/accessrequests.py +230 -0
- ccai/gitlab/rest/clients/groups/admin.py +41 -0
- ccai/gitlab/rest/clients/groups/alertmanagement.py +149 -0
- ccai/gitlab/rest/clients/groups/application.py +68 -0
- ccai/gitlab/rest/clients/groups/applications.py +95 -0
- ccai/gitlab/rest/clients/groups/avatar.py +41 -0
- ccai/gitlab/rest/clients/groups/badges.py +338 -0
- ccai/gitlab/rest/clients/groups/batchedbackgroundmigrations.py +122 -0
- ccai/gitlab/rest/clients/groups/branches.py +230 -0
- ccai/gitlab/rest/clients/groups/broadcastmessages.py +149 -0
- ccai/gitlab/rest/clients/groups/bulkimports.py +176 -0
- ccai/gitlab/rest/clients/groups/clusters.py +149 -0
- ccai/gitlab/rest/clients/groups/jobs.py +95 -0
- ccai/gitlab/rest/clients/groups/metadata.py +68 -0
- ccai/gitlab/rest/clients/groups/migrations.py +41 -0
- ccai/gitlab/rest/clients/groups/pipelinecomposition.py +146 -0
- ccai/gitlab/rest/clients/groups/planlimits.py +68 -0
- ccai/gitlab/rest/clients/parameters/__init__.py +0 -0
- ccai/gitlab/rest/clients/parameters/accessrequests.py +222 -0
- ccai/gitlab/rest/clients/parameters/admin.py +31 -0
- ccai/gitlab/rest/clients/parameters/alertmanagement.py +147 -0
- ccai/gitlab/rest/clients/parameters/application.py +27 -0
- ccai/gitlab/rest/clients/parameters/applications.py +51 -0
- ccai/gitlab/rest/clients/parameters/avatar.py +31 -0
- ccai/gitlab/rest/clients/parameters/badges.py +344 -0
- ccai/gitlab/rest/clients/parameters/batchedbackgroundmigrations.py +105 -0
- ccai/gitlab/rest/clients/parameters/branches.py +236 -0
- ccai/gitlab/rest/clients/parameters/broadcastmessages.py +123 -0
- ccai/gitlab/rest/clients/parameters/bulkimports.py +173 -0
- ccai/gitlab/rest/clients/parameters/clusters.py +100 -0
- ccai/gitlab/rest/clients/parameters/jobs.py +88 -0
- ccai/gitlab/rest/clients/parameters/metadata.py +11 -0
- ccai/gitlab/rest/clients/parameters/migrations.py +29 -0
- ccai/gitlab/rest/clients/parameters/pipelinecomposition.py +123 -0
- ccai/gitlab/rest/clients/parameters/planlimits.py +47 -0
- ccai/gitlab/rest/clients/responses/__init__.py +0 -0
- ccai/gitlab/rest/clients/responses/accessrequests.py +157 -0
- ccai/gitlab/rest/clients/responses/admin.py +24 -0
- ccai/gitlab/rest/clients/responses/alertmanagement.py +100 -0
- ccai/gitlab/rest/clients/responses/application.py +43 -0
- ccai/gitlab/rest/clients/responses/applications.py +62 -0
- ccai/gitlab/rest/clients/responses/avatar.py +24 -0
- ccai/gitlab/rest/clients/responses/badges.py +233 -0
- ccai/gitlab/rest/clients/responses/batchedbackgroundmigrations.py +81 -0
- ccai/gitlab/rest/clients/responses/branches.py +157 -0
- ccai/gitlab/rest/clients/responses/broadcastmessages.py +100 -0
- ccai/gitlab/rest/clients/responses/bulkimports.py +119 -0
- ccai/gitlab/rest/clients/responses/clusters.py +100 -0
- ccai/gitlab/rest/clients/responses/jobs.py +62 -0
- ccai/gitlab/rest/clients/responses/metadata.py +43 -0
- ccai/gitlab/rest/clients/responses/migrations.py +24 -0
- ccai/gitlab/rest/clients/responses/pipelinecomposition.py +100 -0
- ccai/gitlab/rest/clients/responses/planlimits.py +43 -0
- ccai/gitlab/rest/descriptions/gitlab-openapi-hints.yaml +195 -0
- ccai/gitlab/rest/descriptions/gitlab-openapi-spec.yaml +3635 -0
- ccai/gitlab/rest/generation/__init__.py +0 -0
- ccai/gitlab/rest/generation/restclientgenerator.py +581 -0
- ccai/gitlab/rest/itemsresult.py +274 -0
- ccai/gitlab/rest/restapigroup.py +17 -0
- ccai/gitlab/rest/restclientbase.py +676 -0
- ccai/gitlab/rest/restclientutils.py +0 -0
- codecraftai_restclient_gitlab-1.2.0.dist-info/METADATA +90 -0
- codecraftai_restclient_gitlab-1.2.0.dist-info/RECORD +70 -0
- codecraftai_restclient_gitlab-1.2.0.dist-info/WHEEL +4 -0
- codecraftai_restclient_gitlab-1.2.0.dist-info/licenses/LICENSE.md +204 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
.. module:: restclientbase
|
|
4
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
5
|
+
:synopsis: restclientbase module.
|
|
6
|
+
|
|
7
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__author__ = "Myron Walker"
|
|
12
|
+
__copyright__ = "Copyright 2025, Code Craft AI LLC"
|
|
13
|
+
__credits__ = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from typing import Dict, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from base64 import b64encode
|
|
23
|
+
from http import HTTPStatus
|
|
24
|
+
from urllib.parse import quote
|
|
25
|
+
|
|
26
|
+
from requests import Session, Response, Request
|
|
27
|
+
|
|
28
|
+
from ccai.errors.exceptions import HttpForbiddenError
|
|
29
|
+
from ccai.errors.raising import format_http_error_message, raise_for_http_status
|
|
30
|
+
|
|
31
|
+
from ccai.credentials.basecredential import BaseCredential
|
|
32
|
+
from ccai.credentials.basiccredential import BasicCredential
|
|
33
|
+
|
|
34
|
+
from ccai.gitlab.rest.aspectsrest import AspectsRest, LoggingPattern, DEFAULT_REST_ASPECTS
|
|
35
|
+
|
|
36
|
+
from ccai.gitlab.rest.aspectsrest import ActionPattern
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RestClientBase:
|
|
40
|
+
"""
|
|
41
|
+
The :class:`RestClientBase` object is a generic REST client base object that can be inherited from
|
|
42
|
+
in order to implement a REST client that utilizes 'aspects' based APIs
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
AUTH_LEAF = ""
|
|
46
|
+
|
|
47
|
+
# This code is for use with REST clients that can be used in both a production and test environment.
|
|
48
|
+
# we must be able to make calls with SSL certificate verification turned off as some test environments
|
|
49
|
+
# utilized self signed certificates. Allowing the client to make calls with verify turned off is not a bug
|
|
50
|
+
# its a feature.
|
|
51
|
+
VERIFY_CERTIFICATE = False
|
|
52
|
+
|
|
53
|
+
AUTHENTICATION_FAILURES = (HttpForbiddenError)
|
|
54
|
+
|
|
55
|
+
def __init__(self, host: str, credential: BaseCredential, port: Optional[int] = None, scheme: Optional[str] = "https", aspects: AspectsRest = DEFAULT_REST_ASPECTS):
|
|
56
|
+
"""
|
|
57
|
+
Initialize the REST client base.
|
|
58
|
+
|
|
59
|
+
:param host: Hostname for the REST service.
|
|
60
|
+
:param credential: Credential used for authentication.
|
|
61
|
+
:param port: Optional port for the service.
|
|
62
|
+
:param scheme: URL scheme, default is ``https``.
|
|
63
|
+
:param aspects: Default REST aspects.
|
|
64
|
+
|
|
65
|
+
:returns: None
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
self._host = host
|
|
69
|
+
self._credential = credential
|
|
70
|
+
self._aspects = aspects
|
|
71
|
+
|
|
72
|
+
self._host_url = f"{scheme}://{self._host}"
|
|
73
|
+
if port is not None:
|
|
74
|
+
self._host_url = f"{self._host_url}:{port}"
|
|
75
|
+
|
|
76
|
+
self._session: Session = None
|
|
77
|
+
|
|
78
|
+
self._logger = logging.getLogger(__name__)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def credential(self):
|
|
83
|
+
"""
|
|
84
|
+
Credential object used for authentication with the REST service.
|
|
85
|
+
|
|
86
|
+
:returns: The credential instance supplied at construction time.
|
|
87
|
+
"""
|
|
88
|
+
return self._credential
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def host(self):
|
|
92
|
+
"""
|
|
93
|
+
Hostname assigned to this REST client.
|
|
94
|
+
|
|
95
|
+
:returns: The host string passed to the constructor.
|
|
96
|
+
"""
|
|
97
|
+
return self._host
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def authenticate(self):
|
|
101
|
+
"""
|
|
102
|
+
The `authenticate` method can be overloaded in order to modify the method of authentication
|
|
103
|
+
for the rest client. The current method of authentication is `Basic` authentication.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
credential: BasicCredential = self._credential
|
|
107
|
+
|
|
108
|
+
username = credential.username
|
|
109
|
+
password = credential.password
|
|
110
|
+
|
|
111
|
+
authbytes = f"{username}:{password}".encode("utf-8")
|
|
112
|
+
passwd_encoded = b64encode(authbytes).decode("utf-8")
|
|
113
|
+
|
|
114
|
+
headers = {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"Authorization": f"Basic {passwd_encoded}"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
auth_leaf = self.AUTH_LEAF.rstrip("/")
|
|
120
|
+
auth_url = f"{self._host_url}/{auth_leaf}"
|
|
121
|
+
|
|
122
|
+
session = Session()
|
|
123
|
+
session.verify = self.VERIFY_CERTIFICATE
|
|
124
|
+
|
|
125
|
+
resp: Response = session.post(auth_url, headers=headers, verify=self.VERIFY_CERTIFICATE)
|
|
126
|
+
if resp.status_code != HTTPStatus.OK:
|
|
127
|
+
errmsg_lines = [
|
|
128
|
+
f"Error attempting to authenticate on url={auth_url}.",
|
|
129
|
+
f"STATUS: {resp.status_code}",
|
|
130
|
+
"RESPONSE:",
|
|
131
|
+
resp.text
|
|
132
|
+
]
|
|
133
|
+
errmsg = os.linesep.join(errmsg_lines)
|
|
134
|
+
raise HttpForbiddenError(errmsg)
|
|
135
|
+
|
|
136
|
+
self._session = session
|
|
137
|
+
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def perform_call(self, *, leaf: str, method: str, params: Union[Dict[str, str], None], body: Union[Dict[str, str], None],
|
|
142
|
+
exp_status: Union[int, List[int]], headers: Optional[Dict[str, str]] = None,
|
|
143
|
+
context_msg: Optional[str] = None, aspects: Union[AspectsRest, None]) -> Response:
|
|
144
|
+
"""
|
|
145
|
+
Perform a REST call using the specified execution aspects.
|
|
146
|
+
|
|
147
|
+
:param leaf: API leaf path.
|
|
148
|
+
:param method: HTTP method name.
|
|
149
|
+
:param params: Optional query parameters.
|
|
150
|
+
:param body: Optional request body.
|
|
151
|
+
:param exp_status: Expected HTTP status codes.
|
|
152
|
+
:param headers: Optional request headers.
|
|
153
|
+
:param context_msg: Optional context message for logging.
|
|
154
|
+
:param aspects: Aspects controlling call behavior.
|
|
155
|
+
|
|
156
|
+
:returns: The :class:`Response` object for the call.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
if aspects is None:
|
|
160
|
+
aspects = self._aspects
|
|
161
|
+
|
|
162
|
+
response: Response = None
|
|
163
|
+
|
|
164
|
+
action_pattern = aspects.action_pattern
|
|
165
|
+
|
|
166
|
+
if action_pattern == ActionPattern.SINGLE_CALL:
|
|
167
|
+
response = self._perform_single_call(leaf, method, params, body, exp_status, headers, context_msg, aspects)
|
|
168
|
+
elif action_pattern == ActionPattern.SINGLE_CONNECTED_CALL:
|
|
169
|
+
response = self._perform_single_connected_call(leaf, method, params, body, exp_status, headers, context_msg, aspects)
|
|
170
|
+
elif action_pattern == ActionPattern.DO_UNTIL_CONNECTION_FAILURE:
|
|
171
|
+
response = self._perform_call_until_connection_failure(leaf, method, params, body, exp_status, headers, context_msg, aspects)
|
|
172
|
+
elif action_pattern == ActionPattern.DO_UNTIL_SUCCESS:
|
|
173
|
+
response = self._perform_call_until_success(leaf, method, params, body, exp_status, headers, context_msg, aspects)
|
|
174
|
+
elif action_pattern == ActionPattern.DO_WHILE_SUCCESS:
|
|
175
|
+
response = self._perform_call_while_success(leaf, method, params, body, exp_status, headers, context_msg, aspects)
|
|
176
|
+
else:
|
|
177
|
+
errmsg = f"Unsupported action pattern '{action_pattern}'"
|
|
178
|
+
raise RuntimeError(errmsg)
|
|
179
|
+
|
|
180
|
+
return response
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def format_path(self, leaf: str, path_params: Dict[str, str]) -> str:
|
|
184
|
+
"""
|
|
185
|
+
Substitute templated path parameters into an API leaf.
|
|
186
|
+
|
|
187
|
+
Each ``{name}`` placeholder in ``leaf`` is replaced with the
|
|
188
|
+
URL-encoded value supplied in ``path_params``. Values are quoted
|
|
189
|
+
with an empty ``safe`` set so that reserved characters in a value
|
|
190
|
+
(for example a ``/`` in a branch name) cannot escape their path
|
|
191
|
+
segment and alter the request target.
|
|
192
|
+
|
|
193
|
+
:param leaf: API path template containing ``{name}`` placeholders.
|
|
194
|
+
:param path_params: Mapping of placeholder name to value.
|
|
195
|
+
|
|
196
|
+
:returns: The leaf with all supplied placeholders substituted.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
formatted_leaf = leaf
|
|
200
|
+
|
|
201
|
+
for param_name, param_value in path_params.items():
|
|
202
|
+
placeholder = "{" + param_name + "}"
|
|
203
|
+
encoded_value = quote(str(param_value), safe="")
|
|
204
|
+
formatted_leaf = formatted_leaf.replace(placeholder, encoded_value)
|
|
205
|
+
|
|
206
|
+
return formatted_leaf
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _format_call_url(self, leaf) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Compose the full request URL for an API leaf.
|
|
212
|
+
|
|
213
|
+
:param leaf: API path segment to append to the host URL.
|
|
214
|
+
|
|
215
|
+
:returns: Fully qualified URL for the call.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
call_url = f"{self._host_url}{leaf}"
|
|
219
|
+
|
|
220
|
+
return call_url
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _get_session(self, aspects: AspectsRest) -> Session:
|
|
224
|
+
"""
|
|
225
|
+
Retrieve or create the requests session for the given aspects.
|
|
226
|
+
|
|
227
|
+
:param aspects: Execution aspects that may supply a pre-created session.
|
|
228
|
+
|
|
229
|
+
:returns: :class:`Session` object for REST calls.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
session = None
|
|
233
|
+
|
|
234
|
+
if aspects.session is not None:
|
|
235
|
+
session = aspects.session
|
|
236
|
+
else:
|
|
237
|
+
if self._session is None:
|
|
238
|
+
self.authenticate()
|
|
239
|
+
|
|
240
|
+
session = self._session
|
|
241
|
+
|
|
242
|
+
return session
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _resolve_logger(self, aspects: AspectsRest) -> logging.Logger:
|
|
246
|
+
"""
|
|
247
|
+
Select the logger for a call: the one carried by ``aspects`` when it
|
|
248
|
+
has one, otherwise this module's logger. The default aspects carry
|
|
249
|
+
no logger, so without the fallback every retry or error log call
|
|
250
|
+
would fail with ``AttributeError`` instead of reporting the problem.
|
|
251
|
+
|
|
252
|
+
:param aspects: Execution aspects that may supply a logger.
|
|
253
|
+
|
|
254
|
+
:returns: The logger to use for the call.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
logger = aspects.logger
|
|
258
|
+
if logger is None:
|
|
259
|
+
logger = self._logger
|
|
260
|
+
|
|
261
|
+
return logger
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _perform_single_call(self, leaf: str, method: str, params: Union[Dict[str, str], None], body: Union[Dict[str, str], None],
|
|
265
|
+
exp_status: Union[int, List[int]], headers: Union[Dict[str, str], None],
|
|
266
|
+
context_msg: Union[str, None], aspects: AspectsRest) -> Response:
|
|
267
|
+
"""
|
|
268
|
+
Performs a REST call using a newly authenticated session. Only a single attempt to make the call and provide
|
|
269
|
+
the calling code with the result is made. If the call does not succeed due to authentication failure this could
|
|
270
|
+
be due to a credential timeout. Make a single attempt to reauthenticate and then remake the call.
|
|
271
|
+
|
|
272
|
+
:param leaf: API leaf path for the request.
|
|
273
|
+
:param method: HTTP method to invoke.
|
|
274
|
+
:param params: Query parameters for the request.
|
|
275
|
+
:param body: Request body payload.
|
|
276
|
+
:param exp_status: Expected HTTP status code(s).
|
|
277
|
+
:param headers: Optional HTTP headers.
|
|
278
|
+
:param context_msg: Context message for error reporting.
|
|
279
|
+
:param aspects: Aspects controlling execution behaviour.
|
|
280
|
+
|
|
281
|
+
:returns: The :class:`Response` object from the request.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
# We need expected status in the form of a collection for our comparison
|
|
285
|
+
if isinstance(exp_status, int):
|
|
286
|
+
exp_status = [exp_status]
|
|
287
|
+
|
|
288
|
+
call_url = self._format_call_url(leaf)
|
|
289
|
+
|
|
290
|
+
logger = self._resolve_logger(aspects)
|
|
291
|
+
|
|
292
|
+
send_timeouts = (aspects.connection_timeout, aspects.inactivity_timeout)
|
|
293
|
+
allowed_error_codes: List[int] = aspects.allowed_error_codes
|
|
294
|
+
allowed_connection_exceptions: List[BaseException] = aspects.allowed_connection_exceptions
|
|
295
|
+
|
|
296
|
+
auth_retry = False
|
|
297
|
+
while True:
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
session: Session = self._get_session(aspects)
|
|
301
|
+
|
|
302
|
+
req = Request(method=method, url=call_url, headers=headers, params=params, data=body)
|
|
303
|
+
prepared_req = req.prepare()
|
|
304
|
+
|
|
305
|
+
resp :Response = session.send(prepared_req, timeout=send_timeouts, verify=self.VERIFY_CERTIFICATE)
|
|
306
|
+
|
|
307
|
+
except self.AUTHENTICATION_FAILURES as autherr:
|
|
308
|
+
if not auth_retry:
|
|
309
|
+
auth_retry = True
|
|
310
|
+
self._session = None
|
|
311
|
+
else:
|
|
312
|
+
raise # we only make a single attempt to authenticate, bubble any additional failures
|
|
313
|
+
|
|
314
|
+
else:
|
|
315
|
+
break
|
|
316
|
+
|
|
317
|
+
found_status = resp.status_code
|
|
318
|
+
if found_status not in exp_status:
|
|
319
|
+
resp_content = resp.content
|
|
320
|
+
|
|
321
|
+
errmsg = format_http_error_message(found_status, resp_content, call_url, context=context_msg)
|
|
322
|
+
logger.error(errmsg)
|
|
323
|
+
|
|
324
|
+
raise_for_http_status(found_status, resp_content, call_url, context=context_msg)
|
|
325
|
+
|
|
326
|
+
return resp
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _perform_single_connected_call(self, leaf: str, method: str, params: Union[Dict[str, str], None], body: Union[Dict[str, str], None],
|
|
330
|
+
exp_status: Union[int, List[int]], headers: Union[Dict[str, str], None],
|
|
331
|
+
context_msg: Union[str, None], aspects: AspectsRest) -> Response:
|
|
332
|
+
"""
|
|
333
|
+
Perform a single call using an existing connection. This call will utilize aspect parameter such as connection timeout in its attempt
|
|
334
|
+
to connect and only gives up if a constraint is exceeded. Once it make a connection it makes a single attempt to perform the call on
|
|
335
|
+
the API and if the call fails, it does not retry.
|
|
336
|
+
|
|
337
|
+
:param leaf: API leaf path to call.
|
|
338
|
+
:param method: HTTP method to use.
|
|
339
|
+
:param params: Query parameters for the call.
|
|
340
|
+
:param body: Body payload for the call.
|
|
341
|
+
:param exp_status: Expected HTTP status code(s).
|
|
342
|
+
:param headers: Optional HTTP headers.
|
|
343
|
+
:param context_msg: Context message for error reporting.
|
|
344
|
+
:param aspects: REST aspects controlling the call.
|
|
345
|
+
|
|
346
|
+
:returns: Response returned from the request.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
call_url = self._format_call_url(leaf)
|
|
350
|
+
|
|
351
|
+
logger = self._resolve_logger(aspects)
|
|
352
|
+
|
|
353
|
+
send_timeouts = (aspects.connection_timeout, aspects.inactivity_timeout)
|
|
354
|
+
completion_timeout: float = aspects.completion_timeout
|
|
355
|
+
completion_interval: float = aspects.completion_interval
|
|
356
|
+
monitor_delay: float = aspects.monitor_delay
|
|
357
|
+
logging_pattern: LoggingPattern = aspects.logging_pattern
|
|
358
|
+
retry_logging_interval: int = aspects.retry_logging_interval
|
|
359
|
+
allowed_error_codes: List[int] = aspects.allowed_error_codes
|
|
360
|
+
allowed_connection_exceptions: List[BaseException] = aspects.allowed_connection_exceptions
|
|
361
|
+
must_connect: bool = aspects.must_connect
|
|
362
|
+
|
|
363
|
+
# Attempt to establish a connection and make a single call. We will
|
|
364
|
+
# retry connecting until either a connection is made or the completion
|
|
365
|
+
# timeout is exceeded. Once connected, the call is executed only once.
|
|
366
|
+
start_time = time.time()
|
|
367
|
+
attempt = 0
|
|
368
|
+
auth_retry = False
|
|
369
|
+
while True:
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
session: Session = self._get_session(aspects)
|
|
373
|
+
|
|
374
|
+
req = Request(method=method, url=call_url, headers=headers,
|
|
375
|
+
params=params, data=body)
|
|
376
|
+
prepared_req = req.prepare()
|
|
377
|
+
|
|
378
|
+
resp: Response = session.send(
|
|
379
|
+
prepared_req, timeout=send_timeouts,
|
|
380
|
+
verify=self.VERIFY_CERTIFICATE
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
except self.AUTHENTICATION_FAILURES:
|
|
384
|
+
if not auth_retry:
|
|
385
|
+
auth_retry = True
|
|
386
|
+
self._session = None
|
|
387
|
+
continue
|
|
388
|
+
raise
|
|
389
|
+
except Exception as exc: # Connection failure
|
|
390
|
+
if attempt % retry_logging_interval == 0:
|
|
391
|
+
dbg_msg = (
|
|
392
|
+
f"Connection attempt {attempt + 1} failed: {exc}"
|
|
393
|
+
)
|
|
394
|
+
logger.debug(dbg_msg)
|
|
395
|
+
|
|
396
|
+
if time.time() - start_time >= completion_timeout:
|
|
397
|
+
raise TimeoutError(
|
|
398
|
+
f"Timeout attempting call on url={call_url}"
|
|
399
|
+
) from exc
|
|
400
|
+
|
|
401
|
+
time.sleep(completion_interval)
|
|
402
|
+
attempt += 1
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
else:
|
|
406
|
+
break
|
|
407
|
+
|
|
408
|
+
found_status = resp.status_code
|
|
409
|
+
if isinstance(exp_status, int):
|
|
410
|
+
exp_list = [exp_status]
|
|
411
|
+
else:
|
|
412
|
+
exp_list = exp_status
|
|
413
|
+
|
|
414
|
+
if found_status not in exp_list:
|
|
415
|
+
resp_content = resp.content
|
|
416
|
+
errmsg = format_http_error_message(
|
|
417
|
+
found_status, resp_content, call_url, context=context_msg
|
|
418
|
+
)
|
|
419
|
+
logger.error(errmsg)
|
|
420
|
+
raise_for_http_status(
|
|
421
|
+
found_status, resp_content, call_url, context=context_msg
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return resp
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _perform_call_until_connection_failure(self, leaf: str, method: str, params: Union[Dict[str, str], None], body: Union[Dict[str, str], None],
|
|
428
|
+
exp_status: Union[int, List[int]], headers: Union[Dict[str, str], None],
|
|
429
|
+
context_msg: Union[str, None], aspects: AspectsRest) -> Response:
|
|
430
|
+
"""
|
|
431
|
+
Perform repeated calls until the connection fails by exceeding a constraint or until a completion timeout is reached. If there is no connection
|
|
432
|
+
failure prior to the completion timeout. It will raise a TimeoutError
|
|
433
|
+
|
|
434
|
+
:returns: The last response obtained before failure.
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
call_url = self._format_call_url(leaf)
|
|
438
|
+
|
|
439
|
+
logger = self._resolve_logger(aspects)
|
|
440
|
+
|
|
441
|
+
send_timeouts = (aspects.connection_timeout, aspects.inactivity_timeout)
|
|
442
|
+
completion_timeout: float = aspects.completion_timeout
|
|
443
|
+
completion_interval: float = aspects.completion_interval
|
|
444
|
+
monitor_delay: float = aspects.monitor_delay
|
|
445
|
+
logging_pattern: LoggingPattern = aspects.logging_pattern
|
|
446
|
+
retry_logging_interval: int = aspects.retry_logging_interval
|
|
447
|
+
allowed_error_codes: List[int] = aspects.allowed_error_codes
|
|
448
|
+
allowed_connection_exceptions: List[BaseException] = aspects.allowed_connection_exceptions
|
|
449
|
+
must_connect: bool = aspects.must_connect
|
|
450
|
+
|
|
451
|
+
# Repeatedly perform the call until we hit a connection failure or the
|
|
452
|
+
# completion timeout expires.
|
|
453
|
+
start_time = time.time()
|
|
454
|
+
attempt = 0
|
|
455
|
+
last_resp: Response = None
|
|
456
|
+
auth_retry = False
|
|
457
|
+
|
|
458
|
+
while True:
|
|
459
|
+
try:
|
|
460
|
+
session: Session = self._get_session(aspects)
|
|
461
|
+
|
|
462
|
+
req = Request(method=method, url=call_url, headers=headers,
|
|
463
|
+
params=params, data=body)
|
|
464
|
+
prepared_req = req.prepare()
|
|
465
|
+
|
|
466
|
+
resp: Response = session.send(
|
|
467
|
+
prepared_req, timeout=send_timeouts,
|
|
468
|
+
verify=self.VERIFY_CERTIFICATE
|
|
469
|
+
)
|
|
470
|
+
last_resp = resp
|
|
471
|
+
|
|
472
|
+
except self.AUTHENTICATION_FAILURES:
|
|
473
|
+
if not auth_retry:
|
|
474
|
+
auth_retry = True
|
|
475
|
+
self._session = None
|
|
476
|
+
continue
|
|
477
|
+
break
|
|
478
|
+
except Exception as exc:
|
|
479
|
+
# Any exception is treated as a connection failure
|
|
480
|
+
if attempt % retry_logging_interval == 0:
|
|
481
|
+
dbg_msg = (
|
|
482
|
+
f"Connection failure after {attempt + 1} attempts: {exc}"
|
|
483
|
+
)
|
|
484
|
+
logger.debug(dbg_msg)
|
|
485
|
+
break
|
|
486
|
+
|
|
487
|
+
if time.time() - start_time >= completion_timeout:
|
|
488
|
+
raise TimeoutError(
|
|
489
|
+
f"Timeout waiting for connection failure on url={call_url}"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
time.sleep(completion_interval)
|
|
493
|
+
attempt += 1
|
|
494
|
+
|
|
495
|
+
return last_resp
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _perform_call_until_success(self, leaf: str, method: str, params: Union[Dict[str, str], None], body: Union[Dict[str, str], None],
|
|
499
|
+
exp_status: Union[int, List[int]], headers: Union[Dict[str, str], None],
|
|
500
|
+
context_msg: Union[str, None], aspects: AspectsRest) -> Response:
|
|
501
|
+
"""
|
|
502
|
+
Perform repeated calls until one succeeds.
|
|
503
|
+
|
|
504
|
+
:returns: The successful response object.
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
call_url = self._format_call_url(leaf)
|
|
508
|
+
|
|
509
|
+
logger = self._resolve_logger(aspects)
|
|
510
|
+
|
|
511
|
+
send_timeouts = (aspects.connection_timeout, aspects.inactivity_timeout)
|
|
512
|
+
completion_timeout: float = aspects.completion_timeout
|
|
513
|
+
completion_interval: float = aspects.completion_interval
|
|
514
|
+
monitor_delay: float = aspects.monitor_delay
|
|
515
|
+
logging_pattern: LoggingPattern = aspects.logging_pattern
|
|
516
|
+
retry_logging_interval: int = aspects.retry_logging_interval
|
|
517
|
+
allowed_error_codes: List[int] = aspects.allowed_error_codes
|
|
518
|
+
allowed_connection_exceptions: List[BaseException] = aspects.allowed_connection_exceptions
|
|
519
|
+
must_connect: bool = aspects.must_connect
|
|
520
|
+
|
|
521
|
+
# Loop until we obtain a successful response or a timeout occurs.
|
|
522
|
+
start_time = time.time()
|
|
523
|
+
attempt = 0
|
|
524
|
+
auth_retry = False
|
|
525
|
+
|
|
526
|
+
while True:
|
|
527
|
+
try:
|
|
528
|
+
session: Session = self._get_session(aspects)
|
|
529
|
+
|
|
530
|
+
req = Request(method=method, url=call_url, headers=headers,
|
|
531
|
+
params=params, data=body)
|
|
532
|
+
prepared_req = req.prepare()
|
|
533
|
+
|
|
534
|
+
resp: Response = session.send(
|
|
535
|
+
prepared_req, timeout=send_timeouts,
|
|
536
|
+
verify=self.VERIFY_CERTIFICATE
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
except self.AUTHENTICATION_FAILURES:
|
|
540
|
+
if not auth_retry:
|
|
541
|
+
auth_retry = True
|
|
542
|
+
self._session = None
|
|
543
|
+
continue
|
|
544
|
+
if must_connect:
|
|
545
|
+
raise
|
|
546
|
+
resp = None
|
|
547
|
+
except Exception as exc:
|
|
548
|
+
if must_connect:
|
|
549
|
+
raise
|
|
550
|
+
resp = None
|
|
551
|
+
if attempt % retry_logging_interval == 0:
|
|
552
|
+
logger.debug(
|
|
553
|
+
f"Call attempt {attempt + 1} failed: {exc}"
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
else:
|
|
557
|
+
found_status = resp.status_code
|
|
558
|
+
if isinstance(exp_status, int):
|
|
559
|
+
exp_list = [exp_status]
|
|
560
|
+
else:
|
|
561
|
+
exp_list = exp_status
|
|
562
|
+
|
|
563
|
+
if found_status in exp_list:
|
|
564
|
+
return resp
|
|
565
|
+
|
|
566
|
+
if allowed_error_codes and found_status in allowed_error_codes:
|
|
567
|
+
resp = None
|
|
568
|
+
else:
|
|
569
|
+
resp_content = resp.content
|
|
570
|
+
errmsg = format_http_error_message(
|
|
571
|
+
found_status, resp_content, call_url, context=context_msg
|
|
572
|
+
)
|
|
573
|
+
logger.error(errmsg)
|
|
574
|
+
raise_for_http_status(
|
|
575
|
+
found_status, resp_content, call_url, context=context_msg
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if time.time() - start_time >= completion_timeout:
|
|
579
|
+
raise TimeoutError(
|
|
580
|
+
f"Timeout waiting for successful call on url={call_url}"
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
time.sleep(completion_interval)
|
|
584
|
+
attempt += 1
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _perform_call_while_success(self, leaf: str, method: str, params: Union[Dict[str, str], None], body: Union[Dict[str, str], None],
|
|
589
|
+
exp_status: Union[int, List[int]], headers: Union[Dict[str, str], None],
|
|
590
|
+
context_msg: Union[str, None], aspects: AspectsRest) -> Response:
|
|
591
|
+
"""
|
|
592
|
+
Continue performing calls while each one succeeds.
|
|
593
|
+
|
|
594
|
+
:returns: The last response object received.
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
call_url = self._format_call_url(leaf)
|
|
598
|
+
|
|
599
|
+
logger = self._resolve_logger(aspects)
|
|
600
|
+
|
|
601
|
+
send_timeouts = (aspects.connection_timeout, aspects.inactivity_timeout)
|
|
602
|
+
completion_timeout: float = aspects.completion_timeout
|
|
603
|
+
completion_interval: float = aspects.completion_interval
|
|
604
|
+
monitor_delay: float = aspects.monitor_delay
|
|
605
|
+
logging_pattern: LoggingPattern = aspects.logging_pattern
|
|
606
|
+
retry_logging_interval: int = aspects.retry_logging_interval
|
|
607
|
+
allowed_error_codes: List[int] = aspects.allowed_error_codes
|
|
608
|
+
allowed_connection_exceptions: List[BaseException] = aspects.allowed_connection_exceptions
|
|
609
|
+
must_connect: bool = aspects.must_connect
|
|
610
|
+
|
|
611
|
+
# Continue making calls while each call succeeds. Stop when a call
|
|
612
|
+
# fails or when the completion timeout is reached.
|
|
613
|
+
start_time = time.time()
|
|
614
|
+
attempt = 0
|
|
615
|
+
last_resp: Response = None
|
|
616
|
+
auth_retry = False
|
|
617
|
+
|
|
618
|
+
while True:
|
|
619
|
+
try:
|
|
620
|
+
session: Session = self._get_session(aspects)
|
|
621
|
+
|
|
622
|
+
req = Request(method=method, url=call_url, headers=headers,
|
|
623
|
+
params=params, data=body)
|
|
624
|
+
prepared_req = req.prepare()
|
|
625
|
+
|
|
626
|
+
resp: Response = session.send(
|
|
627
|
+
prepared_req, timeout=send_timeouts,
|
|
628
|
+
verify=self.VERIFY_CERTIFICATE
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
except self.AUTHENTICATION_FAILURES:
|
|
632
|
+
if not auth_retry:
|
|
633
|
+
auth_retry = True
|
|
634
|
+
self._session = None
|
|
635
|
+
continue
|
|
636
|
+
if must_connect:
|
|
637
|
+
raise
|
|
638
|
+
break
|
|
639
|
+
except Exception as exc:
|
|
640
|
+
if must_connect:
|
|
641
|
+
raise
|
|
642
|
+
if attempt % retry_logging_interval == 0:
|
|
643
|
+
logger.debug(
|
|
644
|
+
f"Call failed after {attempt + 1} attempts: {exc}"
|
|
645
|
+
)
|
|
646
|
+
break
|
|
647
|
+
else:
|
|
648
|
+
last_resp = resp
|
|
649
|
+
found_status = resp.status_code
|
|
650
|
+
if isinstance(exp_status, int):
|
|
651
|
+
exp_list = [exp_status]
|
|
652
|
+
else:
|
|
653
|
+
exp_list = exp_status
|
|
654
|
+
|
|
655
|
+
if found_status not in exp_list:
|
|
656
|
+
if allowed_error_codes and found_status in allowed_error_codes:
|
|
657
|
+
break
|
|
658
|
+
resp_content = resp.content
|
|
659
|
+
errmsg = format_http_error_message(
|
|
660
|
+
found_status, resp_content, call_url, context=context_msg
|
|
661
|
+
)
|
|
662
|
+
logger.error(errmsg)
|
|
663
|
+
raise_for_http_status(
|
|
664
|
+
found_status, resp_content, call_url, context=context_msg
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
if time.time() - start_time >= completion_timeout:
|
|
668
|
+
raise TimeoutError(
|
|
669
|
+
f"Timeout waiting for call failure on url={call_url}"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
time.sleep(completion_interval)
|
|
673
|
+
attempt += 1
|
|
674
|
+
|
|
675
|
+
return last_resp
|
|
676
|
+
|
|
File without changes
|