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 ADDED
@@ -0,0 +1,9 @@
1
+ import os
2
+ import json
3
+
4
+ from . lib import errors
5
+ from . lib.config import set_environment
6
+ from . lib.event_trigger import event_trigger
7
+ from . lib.loaders import get_env_variable, load_environment, load_runtime_config
8
+
9
+ from .client import HeroClient
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
@@ -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}")
@@ -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