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.
Files changed (70) hide show
  1. ccai/gitlab/py.typed +0 -0
  2. ccai/gitlab/rest/aspectsrest.py +120 -0
  3. ccai/gitlab/rest/clients/__init__.py +0 -0
  4. ccai/gitlab/rest/clients/gitlabrestclient.py +118 -0
  5. ccai/gitlab/rest/clients/groups/__init__.py +0 -0
  6. ccai/gitlab/rest/clients/groups/accessrequests.py +230 -0
  7. ccai/gitlab/rest/clients/groups/admin.py +41 -0
  8. ccai/gitlab/rest/clients/groups/alertmanagement.py +149 -0
  9. ccai/gitlab/rest/clients/groups/application.py +68 -0
  10. ccai/gitlab/rest/clients/groups/applications.py +95 -0
  11. ccai/gitlab/rest/clients/groups/avatar.py +41 -0
  12. ccai/gitlab/rest/clients/groups/badges.py +338 -0
  13. ccai/gitlab/rest/clients/groups/batchedbackgroundmigrations.py +122 -0
  14. ccai/gitlab/rest/clients/groups/branches.py +230 -0
  15. ccai/gitlab/rest/clients/groups/broadcastmessages.py +149 -0
  16. ccai/gitlab/rest/clients/groups/bulkimports.py +176 -0
  17. ccai/gitlab/rest/clients/groups/clusters.py +149 -0
  18. ccai/gitlab/rest/clients/groups/jobs.py +95 -0
  19. ccai/gitlab/rest/clients/groups/metadata.py +68 -0
  20. ccai/gitlab/rest/clients/groups/migrations.py +41 -0
  21. ccai/gitlab/rest/clients/groups/pipelinecomposition.py +146 -0
  22. ccai/gitlab/rest/clients/groups/planlimits.py +68 -0
  23. ccai/gitlab/rest/clients/parameters/__init__.py +0 -0
  24. ccai/gitlab/rest/clients/parameters/accessrequests.py +222 -0
  25. ccai/gitlab/rest/clients/parameters/admin.py +31 -0
  26. ccai/gitlab/rest/clients/parameters/alertmanagement.py +147 -0
  27. ccai/gitlab/rest/clients/parameters/application.py +27 -0
  28. ccai/gitlab/rest/clients/parameters/applications.py +51 -0
  29. ccai/gitlab/rest/clients/parameters/avatar.py +31 -0
  30. ccai/gitlab/rest/clients/parameters/badges.py +344 -0
  31. ccai/gitlab/rest/clients/parameters/batchedbackgroundmigrations.py +105 -0
  32. ccai/gitlab/rest/clients/parameters/branches.py +236 -0
  33. ccai/gitlab/rest/clients/parameters/broadcastmessages.py +123 -0
  34. ccai/gitlab/rest/clients/parameters/bulkimports.py +173 -0
  35. ccai/gitlab/rest/clients/parameters/clusters.py +100 -0
  36. ccai/gitlab/rest/clients/parameters/jobs.py +88 -0
  37. ccai/gitlab/rest/clients/parameters/metadata.py +11 -0
  38. ccai/gitlab/rest/clients/parameters/migrations.py +29 -0
  39. ccai/gitlab/rest/clients/parameters/pipelinecomposition.py +123 -0
  40. ccai/gitlab/rest/clients/parameters/planlimits.py +47 -0
  41. ccai/gitlab/rest/clients/responses/__init__.py +0 -0
  42. ccai/gitlab/rest/clients/responses/accessrequests.py +157 -0
  43. ccai/gitlab/rest/clients/responses/admin.py +24 -0
  44. ccai/gitlab/rest/clients/responses/alertmanagement.py +100 -0
  45. ccai/gitlab/rest/clients/responses/application.py +43 -0
  46. ccai/gitlab/rest/clients/responses/applications.py +62 -0
  47. ccai/gitlab/rest/clients/responses/avatar.py +24 -0
  48. ccai/gitlab/rest/clients/responses/badges.py +233 -0
  49. ccai/gitlab/rest/clients/responses/batchedbackgroundmigrations.py +81 -0
  50. ccai/gitlab/rest/clients/responses/branches.py +157 -0
  51. ccai/gitlab/rest/clients/responses/broadcastmessages.py +100 -0
  52. ccai/gitlab/rest/clients/responses/bulkimports.py +119 -0
  53. ccai/gitlab/rest/clients/responses/clusters.py +100 -0
  54. ccai/gitlab/rest/clients/responses/jobs.py +62 -0
  55. ccai/gitlab/rest/clients/responses/metadata.py +43 -0
  56. ccai/gitlab/rest/clients/responses/migrations.py +24 -0
  57. ccai/gitlab/rest/clients/responses/pipelinecomposition.py +100 -0
  58. ccai/gitlab/rest/clients/responses/planlimits.py +43 -0
  59. ccai/gitlab/rest/descriptions/gitlab-openapi-hints.yaml +195 -0
  60. ccai/gitlab/rest/descriptions/gitlab-openapi-spec.yaml +3635 -0
  61. ccai/gitlab/rest/generation/__init__.py +0 -0
  62. ccai/gitlab/rest/generation/restclientgenerator.py +581 -0
  63. ccai/gitlab/rest/itemsresult.py +274 -0
  64. ccai/gitlab/rest/restapigroup.py +17 -0
  65. ccai/gitlab/rest/restclientbase.py +676 -0
  66. ccai/gitlab/rest/restclientutils.py +0 -0
  67. codecraftai_restclient_gitlab-1.2.0.dist-info/METADATA +90 -0
  68. codecraftai_restclient_gitlab-1.2.0.dist-info/RECORD +70 -0
  69. codecraftai_restclient_gitlab-1.2.0.dist-info/WHEEL +4 -0
  70. 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