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.
- hero_sdk-0.14.0/.github/workflows/docs.yml +68 -0
- hero_sdk-0.14.0/.github/workflows/release.yml +19 -0
- hero_sdk-0.14.0/.gitignore +14 -0
- hero_sdk-0.14.0/.pre-commit-config.yaml +11 -0
- hero_sdk-0.14.0/LICENSE +28 -0
- hero_sdk-0.14.0/PKG-INFO +98 -0
- hero_sdk-0.14.0/README.md +71 -0
- hero_sdk-0.14.0/hero/__init__.py +9 -0
- hero_sdk-0.14.0/hero/client.py +194 -0
- hero_sdk-0.14.0/hero/lib/__init__.py +19 -0
- hero_sdk-0.14.0/hero/lib/attr_mapper.py +82 -0
- hero_sdk-0.14.0/hero/lib/config.py +71 -0
- hero_sdk-0.14.0/hero/lib/credentials.py +88 -0
- hero_sdk-0.14.0/hero/lib/decorators.py +88 -0
- hero_sdk-0.14.0/hero/lib/errors.py +158 -0
- hero_sdk-0.14.0/hero/lib/event_trigger.py +147 -0
- hero_sdk-0.14.0/hero/lib/helpers.py +29 -0
- hero_sdk-0.14.0/hero/lib/loaders.py +51 -0
- hero_sdk-0.14.0/hero/lib/resilient_session.py +44 -0
- hero_sdk-0.14.0/hero/lib/service_base.py +44 -0
- hero_sdk-0.14.0/hero/lib/session_hooks.py +31 -0
- hero_sdk-0.14.0/hero/services/__init__.py +8 -0
- hero_sdk-0.14.0/hero/services/assistant.py +123 -0
- hero_sdk-0.14.0/hero/services/auth.py +1960 -0
- hero_sdk-0.14.0/hero/services/data_repo.py +2027 -0
- hero_sdk-0.14.0/hero/services/data_repo_resilient.py +68 -0
- hero_sdk-0.14.0/hero/services/ml_model_registry.py +2197 -0
- hero_sdk-0.14.0/hero/services/search.py +116 -0
- hero_sdk-0.14.0/hero/services/task_engine.py +674 -0
- hero_sdk-0.14.0/hero/services/task_engine_resilient.py +85 -0
- hero_sdk-0.14.0/hero/url_map.py +48 -0
- hero_sdk-0.14.0/main.py +6 -0
- hero_sdk-0.14.0/pdoc/buildspec.yml +24 -0
- hero_sdk-0.14.0/pdoc/templates/config.mako +15 -0
- hero_sdk-0.14.0/pdoc/templates/head.mako +26 -0
- hero_sdk-0.14.0/pdoc/templates/logo.mako +5 -0
- hero_sdk-0.14.0/pyproject.toml +56 -0
- hero_sdk-0.14.0/run_test.sh +27 -0
- hero_sdk-0.14.0/test/__init__.py +0 -0
- hero_sdk-0.14.0/test/test_auth.py +780 -0
- hero_sdk-0.14.0/test/test_data_repo.py +1000 -0
- hero_sdk-0.14.0/test/test_decorators.py +44 -0
- hero_sdk-0.14.0/test/test_files/pyproject.toml +3 -0
- hero_sdk-0.14.0/test/test_ml_model_registry.py +563 -0
- hero_sdk-0.14.0/test/test_search.py +75 -0
- hero_sdk-0.14.0/test/test_task_engine.py +480 -0
- 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
|
hero_sdk-0.14.0/LICENSE
ADDED
|
@@ -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.
|
hero_sdk-0.14.0/PKG-INFO
ADDED
|
@@ -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,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}")
|