hero-sdk 0.14.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.
Files changed (47) hide show
  1. hero_sdk-0.14.0/.github/workflows/docs.yml +68 -0
  2. hero_sdk-0.14.0/.github/workflows/release.yml +19 -0
  3. hero_sdk-0.14.0/.gitignore +14 -0
  4. hero_sdk-0.14.0/.pre-commit-config.yaml +11 -0
  5. hero_sdk-0.14.0/LICENSE +28 -0
  6. hero_sdk-0.14.0/PKG-INFO +98 -0
  7. hero_sdk-0.14.0/README.md +71 -0
  8. hero_sdk-0.14.0/hero/__init__.py +9 -0
  9. hero_sdk-0.14.0/hero/client.py +194 -0
  10. hero_sdk-0.14.0/hero/lib/__init__.py +19 -0
  11. hero_sdk-0.14.0/hero/lib/attr_mapper.py +82 -0
  12. hero_sdk-0.14.0/hero/lib/config.py +71 -0
  13. hero_sdk-0.14.0/hero/lib/credentials.py +88 -0
  14. hero_sdk-0.14.0/hero/lib/decorators.py +88 -0
  15. hero_sdk-0.14.0/hero/lib/errors.py +158 -0
  16. hero_sdk-0.14.0/hero/lib/event_trigger.py +147 -0
  17. hero_sdk-0.14.0/hero/lib/helpers.py +29 -0
  18. hero_sdk-0.14.0/hero/lib/loaders.py +51 -0
  19. hero_sdk-0.14.0/hero/lib/resilient_session.py +44 -0
  20. hero_sdk-0.14.0/hero/lib/service_base.py +44 -0
  21. hero_sdk-0.14.0/hero/lib/session_hooks.py +31 -0
  22. hero_sdk-0.14.0/hero/services/__init__.py +8 -0
  23. hero_sdk-0.14.0/hero/services/assistant.py +123 -0
  24. hero_sdk-0.14.0/hero/services/auth.py +1960 -0
  25. hero_sdk-0.14.0/hero/services/data_repo.py +2027 -0
  26. hero_sdk-0.14.0/hero/services/data_repo_resilient.py +68 -0
  27. hero_sdk-0.14.0/hero/services/ml_model_registry.py +2197 -0
  28. hero_sdk-0.14.0/hero/services/search.py +116 -0
  29. hero_sdk-0.14.0/hero/services/task_engine.py +674 -0
  30. hero_sdk-0.14.0/hero/services/task_engine_resilient.py +85 -0
  31. hero_sdk-0.14.0/hero/url_map.py +48 -0
  32. hero_sdk-0.14.0/main.py +6 -0
  33. hero_sdk-0.14.0/pdoc/buildspec.yml +24 -0
  34. hero_sdk-0.14.0/pdoc/templates/config.mako +15 -0
  35. hero_sdk-0.14.0/pdoc/templates/head.mako +26 -0
  36. hero_sdk-0.14.0/pdoc/templates/logo.mako +5 -0
  37. hero_sdk-0.14.0/pyproject.toml +56 -0
  38. hero_sdk-0.14.0/run_test.sh +27 -0
  39. hero_sdk-0.14.0/test/__init__.py +0 -0
  40. hero_sdk-0.14.0/test/test_auth.py +780 -0
  41. hero_sdk-0.14.0/test/test_data_repo.py +1000 -0
  42. hero_sdk-0.14.0/test/test_decorators.py +44 -0
  43. hero_sdk-0.14.0/test/test_files/pyproject.toml +3 -0
  44. hero_sdk-0.14.0/test/test_ml_model_registry.py +563 -0
  45. hero_sdk-0.14.0/test/test_search.py +75 -0
  46. hero_sdk-0.14.0/test/test_task_engine.py +480 -0
  47. hero_sdk-0.14.0/uv.lock +637 -0
