databricks-sdk 0.18.0__tar.gz → 0.19.0__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.
Potentially problematic release.
This version of databricks-sdk might be problematic. Click here for more details.
- {databricks-sdk-0.18.0/databricks_sdk.egg-info → databricks-sdk-0.19.0}/PKG-INFO +1 -1
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/__init__.py +30 -1
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/azure.py +14 -0
- databricks-sdk-0.19.0/databricks/sdk/clock.py +49 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/config.py +7 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/core.py +2 -1
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/credentials_provider.py +14 -3
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/environments.py +1 -1
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/errors/__init__.py +1 -1
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/errors/mapper.py +5 -5
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/mixins/workspace.py +3 -3
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/retries.py +9 -5
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/catalog.py +173 -78
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/compute.py +86 -25
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/files.py +136 -22
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/iam.py +42 -36
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/jobs.py +192 -14
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/ml.py +27 -36
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/oauth2.py +3 -4
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/pipelines.py +50 -29
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/settings.py +338 -57
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/sharing.py +3 -4
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/sql.py +24 -17
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/vectorsearch.py +13 -17
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/workspace.py +18 -7
- databricks-sdk-0.19.0/databricks/sdk/version.py +1 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0/databricks_sdk.egg-info}/PKG-INFO +1 -1
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks_sdk.egg-info/SOURCES.txt +2 -1
- databricks-sdk-0.18.0/databricks/sdk/version.py +0 -1
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/LICENSE +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/NOTICE +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/README.md +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/__init__.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/_widgets/__init__.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/_widgets/default_widgets_utils.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/_widgets/ipywidgets_utils.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/dbutils.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/errors/base.py +0 -0
- /databricks-sdk-0.18.0/databricks/sdk/errors/mapping.py → /databricks-sdk-0.19.0/databricks/sdk/errors/platform.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/errors/sdk.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/mixins/__init__.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/mixins/compute.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/mixins/files.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/oauth.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/py.typed +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/runtime/__init__.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/runtime/dbutils_stub.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/runtime/stub.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/__init__.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/_internal.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/billing.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/dashboards.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/provisioning.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks/sdk/service/serving.py +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks_sdk.egg-info/dependency_links.txt +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks_sdk.egg-info/requires.txt +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/databricks_sdk.egg-info/top_level.txt +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/setup.cfg +0 -0
- {databricks-sdk-0.18.0 → databricks-sdk-0.19.0}/setup.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import databricks.sdk.core as client
|
|
2
2
|
import databricks.sdk.dbutils as dbutils
|
|
3
|
+
from databricks.sdk import azure
|
|
3
4
|
from databricks.sdk.credentials_provider import CredentialsProvider
|
|
4
5
|
from databricks.sdk.mixins.compute import ClustersExt
|
|
5
6
|
from databricks.sdk.mixins.files import DbfsExt
|
|
@@ -46,7 +47,7 @@ from databricks.sdk.service.provisioning import (CredentialsAPI,
|
|
|
46
47
|
EncryptionKeysAPI,
|
|
47
48
|
NetworksAPI, PrivateAccessAPI,
|
|
48
49
|
StorageAPI, VpcEndpointsAPI,
|
|
49
|
-
WorkspacesAPI)
|
|
50
|
+
Workspace, WorkspacesAPI)
|
|
50
51
|
from databricks.sdk.service.serving import AppsAPI, ServingEndpointsAPI
|
|
51
52
|
from databricks.sdk.service.settings import (AccountIpAccessListsAPI,
|
|
52
53
|
AccountSettingsAPI,
|
|
@@ -779,5 +780,33 @@ class AccountClient:
|
|
|
779
780
|
"""These APIs manage workspaces for this account."""
|
|
780
781
|
return self._workspaces
|
|
781
782
|
|
|
783
|
+
def get_workspace_client(self, workspace: Workspace) -> WorkspaceClient:
|
|
784
|
+
"""Constructs a ``WorkspaceClient`` for the given workspace.
|
|
785
|
+
|
|
786
|
+
Returns a ``WorkspaceClient`` that is configured to use the same
|
|
787
|
+
credentials as this ``AccountClient``. The underlying config is
|
|
788
|
+
copied from this ``AccountClient``, but the ``host`` and
|
|
789
|
+
``azure_workspace_resource_id`` are overridden to match the
|
|
790
|
+
given workspace, and the ``account_id`` field is cleared.
|
|
791
|
+
|
|
792
|
+
Usage:
|
|
793
|
+
|
|
794
|
+
.. code-block::
|
|
795
|
+
|
|
796
|
+
wss = list(a.workspaces.list())
|
|
797
|
+
if len(wss) == 0:
|
|
798
|
+
pytest.skip("no workspaces")
|
|
799
|
+
w = a.get_workspace_client(wss[0])
|
|
800
|
+
assert w.current_user.me().active
|
|
801
|
+
|
|
802
|
+
:param workspace: The workspace to construct a client for.
|
|
803
|
+
:return: A ``WorkspaceClient`` for the given workspace.
|
|
804
|
+
"""
|
|
805
|
+
config = self._config.copy()
|
|
806
|
+
config.host = config.environment.deployment_url(workspace.deployment_name)
|
|
807
|
+
config.azure_workspace_resource_id = azure.get_azure_resource_id(workspace)
|
|
808
|
+
config.account_id = None
|
|
809
|
+
return WorkspaceClient(config=config)
|
|
810
|
+
|
|
782
811
|
def __repr__(self):
|
|
783
812
|
return f"AccountClient(account_id='{self._config.account_id}', auth_type='{self._config.auth_type}', ...)"
|
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import Dict
|
|
3
3
|
|
|
4
4
|
from .oauth import TokenSource
|
|
5
|
+
from .service.provisioning import Workspace
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
@dataclass
|
|
@@ -38,3 +39,16 @@ def add_workspace_id_header(cfg: 'Config', headers: Dict[str, str]):
|
|
|
38
39
|
def add_sp_management_token(token_source: 'TokenSource', headers: Dict[str, str]):
|
|
39
40
|
mgmt_token = token_source.token()
|
|
40
41
|
headers['X-Databricks-Azure-SP-Management-Token'] = mgmt_token.access_token
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_azure_resource_id(workspace: Workspace):
|
|
45
|
+
"""
|
|
46
|
+
Returns the Azure Resource ID for the given workspace, if it is an Azure workspace.
|
|
47
|
+
:param workspace:
|
|
48
|
+
:return:
|
|
49
|
+
"""
|
|
50
|
+
if workspace.azure_workspace_info is None:
|
|
51
|
+
return None
|
|
52
|
+
return (f'/subscriptions/{workspace.azure_workspace_info.subscription_id}'
|
|
53
|
+
f'/resourceGroups/{workspace.azure_workspace_info.resource_group}'
|
|
54
|
+
f'/providers/Microsoft.Databricks/workspaces/{workspace.workspace_name}')
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Clock(metaclass=abc.ABCMeta):
|
|
6
|
+
|
|
7
|
+
@abc.abstractmethod
|
|
8
|
+
def time(self) -> float:
|
|
9
|
+
"""
|
|
10
|
+
Return the current time in seconds since the Epoch.
|
|
11
|
+
Fractions of a second may be present if the system clock provides them.
|
|
12
|
+
|
|
13
|
+
:return: The current time in seconds since the Epoch.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@abc.abstractmethod
|
|
17
|
+
def sleep(self, seconds: float) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Delay execution for a given number of seconds. The argument may be
|
|
20
|
+
a floating point number for subsecond precision.
|
|
21
|
+
|
|
22
|
+
:param seconds: The duration to sleep in seconds.
|
|
23
|
+
:return:
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RealClock(Clock):
|
|
28
|
+
"""
|
|
29
|
+
A real clock that uses the ``time`` module to get the current time and sleep.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def time(self) -> float:
|
|
33
|
+
"""
|
|
34
|
+
Return the current time in seconds since the Epoch.
|
|
35
|
+
Fractions of a second may be present if the system clock provides them.
|
|
36
|
+
|
|
37
|
+
:return: The current time in seconds since the Epoch.
|
|
38
|
+
"""
|
|
39
|
+
return time.time()
|
|
40
|
+
|
|
41
|
+
def sleep(self, seconds: float) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Delay execution for a given number of seconds. The argument may be
|
|
44
|
+
a floating point number for subsecond precision.
|
|
45
|
+
|
|
46
|
+
:param seconds: The duration to sleep in seconds.
|
|
47
|
+
:return:
|
|
48
|
+
"""
|
|
49
|
+
time.sleep(seconds)
|
|
@@ -11,6 +11,7 @@ from typing import Dict, Iterable, Optional
|
|
|
11
11
|
import requests
|
|
12
12
|
|
|
13
13
|
from .azure import AzureEnvironment
|
|
14
|
+
from .clock import Clock, RealClock
|
|
14
15
|
from .credentials_provider import CredentialsProvider, DefaultCredentials
|
|
15
16
|
from .environments import (ALL_ENVS, DEFAULT_ENVIRONMENT, Cloud,
|
|
16
17
|
DatabricksEnvironment)
|
|
@@ -84,6 +85,7 @@ class Config:
|
|
|
84
85
|
credentials_provider: CredentialsProvider = None,
|
|
85
86
|
product="unknown",
|
|
86
87
|
product_version="0.0.0",
|
|
88
|
+
clock: Clock = None,
|
|
87
89
|
**kwargs):
|
|
88
90
|
self._inner = {}
|
|
89
91
|
self._user_agent_other_info = []
|
|
@@ -91,6 +93,7 @@ class Config:
|
|
|
91
93
|
if 'databricks_environment' in kwargs:
|
|
92
94
|
self.databricks_environment = kwargs['databricks_environment']
|
|
93
95
|
del kwargs['databricks_environment']
|
|
96
|
+
self._clock = clock if clock is not None else RealClock()
|
|
94
97
|
try:
|
|
95
98
|
self._set_inner_config(kwargs)
|
|
96
99
|
self._load_from_env()
|
|
@@ -317,6 +320,10 @@ class Config:
|
|
|
317
320
|
if self.warehouse_id:
|
|
318
321
|
return f'/sql/1.0/warehouses/{self.warehouse_id}'
|
|
319
322
|
|
|
323
|
+
@property
|
|
324
|
+
def clock(self) -> Clock:
|
|
325
|
+
return self._clock
|
|
326
|
+
|
|
320
327
|
@classmethod
|
|
321
328
|
def attributes(cls) -> Iterable[ConfigAttribute]:
|
|
322
329
|
""" Returns a list of Databricks SDK configuration metadata """
|
|
@@ -123,7 +123,8 @@ class ApiClient:
|
|
|
123
123
|
headers = {}
|
|
124
124
|
headers['User-Agent'] = self._user_agent_base
|
|
125
125
|
retryable = retried(timeout=timedelta(seconds=self._retry_timeout_seconds),
|
|
126
|
-
is_retryable=self._is_retryable
|
|
126
|
+
is_retryable=self._is_retryable,
|
|
127
|
+
clock=self._cfg.clock)
|
|
127
128
|
return retryable(self._perform)(method,
|
|
128
129
|
path,
|
|
129
130
|
query=query,
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
8
|
import pathlib
|
|
9
|
+
import platform
|
|
9
10
|
import subprocess
|
|
10
11
|
import sys
|
|
11
12
|
from datetime import datetime
|
|
@@ -473,11 +474,21 @@ class DatabricksCliTokenSource(CliTokenSource):
|
|
|
473
474
|
args += ['--account-id', cfg.account_id]
|
|
474
475
|
|
|
475
476
|
cli_path = cfg.databricks_cli_path
|
|
477
|
+
|
|
478
|
+
# If the path is not specified look for "databricks" / "databricks.exe" in PATH.
|
|
476
479
|
if not cli_path:
|
|
477
|
-
|
|
480
|
+
try:
|
|
481
|
+
# Try to find "databricks" in PATH
|
|
482
|
+
cli_path = self.__class__._find_executable("databricks")
|
|
483
|
+
except FileNotFoundError as e:
|
|
484
|
+
# If "databricks" is not found, try to find "databricks.exe" in PATH (Windows)
|
|
485
|
+
if platform.system() == "Windows":
|
|
486
|
+
cli_path = self.__class__._find_executable("databricks.exe")
|
|
487
|
+
else:
|
|
488
|
+
raise e
|
|
478
489
|
|
|
479
490
|
# If the path is unqualified, look it up in PATH.
|
|
480
|
-
|
|
491
|
+
elif cli_path.count("/") == 0:
|
|
481
492
|
cli_path = self.__class__._find_executable(cli_path)
|
|
482
493
|
|
|
483
494
|
super().__init__(cmd=[cli_path, *args],
|
|
@@ -505,7 +516,7 @@ class DatabricksCliTokenSource(CliTokenSource):
|
|
|
505
516
|
raise err
|
|
506
517
|
|
|
507
518
|
|
|
508
|
-
@credentials_provider('databricks-cli', ['host'
|
|
519
|
+
@credentials_provider('databricks-cli', ['host'])
|
|
509
520
|
def databricks_cli(cfg: 'Config') -> Optional[HeaderFactory]:
|
|
510
521
|
try:
|
|
511
522
|
token_source = DatabricksCliTokenSource(cfg)
|
|
@@ -19,7 +19,7 @@ class DatabricksEnvironment:
|
|
|
19
19
|
azure_environment: Optional[AzureEnvironment] = None
|
|
20
20
|
|
|
21
21
|
def deployment_url(self, name: str) -> str:
|
|
22
|
-
return f"https://{name}
|
|
22
|
+
return f"https://{name}{self.dns_zone}"
|
|
23
23
|
|
|
24
24
|
@property
|
|
25
25
|
def azure_service_management_endpoint(self) -> Optional[str]:
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
from databricks.sdk.errors import
|
|
1
|
+
from databricks.sdk.errors import platform
|
|
2
2
|
from databricks.sdk.errors.base import DatabricksError
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def error_mapper(status_code: int, raw: dict) -> DatabricksError:
|
|
6
6
|
error_code = raw.get('error_code', None)
|
|
7
|
-
if error_code in
|
|
7
|
+
if error_code in platform.ERROR_CODE_MAPPING:
|
|
8
8
|
# more specific error codes override more generic HTTP status codes
|
|
9
|
-
return
|
|
9
|
+
return platform.ERROR_CODE_MAPPING[error_code](**raw)
|
|
10
10
|
|
|
11
|
-
if status_code in
|
|
11
|
+
if status_code in platform.STATUS_CODE_MAPPING:
|
|
12
12
|
# more generic HTTP status codes matched after more specific error codes,
|
|
13
13
|
# where there's a default exception class per HTTP status code, and we do
|
|
14
14
|
# rely on Databricks platform exception mapper to do the right thing.
|
|
15
|
-
return
|
|
15
|
+
return platform.STATUS_CODE_MAPPING[status_code](**raw)
|
|
16
16
|
|
|
17
17
|
# backwards-compatible error creation for cases like using older versions of
|
|
18
18
|
# the SDK on way never releases of the platform.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import BinaryIO, Iterator, Optional
|
|
1
|
+
from typing import BinaryIO, Iterator, Optional, Union
|
|
2
2
|
|
|
3
3
|
from ..core import DatabricksError
|
|
4
4
|
from ..service.workspace import (ExportFormat, ImportFormat, Language,
|
|
@@ -37,7 +37,7 @@ class WorkspaceExt(WorkspaceAPI):
|
|
|
37
37
|
|
|
38
38
|
def upload(self,
|
|
39
39
|
path: str,
|
|
40
|
-
content: BinaryIO,
|
|
40
|
+
content: Union[bytes, BinaryIO],
|
|
41
41
|
*,
|
|
42
42
|
format: Optional[ImportFormat] = None,
|
|
43
43
|
language: Optional[Language] = None,
|
|
@@ -51,7 +51,7 @@ class WorkspaceExt(WorkspaceAPI):
|
|
|
51
51
|
* `INVALID_PARAMETER_VALUE`: if `format` and `content` values are not compatible.
|
|
52
52
|
|
|
53
53
|
:param path: target location of the file on workspace.
|
|
54
|
-
:param content: file-like `io.BinaryIO` of the `path` contents.
|
|
54
|
+
:param content: the contents as either raw binary data `bytes` or a file-like the file-like `io.BinaryIO` of the `path` contents.
|
|
55
55
|
:param format: By default, `ImportFormat.SOURCE`. If using `ImportFormat.AUTO` the `path`
|
|
56
56
|
is imported or exported as either a workspace file or a notebook, depending
|
|
57
57
|
on an analysis of the `item`’s extension and the header content provided in
|
|
@@ -1,30 +1,34 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import logging
|
|
3
|
-
import time
|
|
4
3
|
from datetime import timedelta
|
|
5
4
|
from random import random
|
|
6
5
|
from typing import Callable, Optional, Sequence, Type
|
|
7
6
|
|
|
7
|
+
from .clock import Clock, RealClock
|
|
8
|
+
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def retried(*,
|
|
12
13
|
on: Sequence[Type[BaseException]] = None,
|
|
13
14
|
is_retryable: Callable[[BaseException], Optional[str]] = None,
|
|
14
|
-
timeout=timedelta(minutes=20)
|
|
15
|
+
timeout=timedelta(minutes=20),
|
|
16
|
+
clock: Clock = None):
|
|
15
17
|
has_allowlist = on is not None
|
|
16
18
|
has_callback = is_retryable is not None
|
|
17
19
|
if not (has_allowlist or has_callback) or (has_allowlist and has_callback):
|
|
18
20
|
raise SyntaxError('either on=[Exception] or callback=lambda x: .. is required')
|
|
21
|
+
if clock is None:
|
|
22
|
+
clock = RealClock()
|
|
19
23
|
|
|
20
24
|
def decorator(func):
|
|
21
25
|
|
|
22
26
|
@functools.wraps(func)
|
|
23
27
|
def wrapper(*args, **kwargs):
|
|
24
|
-
deadline =
|
|
28
|
+
deadline = clock.time() + timeout.total_seconds()
|
|
25
29
|
attempt = 1
|
|
26
30
|
last_err = None
|
|
27
|
-
while
|
|
31
|
+
while clock.time() < deadline:
|
|
28
32
|
try:
|
|
29
33
|
return func(*args, **kwargs)
|
|
30
34
|
except Exception as err:
|
|
@@ -50,7 +54,7 @@ def retried(*,
|
|
|
50
54
|
raise err
|
|
51
55
|
|
|
52
56
|
logger.debug(f'Retrying: {retry_reason} (sleeping ~{sleep}s)')
|
|
53
|
-
|
|
57
|
+
clock.sleep(sleep + random())
|
|
54
58
|
attempt += 1
|
|
55
59
|
raise TimeoutError(f'Timed out after {timeout}') from last_err
|
|
56
60
|
|