hero-sdk 0.14.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.
- hero/__init__.py +9 -0
- hero/client.py +194 -0
- hero/lib/__init__.py +19 -0
- hero/lib/attr_mapper.py +82 -0
- hero/lib/config.py +71 -0
- hero/lib/credentials.py +88 -0
- hero/lib/decorators.py +88 -0
- hero/lib/errors.py +158 -0
- hero/lib/event_trigger.py +147 -0
- hero/lib/helpers.py +29 -0
- hero/lib/loaders.py +51 -0
- hero/lib/resilient_session.py +44 -0
- hero/lib/service_base.py +44 -0
- hero/lib/session_hooks.py +31 -0
- hero/services/__init__.py +8 -0
- hero/services/assistant.py +123 -0
- hero/services/auth.py +1960 -0
- hero/services/data_repo.py +2027 -0
- hero/services/data_repo_resilient.py +68 -0
- hero/services/ml_model_registry.py +2197 -0
- hero/services/search.py +116 -0
- hero/services/task_engine.py +674 -0
- hero/services/task_engine_resilient.py +85 -0
- hero/url_map.py +48 -0
- hero_sdk-0.14.0.dist-info/METADATA +98 -0
- hero_sdk-0.14.0.dist-info/RECORD +28 -0
- hero_sdk-0.14.0.dist-info/WHEEL +4 -0
- hero_sdk-0.14.0.dist-info/licenses/LICENSE +28 -0
hero/__init__.py
ADDED
hero/client.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import jwt
|
|
2
|
+
import base64
|
|
3
|
+
from requests import Session
|
|
4
|
+
from jwt import (
|
|
5
|
+
PyJWKClient,
|
|
6
|
+
ExpiredSignatureError,
|
|
7
|
+
InvalidTokenError,
|
|
8
|
+
InvalidSignatureError,
|
|
9
|
+
DecodeError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .url_map import URL_MAP
|
|
13
|
+
from .lib import (
|
|
14
|
+
get_conf_from_collection,
|
|
15
|
+
get_env,
|
|
16
|
+
get_client_credentials,
|
|
17
|
+
)
|
|
18
|
+
from .services import (
|
|
19
|
+
AuthService,
|
|
20
|
+
DataRepoService,
|
|
21
|
+
TaskEngineService,
|
|
22
|
+
MLModelRegistry,
|
|
23
|
+
AssistantService,
|
|
24
|
+
)
|
|
25
|
+
from .services import SearchService
|
|
26
|
+
|
|
27
|
+
from .lib.errors import (
|
|
28
|
+
TokenDecodeError,
|
|
29
|
+
TokenInvalidSignatureError,
|
|
30
|
+
TokenInvalidError,
|
|
31
|
+
TokenGeneralError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HeroClient:
|
|
36
|
+
"""
|
|
37
|
+
Primary client for interfacing with the HERO API. Provides access to all services.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, client_id: str = None, client_secret: str = None):
|
|
41
|
+
"""
|
|
42
|
+
Creates the Hero client.
|
|
43
|
+
"""
|
|
44
|
+
self._scopes = []
|
|
45
|
+
|
|
46
|
+
self.env = get_env()
|
|
47
|
+
self.api = Session()
|
|
48
|
+
region = "us-west-2"
|
|
49
|
+
|
|
50
|
+
if client_id is None or client_secret is None:
|
|
51
|
+
client_id, client_secret = get_client_credentials()
|
|
52
|
+
|
|
53
|
+
self._client_id = client_id
|
|
54
|
+
self._client_secret = client_secret
|
|
55
|
+
self._cognito_api_url = get_conf_from_collection(
|
|
56
|
+
URL_MAP, "HERO_COGNITO_API_URL"
|
|
57
|
+
)
|
|
58
|
+
self._cognito_user_pool_id = get_conf_from_collection(URL_MAP, "USER_POOL_ID")
|
|
59
|
+
self._cognito_auth_url = f"https://cognito-idp.{region}.amazonaws.com/{region}_{self._cognito_user_pool_id}"
|
|
60
|
+
self._jwks_url = f"{self._cognito_auth_url}/.well-known/jwks.json"
|
|
61
|
+
self._jwk_client = PyJWKClient(self._jwks_url)
|
|
62
|
+
self._access_token = None
|
|
63
|
+
|
|
64
|
+
def _fetch_token(self):
|
|
65
|
+
"""
|
|
66
|
+
Login to the Cognito user pool. Requires a client with a client secret and authorization to assign requested scopes.
|
|
67
|
+
|
|
68
|
+
Returns a JWT access token.
|
|
69
|
+
"""
|
|
70
|
+
app_client_id_secret = f"{self._client_id}:{self._client_secret}".encode(
|
|
71
|
+
"utf-8"
|
|
72
|
+
)
|
|
73
|
+
# Request access_token following client credentials grant flow
|
|
74
|
+
basic_auth = f"Basic {base64.urlsafe_b64encode(app_client_id_secret).decode()}"
|
|
75
|
+
|
|
76
|
+
response = self.api.post(
|
|
77
|
+
self._cognito_api_url,
|
|
78
|
+
data=f'grant_type=client_credentials&scope={" ".join(self._scopes)}&client_id={self._client_id}',
|
|
79
|
+
headers={
|
|
80
|
+
"Authorization": basic_auth,
|
|
81
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
82
|
+
},
|
|
83
|
+
verify=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if response.status_code != 200:
|
|
87
|
+
raise Exception(
|
|
88
|
+
f"Failed to fetch access token: {response.status_code} - {response.text}, {self._scopes}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self._access_token = response.json()["access_token"]
|
|
92
|
+
|
|
93
|
+
def _decode_token(self, token):
|
|
94
|
+
"""
|
|
95
|
+
Decodes a JWT token and returns the payload, verifying its signature and expiration.
|
|
96
|
+
|
|
97
|
+
Raises TokenInvalidSignatureError if the signature is invalid.
|
|
98
|
+
Raises TokenDecodeError if the token is malformed.
|
|
99
|
+
Raises TokenInvalidError if the token is invalid.
|
|
100
|
+
Raises TokenGeneralError for any other unexpected errors.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
signing_key = self._jwk_client.get_signing_key_from_jwt(token).key
|
|
104
|
+
# Token is valid and not expired
|
|
105
|
+
return jwt.decode(
|
|
106
|
+
token,
|
|
107
|
+
key=signing_key,
|
|
108
|
+
algorithms=["RS256"],
|
|
109
|
+
options={"verify_exp": True, "verify_iat": False},
|
|
110
|
+
)
|
|
111
|
+
except ExpiredSignatureError:
|
|
112
|
+
# token expired, we need to refresh it
|
|
113
|
+
self._fetch_token()
|
|
114
|
+
# try to decode again
|
|
115
|
+
return jwt.decode(
|
|
116
|
+
token, algorithms=["RS256"], options={"verify_signature": False}
|
|
117
|
+
)
|
|
118
|
+
except InvalidSignatureError:
|
|
119
|
+
# The signature doesn't match — token could be tampered with
|
|
120
|
+
raise TokenInvalidSignatureError("Invalid token signature")
|
|
121
|
+
except DecodeError:
|
|
122
|
+
# Token is malformed (e.g., bad base64, wrong structure)
|
|
123
|
+
raise TokenDecodeError("Malformed token")
|
|
124
|
+
except InvalidTokenError as e:
|
|
125
|
+
# Other invalid token cases
|
|
126
|
+
raise TokenInvalidError(f"Invalid token: {str(e)}")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
# Any other unexpected errors
|
|
129
|
+
raise TokenGeneralError("Unexpected error")
|
|
130
|
+
|
|
131
|
+
def add_scope(self, scope):
|
|
132
|
+
"""
|
|
133
|
+
Adds a scope to the client.
|
|
134
|
+
"""
|
|
135
|
+
if scope in self._scopes:
|
|
136
|
+
return
|
|
137
|
+
self._scopes.append(scope)
|
|
138
|
+
|
|
139
|
+
def authenticate(self):
|
|
140
|
+
"""
|
|
141
|
+
Authenticates the client with the Cognito user pool.
|
|
142
|
+
"""
|
|
143
|
+
self._fetch_token()
|
|
144
|
+
|
|
145
|
+
def get_token(self):
|
|
146
|
+
"""
|
|
147
|
+
Returns the access token if it is valid.
|
|
148
|
+
|
|
149
|
+
Note: we could add re-auth capabilities here Or manage elsewhere
|
|
150
|
+
(i.e. resilient sessions, etc)
|
|
151
|
+
"""
|
|
152
|
+
access_token_decoded = self._decode_token(self._access_token)
|
|
153
|
+
if access_token_decoded:
|
|
154
|
+
return self._access_token
|
|
155
|
+
else:
|
|
156
|
+
self._fetch_token()
|
|
157
|
+
return self._access_token
|
|
158
|
+
|
|
159
|
+
def Auth(self):
|
|
160
|
+
"""
|
|
161
|
+
Returns a AuthService instance.
|
|
162
|
+
"""
|
|
163
|
+
return AuthService(self)
|
|
164
|
+
|
|
165
|
+
def DataRepo(self, application_id=None):
|
|
166
|
+
"""
|
|
167
|
+
Returns a DataRepoService instance.
|
|
168
|
+
"""
|
|
169
|
+
return DataRepoService(self, application_id)
|
|
170
|
+
|
|
171
|
+
def TaskEngine(self, application_id=None):
|
|
172
|
+
"""
|
|
173
|
+
Returns a TaskEngineService instance.
|
|
174
|
+
"""
|
|
175
|
+
return TaskEngineService(self, application_id)
|
|
176
|
+
|
|
177
|
+
def MLModelRegistry(self, application_id=None):
|
|
178
|
+
"""
|
|
179
|
+
Returns a MLModelRegistry instance.
|
|
180
|
+
"""
|
|
181
|
+
self.authInstance = self.Auth()
|
|
182
|
+
return MLModelRegistry(self, application_id)
|
|
183
|
+
|
|
184
|
+
def Search(self):
|
|
185
|
+
"""
|
|
186
|
+
Returns a SearchService instance.
|
|
187
|
+
"""
|
|
188
|
+
return SearchService(self)
|
|
189
|
+
|
|
190
|
+
def Assistant(self, application_id):
|
|
191
|
+
"""
|
|
192
|
+
Returns a AssistantService instance.
|
|
193
|
+
"""
|
|
194
|
+
return AssistantService(self, application_id)
|
hero/lib/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .service_base import ServiceBase
|
|
2
|
+
from .config import (
|
|
3
|
+
get_resilient_session,
|
|
4
|
+
get_client_credentials,
|
|
5
|
+
get_service_id,
|
|
6
|
+
get_conf_from_collection,
|
|
7
|
+
get_env,
|
|
8
|
+
set_environment,
|
|
9
|
+
)
|
|
10
|
+
from .decorators import (
|
|
11
|
+
decorate_all,
|
|
12
|
+
log_errors,
|
|
13
|
+
track_calls,
|
|
14
|
+
retry_method,
|
|
15
|
+
)
|
|
16
|
+
from .errors import HeroRetryError
|
|
17
|
+
from .helpers import set_log_level_from_env
|
|
18
|
+
from .attr_mapper import HeroObject
|
|
19
|
+
from .credentials import HeroCredentialsProvider
|
hero/lib/attr_mapper.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from collections.abc import MutableMapping
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _to_attr(value):
|
|
5
|
+
if isinstance(value, dict):
|
|
6
|
+
return HeroObject(value)
|
|
7
|
+
if isinstance(value, list):
|
|
8
|
+
return [_to_attr(v) for v in value]
|
|
9
|
+
if isinstance(value, tuple):
|
|
10
|
+
return tuple(_to_attr(v) for v in value)
|
|
11
|
+
return value
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HeroObject(MutableMapping):
|
|
15
|
+
"""
|
|
16
|
+
Mapping that supports attribute-style access and recursive wrapping.
|
|
17
|
+
Example:
|
|
18
|
+
run = HeroObject({"info": {"run_id": "123"}, "data": {"tags": [{"key": "k","value": "v"}]}})
|
|
19
|
+
run.info.run_id -> "123"
|
|
20
|
+
run["info"]["run_id"] -> "123"
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__slots__ = ("_store",)
|
|
24
|
+
|
|
25
|
+
def __init__(self, data=None, **kwargs):
|
|
26
|
+
object.__setattr__(self, "_store", {})
|
|
27
|
+
if data:
|
|
28
|
+
for k, v in data.items():
|
|
29
|
+
self._store[k] = _to_attr(v)
|
|
30
|
+
for k, v in kwargs.items():
|
|
31
|
+
self._store[k] = _to_attr(v)
|
|
32
|
+
|
|
33
|
+
# Mapping protocol
|
|
34
|
+
def __getitem__(self, key):
|
|
35
|
+
return self._store[key]
|
|
36
|
+
|
|
37
|
+
def __setitem__(self, key, value):
|
|
38
|
+
self._store[key] = _to_attr(value)
|
|
39
|
+
|
|
40
|
+
def __delitem__(self, key):
|
|
41
|
+
del self._store[key]
|
|
42
|
+
|
|
43
|
+
def __iter__(self):
|
|
44
|
+
return iter(self._store)
|
|
45
|
+
|
|
46
|
+
def __len__(self):
|
|
47
|
+
return len(self._store)
|
|
48
|
+
|
|
49
|
+
# Attribute access
|
|
50
|
+
def __getattr__(self, name):
|
|
51
|
+
try:
|
|
52
|
+
return self._store[name]
|
|
53
|
+
except KeyError as e:
|
|
54
|
+
raise AttributeError(name) from e
|
|
55
|
+
|
|
56
|
+
def __setattr__(self, name, value):
|
|
57
|
+
# keep internal slot private; everything else goes in the mapping
|
|
58
|
+
if name == "_store":
|
|
59
|
+
object.__setattr__(self, name, value)
|
|
60
|
+
else:
|
|
61
|
+
self._store[name] = _to_attr(value)
|
|
62
|
+
|
|
63
|
+
def __delattr__(self, name):
|
|
64
|
+
try:
|
|
65
|
+
del self._store[name]
|
|
66
|
+
except KeyError as e:
|
|
67
|
+
raise AttributeError(name) from e
|
|
68
|
+
|
|
69
|
+
def to_dict(self):
|
|
70
|
+
def _to_plain(v):
|
|
71
|
+
if isinstance(v, HeroObject):
|
|
72
|
+
return {k: _to_plain(v[k]) for k in v}
|
|
73
|
+
if isinstance(v, list):
|
|
74
|
+
return [_to_plain(i) for i in v]
|
|
75
|
+
if isinstance(v, tuple):
|
|
76
|
+
return tuple(_to_plain(i) for i in v)
|
|
77
|
+
return v
|
|
78
|
+
|
|
79
|
+
return {k: _to_plain(v) for k, v in self._store.items()}
|
|
80
|
+
|
|
81
|
+
def __repr__(self):
|
|
82
|
+
return f"HeroObject({self.to_dict()!r})"
|
hero/lib/config.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
log = logging.getLogger("hero:config")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_env():
|
|
11
|
+
return os.environ.get("HERO_ENV", "dev")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_service_id(key):
|
|
15
|
+
env = get_env()
|
|
16
|
+
return f"{env}-{os.environ[key]}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_conf_from_collection(collection, key):
|
|
20
|
+
return os.environ.get(key, collection[get_env()][key])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_resilient_session():
|
|
24
|
+
return os.environ.get("HERO_RESILIENT_SESSION", "False").lower() in ("true")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_client_credentials():
|
|
28
|
+
"""Returns the client credentials tuple (client_id, client_secret) from the environment variables HERO_CLIENT_ID and HERO_CLIENT_SECRET"""
|
|
29
|
+
client_credentials = (
|
|
30
|
+
os.environ["HERO_CLIENT_ID"],
|
|
31
|
+
os.environ["HERO_CLIENT_SECRET"],
|
|
32
|
+
)
|
|
33
|
+
return client_credentials
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def set_environment(application_id, path=None):
|
|
37
|
+
"""
|
|
38
|
+
This function will set the HERO environment variables from a file stored in ~/.hero/.credentials.json
|
|
39
|
+
|
|
40
|
+
Ensure you have your hero credentials saved in `~/.hero/credentials.json` format.
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
"dev-aeroportal": {
|
|
44
|
+
"HERO_CLIENT_ID": "1c5ngb6o6lvtdfkus0sflstdq4",
|
|
45
|
+
"HERO_CLIENT_SECRET": "******",
|
|
46
|
+
"[ANOTHER_KEY]": "***"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
application_id: name in the credentials.json file (e.g. dev-aeroportal)
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
if path is None:
|
|
57
|
+
path = str(pathlib.Path(os.environ.get("HOME")) / ".hero" / "credentials.json")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
credentials = json.loads(open(path, "r").read())
|
|
61
|
+
these_credentials = credentials[application_id]
|
|
62
|
+
for key, value in these_credentials.items():
|
|
63
|
+
os.environ[key] = value
|
|
64
|
+
|
|
65
|
+
except FileNotFoundError as e:
|
|
66
|
+
log.error(f"Unable to load {path}")
|
|
67
|
+
except KeyError as e:
|
|
68
|
+
log.error(f"Unable to read key {e}")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
log.error(str(e))
|
|
71
|
+
log.error(f"Unable to set credentials from {path}")
|
hero/lib/credentials.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from ..client import HeroClient
|
|
2
|
+
from typing import Optional, Dict
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
class HeroCredentialsProvider():
|
|
6
|
+
"""
|
|
7
|
+
Provide AWS credentials through Hero's Token Vending Machine.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._hero_client = None
|
|
11
|
+
self._auth = None
|
|
12
|
+
self._initialize_hero_client()
|
|
13
|
+
|
|
14
|
+
def _initialize_hero_client(self):
|
|
15
|
+
try:
|
|
16
|
+
self._hero_client = HeroClient()
|
|
17
|
+
self._auth = self._hero_client.Auth()
|
|
18
|
+
self._hero_client.authenticate()
|
|
19
|
+
except Exception as e:
|
|
20
|
+
raise RuntimeError(f"Failed to initialize Hero Client: {e}")
|
|
21
|
+
|
|
22
|
+
def get_aws_credentials(
|
|
23
|
+
self,
|
|
24
|
+
app_id: str,
|
|
25
|
+
app_type: str,
|
|
26
|
+
action: str,
|
|
27
|
+
resource_id: Optional[str] = None,
|
|
28
|
+
resource_type: Optional[str] = None,
|
|
29
|
+
aws_region: str = "us-west-2"
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Retrieve temporary AWS credentials from Hero's TVM.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
app_id : str, required
|
|
37
|
+
The ID of the application
|
|
38
|
+
app_type: str, required
|
|
39
|
+
The type of application
|
|
40
|
+
action : str, required
|
|
41
|
+
The action to perform (e.g., 'readFile', 'executeQuery')
|
|
42
|
+
resource_id : str, optional
|
|
43
|
+
ID of the specific resource for fine-grained access control
|
|
44
|
+
resource_type : str, optional
|
|
45
|
+
Type of resource for fine-grained access control
|
|
46
|
+
aws_region : str, optional
|
|
47
|
+
AWS region for credentials ( default: 'us-west-2')
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
dict
|
|
52
|
+
Dictionary containing access_key_id, secret_access_key,
|
|
53
|
+
session_token, region, and expiration
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
hero_response = self._auth.get_tvm_session(
|
|
57
|
+
app_id=app_id,
|
|
58
|
+
app_type=app_type,
|
|
59
|
+
resource_id=resource_id,
|
|
60
|
+
resource_type=resource_type,
|
|
61
|
+
action=action
|
|
62
|
+
)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise RuntimeError(f"Failed to retrieve credentials from Hero TVM: {e}")
|
|
65
|
+
|
|
66
|
+
credentials = {
|
|
67
|
+
'access_key_id': hero_response.get('AccessKeyId'),
|
|
68
|
+
'secret_access_key': hero_response.get('SecretAccessKey'),
|
|
69
|
+
'session_token': hero_response.get('SessionToken'),
|
|
70
|
+
'region': aws_region,
|
|
71
|
+
'expiration': hero_response.get('Expiration'),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return credentials
|
|
75
|
+
|
|
76
|
+
def inject_into_env(self, credentials: Dict[str, str]):
|
|
77
|
+
"""
|
|
78
|
+
Inject AWS Credentials into the environment variables
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
credentials : dict
|
|
83
|
+
Credentials dictionary from get_aws_credentials()
|
|
84
|
+
"""
|
|
85
|
+
os.environ['AWS_ACCESS_KEY_ID'] = credentials['access_key_id']
|
|
86
|
+
os.environ['AWS_SECRET_ACCESS_KEY'] = credentials['secret_access_key']
|
|
87
|
+
os.environ['AWS_SESSION_TOKEN'] = credentials['session_token']
|
|
88
|
+
os.environ['AWS_REGION'] = credentials['region']
|
hero/lib/decorators.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from tenacity import stop_after_attempt, wait_fixed, wait_exponential
|
|
5
|
+
|
|
6
|
+
from .errors import HeroRetryError
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger("hero:service")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def decorate_all(decorator):
|
|
12
|
+
|
|
13
|
+
def decorate(cls):
|
|
14
|
+
for name, attr in cls.__dict__.items():
|
|
15
|
+
if name.startswith("__"):
|
|
16
|
+
continue
|
|
17
|
+
if isinstance(attr, staticmethod):
|
|
18
|
+
setattr(cls, name, staticmethod(decorator(attr.__func__)))
|
|
19
|
+
elif isinstance(attr, classmethod):
|
|
20
|
+
setattr(cls, name, classmethod(decorator(attr.__func__)))
|
|
21
|
+
elif callable(attr):
|
|
22
|
+
setattr(cls, name, decorator(attr))
|
|
23
|
+
return cls
|
|
24
|
+
|
|
25
|
+
return decorate
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def log_errors(func):
|
|
29
|
+
@wraps(func)
|
|
30
|
+
def wrapper(*args, **kwargs):
|
|
31
|
+
try:
|
|
32
|
+
return func(*args, **kwargs)
|
|
33
|
+
except Exception:
|
|
34
|
+
if os.getenv("HERO_LOG_ALL_ERRORS") == "True":
|
|
35
|
+
log.error(f"Hero Service Error in {func.__name__}", exc_info=True)
|
|
36
|
+
raise
|
|
37
|
+
|
|
38
|
+
return wrapper
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def track_calls(func):
|
|
42
|
+
@wraps(func)
|
|
43
|
+
def wrapper(self, *args, **kwargs):
|
|
44
|
+
self._calls += 1
|
|
45
|
+
return func(self, *args, **kwargs)
|
|
46
|
+
|
|
47
|
+
return wrapper
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def retry_method(func, errFunc):
|
|
51
|
+
@wraps(func)
|
|
52
|
+
def wrapper(self, *args, **kwargs):
|
|
53
|
+
# attempts order: kwargs -> EVN -> default
|
|
54
|
+
|
|
55
|
+
attempts = int(
|
|
56
|
+
kwargs.get(
|
|
57
|
+
"attempts",
|
|
58
|
+
os.environ.get("HERO_RETRY_ATTEMPTS", self.default_attempts),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
wait_schedule = str(
|
|
63
|
+
kwargs.get(
|
|
64
|
+
"wait",
|
|
65
|
+
os.environ.get("HERO_RETRY_WAIT", self.default_wait),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# print(str(func.__name__), wait_schedule)
|
|
70
|
+
wait = wait_fixed(1)
|
|
71
|
+
if wait_schedule == "exp":
|
|
72
|
+
wait = wait_exponential(multiplier=1, min=1, max=60)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
local_instance = errFunc.retry_with(
|
|
76
|
+
stop=stop_after_attempt(attempts), wait=wait
|
|
77
|
+
)
|
|
78
|
+
results = local_instance.__call__(self, func, *args, **kwargs)
|
|
79
|
+
return results
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise HeroRetryError(
|
|
83
|
+
str(e),
|
|
84
|
+
local_instance.retry.statistics.get("attempt_number"),
|
|
85
|
+
local_instance.retry.statistics.get("idle_for"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return wrapper
|