@@ -0,0 +1,68 @@
1
+ # This workflow will publish pdoc to Github Pages on each published release
2
+
3
+ # How it works:
4
+ # This workflow runs pdoc.sh which generates the documentation
5
+ # The static documentation pages gets bundled up into an artifact as a tarball
6
+ # In the deploy phase the artifact gets uploaded to Github Pages
7
+
8
+ name: pdoc to Github Pages
9
+
10
+ # build the documentation whenever a pull request is made
11
+ # on:
12
+ # pull_request:
13
+
14
+ # build the documentation whenever there are new commits on main
15
+ # on:
16
+ # push:
17
+ # branches:
18
+ # - main
19
+ # Alternative: only build for tags.
20
+ # tags:
21
+ # - '*'
22
+
23
+ # build the documentation whenever a release is created, edited, or published
24
+ on:
25
+ push:
26
+ branches:
27
+ - main
28
+
29
+ # security: restrict permissions for CI jobs.
30
+ permissions:
31
+ contents: read
32
+
33
+ jobs:
34
+ # Build the documentation and upload the static HTML files as an artifact.
35
+ build:
36
+ runs-on: [ hero-runners ]
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - uses: actions/setup-python@v5
40
+ with:
41
+ python-version: '3.11'
42
+
43
+ # ADJUST THIS: install all dependencies (including pdoc)
44
+ - run: pip install .
45
+ - run: pip install pdoc
46
+ # ADJUST THIS: build your documentation into docs/.
47
+ # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here.
48
+ - run: pdoc ./hero --template-dir ./pdoc/templates --output-dir docs
49
+ # - run: PUT COMMAND TO MOVE FILES UP A DIR HERE <- we don't need this anymore
50
+
51
+ - uses: actions/upload-pages-artifact@v3
52
+ with:
53
+ path: docs
54
+
55
+ # Deploy the artifact to GitHub pages.
56
+ # This is a separate job so that only actions/deploy-pages has the necessary permissions.
57
+ deploy:
58
+ needs: build
59
+ runs-on: ubuntu-latest
60
+ permissions:
61
+ pages: write
62
+ id-token: write
63
+ environment:
64
+ name: github-pages
65
+ url: ${{ steps.deployment.outputs.page_url }}
66
+ steps:
67
+ - id: deployment
68
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,19 @@
1
+ name: GitHub Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+
12
+ permissions:
13
+ contents: write
14
+
15
+ steps:
16
+ - name: Create GitHub Release
17
+ uses: softprops/action-gh-release@v2
18
+ with:
19
+ generate_release_notes: true
@@ -0,0 +1,14 @@
1
+ *.egg-info
2
+ env.sh
3
+ env
4
+ __pycache__
5
+ .pytest_cache
6
+ .python-version
7
+ docs
8
+ build
9
+ dist
10
+
11
+ test/download.json
12
+ test/example.json
13
+
14
+ .DS_Store
@@ -0,0 +1,11 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v2.3.0
4
+ hooks:
5
+ - id: check-yaml
6
+ - id: end-of-file-fixer
7
+ - id: trailing-whitespace
8
+ - repo: https://github.com/psf/black
9
+ rev: 24.8.0
10
+ hooks:
11
+ - id: black
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026 Alliance for Energy Innovation, LLC
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: hero-sdk
3
+ Version: 0.14.0
4
+ Summary: Python client for HERO
5
+ Project-URL: Homepage, https://github.nrel.gov/Hero/hero
6
+ Project-URL: Documentation, https://github.nrel.gov/Hero/hero
7
+ Project-URL: Repository, https://github.nrel.gov/Hero/hero
8
+ Project-URL: Bug Tracker, https://github.nrel.gov/Hero/hero/issues
9
+ Author-email: Monte Lunacek <monte.lunacek@nrel.gov>, Nick Wunder <nick.wunder@nrel.gov>
10
+ Maintainer-email: Monte Lunacek <monte.lunacek@nrel.gov>, Nick Wunder <nick.wunder@nrel.gov>, Joseph Smith <joseph.smith@nrel.gov>
11
+ License: Proprietary
12
+ License-File: LICENSE
13
+ Keywords: data repo,hero,search,task engine
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Programming Language :: Python
16
+ Requires-Python: <=3.12,>=3.10
17
+ Requires-Dist: cryptography>=44.0.3
18
+ Requires-Dist: pyjwt>=2.8.0
19
+ Requires-Dist: requests>=2.32.3
20
+ Requires-Dist: tenacity>=8.3.0
21
+ Requires-Dist: tqdm>=4.66.4
22
+ Provides-Extra: dev
23
+ Requires-Dist: black>=24.8.0; extra == 'dev'
24
+ Requires-Dist: pre-commit>=3.8.0; extra == 'dev'
25
+ Requires-Dist: pytest>=8.3.5; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # HERO Python SDK
29
+
30
+ This is the Python SDK for HERO.
31
+
32
+ ## Installation
33
+
34
+ The HERO team recommends using [uv](https://docs.astral.sh/uv/) to install and manage project dependencies.
35
+
36
+ ### Using uv
37
+
38
+ ```
39
+ uv add https://github.nrel.gov/Hero/hero/archive/refs/tags/v0.14.0.zip
40
+ ```
41
+
42
+ ### Using pip
43
+
44
+ ```
45
+ pip install git+https://github.nrel.gov/Hero/hero@v0.14.0#egg=hero
46
+ ```
47
+
48
+ ## Development Installation and Release
49
+
50
+ First, clone this repo locally. Then install dependencies and pre-commit hooks:
51
+
52
+ ```
53
+ uv sync
54
+ uv run pre-commit install
55
+ ```
56
+
57
+ To run the tests:
58
+
59
+ ```
60
+ ./run_test.sh
61
+ ```
62
+
63
+ To link the local HERO codebase into a consuming project for feature development:
64
+
65
+ - Checkout the target branch in this repo
66
+ - In your consuming project, run `uv add --editable THE-PATH-TO-THE-NEWLY-CLONED-HERO-REPO`
67
+
68
+ ### Releasing a New Version
69
+
70
+ Once development is complete on a given feature/bugfix/etc, pleaes do the following to tag a new release.
71
+ - Update the version in `pyproject.toml`.
72
+ - Update the version in the Installation section(s) in the `README.md` (this file).
73
+ - Add and commit the changes made in the above two steps.
74
+ - Perform a non fast-forward the working branch into main
75
+ - `git checkout main`
76
+ - `git merge --no-ff THE-BRANCH-NAME-YOU-ARE-MERGING`.
77
+ - Tag the main branch with the new version via `git tag THE-NEW-VERSION-NUMBER`
78
+ - Push with tags `git push && git push --tags`
79
+
80
+
81
+ ## Usage
82
+
83
+ You need to have the following environment variables defined.
84
+
85
+ ```
86
+ export HERO_ENV=["dev", "stage", "prod"]
87
+ export HERO_PROJECT="aeroportal-app"
88
+ export HERO_CLIENT_ID="*******************************"
89
+ export HERO_CLIENT_SECRET="*******************************"
90
+ ```
91
+
92
+ ### Examples
93
+
94
+ Please check out the [HERO examples](https://github.com/nrel-hero/hero-examples).
95
+
96
+ Additionally, the tests in the `test` directory of this repo may also prove useful for basic usage examples.
97
+
98
+ SWR 26-024
@@ -0,0 +1,71 @@
1
+ # HERO Python SDK
2
+
3
+ This is the Python SDK for HERO.
4
+
5
+ ## Installation
6
+
7
+ The HERO team recommends using [uv](https://docs.astral.sh/uv/) to install and manage project dependencies.
8
+
9
+ ### Using uv
10
+
11
+ ```
12
+ uv add https://github.nrel.gov/Hero/hero/archive/refs/tags/v0.14.0.zip
13
+ ```
14
+
15
+ ### Using pip
16
+
17
+ ```
18
+ pip install git+https://github.nrel.gov/Hero/hero@v0.14.0#egg=hero
19
+ ```
20
+
21
+ ## Development Installation and Release
22
+
23
+ First, clone this repo locally. Then install dependencies and pre-commit hooks:
24
+
25
+ ```
26
+ uv sync
27
+ uv run pre-commit install
28
+ ```
29
+
30
+ To run the tests:
31
+
32
+ ```
33
+ ./run_test.sh
34
+ ```
35
+
36
+ To link the local HERO codebase into a consuming project for feature development:
37
+
38
+ - Checkout the target branch in this repo
39
+ - In your consuming project, run `uv add --editable THE-PATH-TO-THE-NEWLY-CLONED-HERO-REPO`
40
+
41
+ ### Releasing a New Version
42
+
43
+ Once development is complete on a given feature/bugfix/etc, pleaes do the following to tag a new release.
44
+ - Update the version in `pyproject.toml`.
45
+ - Update the version in the Installation section(s) in the `README.md` (this file).
46
+ - Add and commit the changes made in the above two steps.
47
+ - Perform a non fast-forward the working branch into main
48
+ - `git checkout main`
49
+ - `git merge --no-ff THE-BRANCH-NAME-YOU-ARE-MERGING`.
50
+ - Tag the main branch with the new version via `git tag THE-NEW-VERSION-NUMBER`
51
+ - Push with tags `git push && git push --tags`
52
+
53
+
54
+ ## Usage
55
+
56
+ You need to have the following environment variables defined.
57
+
58
+ ```
59
+ export HERO_ENV=["dev", "stage", "prod"]
60
+ export HERO_PROJECT="aeroportal-app"
61
+ export HERO_CLIENT_ID="*******************************"
62
+ export HERO_CLIENT_SECRET="*******************************"
63
+ ```
64
+
65
+ ### Examples
66
+
67
+ Please check out the [HERO examples](https://github.com/nrel-hero/hero-examples).
68
+
69
+ Additionally, the tests in the `test` directory of this repo may also prove useful for basic usage examples.
70
+
71
+ SWR 26-024
@@ -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
@@ -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)
@@ -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})"
@@ -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}")