deeporigin-sdk 4.7.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.
- deeporigin_sdk/VERSION +1 -0
- deeporigin_sdk/__init__.py +49 -0
- deeporigin_sdk/auth.py +290 -0
- deeporigin_sdk/config.py +184 -0
- deeporigin_sdk/exceptions.py +125 -0
- deeporigin_sdk/platform/__init__.py +23 -0
- deeporigin_sdk/platform/billing.py +56 -0
- deeporigin_sdk/platform/client.py +990 -0
- deeporigin_sdk/platform/clusters.py +81 -0
- deeporigin_sdk/platform/constants.py +22 -0
- deeporigin_sdk/platform/executions.py +122 -0
- deeporigin_sdk/platform/files.py +453 -0
- deeporigin_sdk/platform/functions.py +187 -0
- deeporigin_sdk/platform/job.py +1735 -0
- deeporigin_sdk/platform/job_viz_functions.py +485 -0
- deeporigin_sdk/platform/organizations.py +57 -0
- deeporigin_sdk/platform/tools.py +85 -0
- deeporigin_sdk/utils/__init__.py +0 -0
- deeporigin_sdk/utils/constants.py +33 -0
- deeporigin_sdk/utils/core.py +449 -0
- deeporigin_sdk/utils/network.py +85 -0
- deeporigin_sdk-4.7.0.dist-info/METADATA +11 -0
- deeporigin_sdk-4.7.0.dist-info/RECORD +25 -0
- deeporigin_sdk-4.7.0.dist-info/WHEEL +5 -0
- deeporigin_sdk-4.7.0.dist-info/top_level.txt +1 -0
deeporigin_sdk/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.7.0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from deeporigin_sdk.exceptions import DeepOriginException
|
|
7
|
+
|
|
8
|
+
__all__ = ["__version__", "DeepOriginException"]
|
|
9
|
+
|
|
10
|
+
SRC_DIR = pathlib.Path(__file__).parent
|
|
11
|
+
|
|
12
|
+
version_filename = os.path.join(SRC_DIR, "VERSION")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_pep440_version():
|
|
16
|
+
"""get a pep440-compliant local version number"""
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
["git", "describe", "--tags", "--dirty", "--long"],
|
|
19
|
+
capture_output=True,
|
|
20
|
+
universal_newlines=True,
|
|
21
|
+
cwd=Path(__file__).parent.parent,
|
|
22
|
+
)
|
|
23
|
+
describe_str = result.stdout.strip()
|
|
24
|
+
|
|
25
|
+
# Parse the git describe output
|
|
26
|
+
tag, commits, commit_hash_dirty = describe_str.rsplit("-", 2)
|
|
27
|
+
commit_hash, dirty = (commit_hash_dirty.split("-") + [""])[:2]
|
|
28
|
+
|
|
29
|
+
# Format to PEP 440
|
|
30
|
+
if dirty:
|
|
31
|
+
local_version = f"+{commits}.g{commit_hash}.dirty"
|
|
32
|
+
else:
|
|
33
|
+
local_version = f"+{commits}.g{commit_hash}"
|
|
34
|
+
|
|
35
|
+
pep440_version = f"{tag}{local_version}"
|
|
36
|
+
|
|
37
|
+
return pep440_version
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
with open(version_filename, "r") as file:
|
|
41
|
+
__version__ = file.read().strip()
|
|
42
|
+
|
|
43
|
+
if __version__ == "0.0.0.dev0":
|
|
44
|
+
# in dev mode. we use git to get a "version number"
|
|
45
|
+
# that will change with tags and commits, if possible
|
|
46
|
+
try:
|
|
47
|
+
__version__ = _get_pep440_version()
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
deeporigin_sdk/auth.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""this module handles authentication actions and interactions
|
|
2
|
+
with tokens"""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from beartype import beartype
|
|
10
|
+
import httpx
|
|
11
|
+
import jwt
|
|
12
|
+
|
|
13
|
+
from deeporigin_sdk.config import get_value as get_config
|
|
14
|
+
from deeporigin_sdk.exceptions import DeepOriginException
|
|
15
|
+
from deeporigin_sdk.utils.constants import ENV_VARIABLES, ENVS
|
|
16
|
+
from deeporigin_sdk.utils.core import (
|
|
17
|
+
_ensure_do_folder,
|
|
18
|
+
_supports_unicode_output,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"get_token",
|
|
23
|
+
"save_token",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
AUTH_DOMAIN = {
|
|
27
|
+
"dev": "https://login.dev.deeporigin.io",
|
|
28
|
+
"prod": "https://login.deeporigin.io",
|
|
29
|
+
"staging": "https://login.staging.deeporigin.io",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@beartype
|
|
34
|
+
def _get_api_tokens_filepath() -> Path:
|
|
35
|
+
"""get location of the api tokens file"""
|
|
36
|
+
|
|
37
|
+
return _ensure_do_folder() / "api_tokens.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@beartype
|
|
41
|
+
def read_cached_token(*, env: ENVS | None = None) -> str | None:
|
|
42
|
+
"""Read cached API access token for a specific environment.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
env: Environment name (e.g., 'prod', 'staging', 'edge').
|
|
46
|
+
If None, reads from config.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Access token string for the specified environment.
|
|
50
|
+
Returns None if token does not exist for that environment.
|
|
51
|
+
"""
|
|
52
|
+
if env is None:
|
|
53
|
+
env = get_config()["env"]
|
|
54
|
+
|
|
55
|
+
filepath = _get_api_tokens_filepath()
|
|
56
|
+
|
|
57
|
+
if not filepath.exists():
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
with open(filepath, "r") as file:
|
|
61
|
+
all_tokens = json.load(file)
|
|
62
|
+
|
|
63
|
+
# Return access token for the specific environment
|
|
64
|
+
return all_tokens.get(env)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@beartype
|
|
68
|
+
def tokens_exist(*, env: ENVS | None = None) -> bool:
|
|
69
|
+
"""Check if cached API token exist for a specific environment.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
env: Environment name. If None, checks for current config environment.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if token exists for the environment, False otherwise.
|
|
76
|
+
"""
|
|
77
|
+
if env is None:
|
|
78
|
+
env = get_config()["env"]
|
|
79
|
+
|
|
80
|
+
filepath = _get_api_tokens_filepath()
|
|
81
|
+
|
|
82
|
+
if not filepath.exists():
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
with open(filepath, "r") as file:
|
|
86
|
+
all_tokens = json.load(file)
|
|
87
|
+
|
|
88
|
+
return bool(env in all_tokens and all_tokens[env])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@beartype
|
|
92
|
+
def get_token(*, env: ENVS | None = None) -> str:
|
|
93
|
+
"""Get access token for accessing the Deep Origin API
|
|
94
|
+
|
|
95
|
+
Gets token to access Deep Origin API.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
If an access token exists in the ENV, then it is used before
|
|
99
|
+
anything else. If not, then the tokens file is
|
|
100
|
+
checked for access token, and used if they exist.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
env: Environment name. If None, uses current config environment.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
API access token string
|
|
107
|
+
"""
|
|
108
|
+
if env is None:
|
|
109
|
+
env = get_config()["env"]
|
|
110
|
+
|
|
111
|
+
token = None
|
|
112
|
+
|
|
113
|
+
# Try to read from disk first
|
|
114
|
+
if tokens_exist(env=env):
|
|
115
|
+
token = read_cached_token(env=env)
|
|
116
|
+
|
|
117
|
+
# tokens in env override tokens on disk
|
|
118
|
+
if ENV_VARIABLES["access_token"] in os.environ:
|
|
119
|
+
token = os.environ[ENV_VARIABLES["access_token"]]
|
|
120
|
+
|
|
121
|
+
if not token:
|
|
122
|
+
raise DeepOriginException(
|
|
123
|
+
"No access token found. Failed to get a token from the environment or disk."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# check if the access token is expired
|
|
127
|
+
if is_token_expired(token):
|
|
128
|
+
raise DeepOriginException(
|
|
129
|
+
title="Token Expired",
|
|
130
|
+
message="Token is expired. Please refer to https://client-docs.deeporigin.io/how-to/auth.html to get a new token.",
|
|
131
|
+
level="danger",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return token
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@beartype
|
|
138
|
+
def token_to_env(token: str) -> ENVS:
|
|
139
|
+
"""Determine the environment from a token's issuer.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
token: Access token string.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Environment name ('dev', 'staging', or 'prod').
|
|
146
|
+
"""
|
|
147
|
+
decoded_token = decode_access_token(token)
|
|
148
|
+
if "dev" in decoded_token["iss"]:
|
|
149
|
+
return "dev"
|
|
150
|
+
elif "staging" in decoded_token["iss"]:
|
|
151
|
+
return "staging"
|
|
152
|
+
elif "local" in decoded_token["iss"]:
|
|
153
|
+
return "local"
|
|
154
|
+
else:
|
|
155
|
+
return "prod"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@beartype
|
|
159
|
+
def save_token(token: str) -> None:
|
|
160
|
+
"""Save a long-lived token from the UI to disk.
|
|
161
|
+
|
|
162
|
+
This function validates and saves a long-lived token obtained from the
|
|
163
|
+
Deep Origin UI. The token will be stored in the api_tokens.json file
|
|
164
|
+
and used by get_token() and client initialization.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
token: Long-lived token string obtained from the UI.
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
DeepOriginException: If token is invalid or cannot be decoded.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
env = token_to_env(token)
|
|
175
|
+
decoded_token = decode_access_token(token)
|
|
176
|
+
|
|
177
|
+
# Save tokens to disk
|
|
178
|
+
filepath = _get_api_tokens_filepath()
|
|
179
|
+
|
|
180
|
+
# Load existing tokens for all environments
|
|
181
|
+
all_tokens = {}
|
|
182
|
+
if filepath.exists():
|
|
183
|
+
with open(filepath, "r") as file:
|
|
184
|
+
all_tokens = json.load(file)
|
|
185
|
+
|
|
186
|
+
# Update tokens for the specific environment
|
|
187
|
+
all_tokens[env] = token
|
|
188
|
+
|
|
189
|
+
# Write back all environments
|
|
190
|
+
with open(filepath, "w") as file:
|
|
191
|
+
json.dump(all_tokens, file, indent=2)
|
|
192
|
+
|
|
193
|
+
name = decoded_token.get("name", "Unknown User")
|
|
194
|
+
|
|
195
|
+
# Print confirmation
|
|
196
|
+
if _supports_unicode_output():
|
|
197
|
+
check = "✔︎"
|
|
198
|
+
else:
|
|
199
|
+
check = "OK"
|
|
200
|
+
print(
|
|
201
|
+
f"{check} Long-lived token for {name} saved successfully for environment '{env}'"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@beartype
|
|
206
|
+
def is_token_expired(token: str) -> bool:
|
|
207
|
+
"""
|
|
208
|
+
Check if the JWT token is expired. The token is expected to have an 'exp' field as a Unix timestamp.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
token: The JWT token string to check.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
bool: True if the token is expired, False otherwise.
|
|
215
|
+
"""
|
|
216
|
+
decoded_token = decode_access_token(token)
|
|
217
|
+
# Get the expiration time from the token, defaulting to 0 if not found.
|
|
218
|
+
exp_time = decoded_token.get("exp", 0)
|
|
219
|
+
current_time = time.time() # Get current time in seconds since the epoch.
|
|
220
|
+
|
|
221
|
+
# If current time is greater than the expiration time, it's expired.
|
|
222
|
+
return current_time > exp_time
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@beartype
|
|
226
|
+
def decode_access_token(token: str) -> dict:
|
|
227
|
+
"""decode access token into human readable data"""
|
|
228
|
+
|
|
229
|
+
# Get the JWT header
|
|
230
|
+
header = jwt.get_unverified_header(token)
|
|
231
|
+
|
|
232
|
+
# Decode the JWT using the public key
|
|
233
|
+
return jwt.decode(
|
|
234
|
+
token, algorithms=header["alg"], options={"verify_signature": False}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@beartype
|
|
239
|
+
def _get_keycloak_token(
|
|
240
|
+
*,
|
|
241
|
+
email: str,
|
|
242
|
+
password: str,
|
|
243
|
+
realm: str = "deeporigin",
|
|
244
|
+
base_url: str = "https://login.dev.deeporigin.io",
|
|
245
|
+
scope: str = "openid email super-user",
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""get a token, with optional super user scope from keycloak
|
|
248
|
+
|
|
249
|
+
This returns a super-user token (if possible) from keycloak. Do not use this function.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
email: the email of the super user
|
|
253
|
+
password: the password of the super user
|
|
254
|
+
realm: the realm to get the token from
|
|
255
|
+
base_url: the base url of the keycloak instance
|
|
256
|
+
scope: the scope of the token
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
DeepOriginException: If email or password is empty or not a string.
|
|
260
|
+
"""
|
|
261
|
+
# Validate input parameters
|
|
262
|
+
if not email.strip():
|
|
263
|
+
raise DeepOriginException(
|
|
264
|
+
title="Invalid email parameter",
|
|
265
|
+
message="Email must be a non-empty string.",
|
|
266
|
+
)
|
|
267
|
+
if not password.strip():
|
|
268
|
+
raise DeepOriginException(
|
|
269
|
+
title="Invalid password parameter",
|
|
270
|
+
message="Password must be a non-empty string.",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
keycloak_url = f"{base_url}/realms/{realm}/protocol/openid-connect/token"
|
|
274
|
+
|
|
275
|
+
data = {
|
|
276
|
+
"grant_type": "password",
|
|
277
|
+
"username": email,
|
|
278
|
+
"password": password,
|
|
279
|
+
"client_id": "do-app",
|
|
280
|
+
"scope": scope,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
response = httpx.post(
|
|
284
|
+
keycloak_url,
|
|
285
|
+
data=data, # sent as application/x-www-form-urlencoded
|
|
286
|
+
# Let httpx set Content-Type automatically for form data
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
response.raise_for_status()
|
|
290
|
+
return response.json()
|
deeporigin_sdk/config.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Simplified configuration management for Deep Origin client.
|
|
2
|
+
|
|
3
|
+
This module stores and retrieves only two configuration values:
|
|
4
|
+
`env` and `org_key`.
|
|
5
|
+
|
|
6
|
+
Behavior:
|
|
7
|
+
- If the config file does not exist, it is created with `env=prod` and an
|
|
8
|
+
empty `org_key`.
|
|
9
|
+
- If the config file exists, it is read and a dictionary is returned.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from typing import TYPE_CHECKING, Literal
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from deeporigin_sdk.utils.constants import ENV_VARIABLES
|
|
20
|
+
from deeporigin_sdk.utils.core import _ensure_do_folder, _supports_unicode_output
|
|
21
|
+
|
|
22
|
+
CONFIG_JSON_LOCATION = _ensure_do_folder() / "config.json"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"get_org",
|
|
26
|
+
"set_org",
|
|
27
|
+
"get_env",
|
|
28
|
+
"set_env",
|
|
29
|
+
"get_value",
|
|
30
|
+
"CONFIG_JSON_LOCATION",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ensure_config_file_exists() -> None:
|
|
35
|
+
"""Ensure the configuration file exists; create with defaults if missing."""
|
|
36
|
+
|
|
37
|
+
if not os.path.isfile(CONFIG_JSON_LOCATION):
|
|
38
|
+
default_data: dict = {"env": "prod", "org_key": ""}
|
|
39
|
+
os.makedirs(os.path.dirname(CONFIG_JSON_LOCATION), exist_ok=True)
|
|
40
|
+
with open(CONFIG_JSON_LOCATION, "w") as file:
|
|
41
|
+
json.dump(default_data, file, indent=2)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_org() -> str | None:
|
|
45
|
+
"""Get the organization key.
|
|
46
|
+
|
|
47
|
+
Creates the config file with defaults if it doesn't exist.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The organization key, or None if not set. Environment variables
|
|
51
|
+
override the config file value.
|
|
52
|
+
"""
|
|
53
|
+
return get_value()["org_key"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_org(value: str) -> None:
|
|
57
|
+
"""Set the organization key.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
value: The organization key to set.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
DeepOriginException: If the organization key does not exist in the
|
|
64
|
+
list of accessible organizations.
|
|
65
|
+
"""
|
|
66
|
+
from deeporigin_sdk.exceptions import DeepOriginException
|
|
67
|
+
|
|
68
|
+
# Validate that the org key exists
|
|
69
|
+
orgs_df = list_orgs()
|
|
70
|
+
if value not in orgs_df["key"].values:
|
|
71
|
+
available_keys = ", ".join(orgs_df["key"].tolist())
|
|
72
|
+
raise DeepOriginException(
|
|
73
|
+
title="Invalid organization key",
|
|
74
|
+
message=f"Organization key '{value}' not found in accessible organizations.",
|
|
75
|
+
fix=f"Available organization keys: {available_keys}",
|
|
76
|
+
level="danger",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_set_value("org_key", value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_env() -> str:
|
|
83
|
+
"""Get the environment.
|
|
84
|
+
|
|
85
|
+
Creates the config file with defaults if it doesn't exist.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The environment (e.g., 'prod', 'staging', 'edge', 'dev', 'local'). Defaults to 'prod'.
|
|
89
|
+
Environment variables override the config file value.
|
|
90
|
+
"""
|
|
91
|
+
return get_value()["env"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_env(value: str) -> None:
|
|
95
|
+
"""Set the environment.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
value: The environment to set (e.g., 'prod', 'staging', 'edge', 'dev', 'local').
|
|
99
|
+
"""
|
|
100
|
+
_set_value("env", value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _set_value(key: Literal["env", "org_key"], value) -> None:
|
|
104
|
+
"""Internal helper to set a configuration value.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
key: Configuration key to set (must be 'env' or 'org_key').
|
|
108
|
+
value: Value to set.
|
|
109
|
+
"""
|
|
110
|
+
_ensure_config_file_exists()
|
|
111
|
+
|
|
112
|
+
with open(CONFIG_JSON_LOCATION, "r") as file:
|
|
113
|
+
data = json.load(file) or {}
|
|
114
|
+
|
|
115
|
+
data[key] = value
|
|
116
|
+
|
|
117
|
+
# Persist updated data
|
|
118
|
+
with open(CONFIG_JSON_LOCATION, "w") as file:
|
|
119
|
+
json.dump(data, file, indent=2)
|
|
120
|
+
|
|
121
|
+
# Prefer Unicode on capable terminals; fall back to ASCII-safe symbols
|
|
122
|
+
if _supports_unicode_output():
|
|
123
|
+
check, arrow = "✔︎", "→"
|
|
124
|
+
else:
|
|
125
|
+
check, arrow = "OK", "->"
|
|
126
|
+
print(f"{check} {key} {arrow} {value}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_value() -> dict:
|
|
130
|
+
"""Get the configuration values.
|
|
131
|
+
|
|
132
|
+
Creates the file with defaults if it doesn't exist, then returns a dict
|
|
133
|
+
with keys `env` and `org_key`.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
config_file_location: Optional custom path for the config file.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A dictionary with keys `env` and `org_key`.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
_ensure_config_file_exists()
|
|
143
|
+
|
|
144
|
+
with open(CONFIG_JSON_LOCATION, "r") as file:
|
|
145
|
+
data = json.load(file) or {}
|
|
146
|
+
|
|
147
|
+
# Fill defaults if missing
|
|
148
|
+
env = data.get("env", "prod")
|
|
149
|
+
org_key = data.get("org_key", None)
|
|
150
|
+
|
|
151
|
+
# env variables override config file
|
|
152
|
+
if ENV_VARIABLES["env"] in os.environ:
|
|
153
|
+
env = os.environ[ENV_VARIABLES["env"]]
|
|
154
|
+
if ENV_VARIABLES["org_key"] in os.environ:
|
|
155
|
+
org_key = os.environ[ENV_VARIABLES["org_key"]]
|
|
156
|
+
|
|
157
|
+
return {"env": env, "org_key": org_key}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def list_orgs() -> "pd.DataFrame":
|
|
161
|
+
"""List all organizations accessible to the authenticated user.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
A pandas DataFrame with columns: name, key, autoApproveMaxAmount, threshold.
|
|
165
|
+
"""
|
|
166
|
+
import pandas as pd
|
|
167
|
+
|
|
168
|
+
from deeporigin_sdk.platform.client import DeepOriginClient
|
|
169
|
+
|
|
170
|
+
client = DeepOriginClient.get()
|
|
171
|
+
orgs = client.organizations.list()
|
|
172
|
+
|
|
173
|
+
# Extract only the required columns and map orgKey to key
|
|
174
|
+
data = [
|
|
175
|
+
{
|
|
176
|
+
"name": org["name"],
|
|
177
|
+
"key": org["orgKey"],
|
|
178
|
+
"autoApproveMaxAmount": org["autoApproveMaxAmount"],
|
|
179
|
+
"threshold": org["threshold"],
|
|
180
|
+
}
|
|
181
|
+
for org in orgs
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
return pd.DataFrame(data)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""custom exceptions to surface better errors in notebooks"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from deeporigin_sdk.utils.core import _supports_color
|
|
6
|
+
|
|
7
|
+
__all__ = ["DeepOriginException", "install_silent_error_handler"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeepOriginException(Exception):
|
|
11
|
+
"""Stops execution without showing a traceback, displays a styled error card."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, title="Error", message=None, fix=None, level="danger"):
|
|
14
|
+
super().__init__(message or title)
|
|
15
|
+
self.title = title
|
|
16
|
+
self.body = message or ""
|
|
17
|
+
self.footer = fix
|
|
18
|
+
# accepted: danger | warning | info | success | secondary
|
|
19
|
+
self.level = level
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
"""Format exception for display. Returns minimal output in notebooks (where HTML rendering is handled separately) or formatted console output otherwise."""
|
|
23
|
+
# Try to use IPython display if available (for notebooks)
|
|
24
|
+
try:
|
|
25
|
+
from IPython import get_ipython
|
|
26
|
+
|
|
27
|
+
ip = get_ipython()
|
|
28
|
+
if ip is not None and "pytest" not in sys.modules:
|
|
29
|
+
# In notebook, let the custom handler display HTML
|
|
30
|
+
# Return minimal string to avoid double display
|
|
31
|
+
return self.title
|
|
32
|
+
except ImportError:
|
|
33
|
+
# IPython is not available; fall back to console output formatting below.
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
# Format for console output
|
|
37
|
+
lines = []
|
|
38
|
+
|
|
39
|
+
# Add colored title if terminal supports it
|
|
40
|
+
if _supports_color():
|
|
41
|
+
level_colors = {
|
|
42
|
+
"danger": "\033[91m", # Red
|
|
43
|
+
"warning": "\033[93m", # Yellow
|
|
44
|
+
"info": "\033[94m", # Blue
|
|
45
|
+
"success": "\033[92m", # Green
|
|
46
|
+
"secondary": "\033[90m", # Gray
|
|
47
|
+
}
|
|
48
|
+
reset = "\033[0m"
|
|
49
|
+
color = level_colors.get(self.level, reset)
|
|
50
|
+
lines.append(f"{color}╔═ {self.title} ═╗{reset}")
|
|
51
|
+
else:
|
|
52
|
+
lines.append(f"╔═ {self.title} ═╗")
|
|
53
|
+
|
|
54
|
+
# Add body
|
|
55
|
+
if self.body:
|
|
56
|
+
lines.append(self.body)
|
|
57
|
+
|
|
58
|
+
# Add footer/fix if present
|
|
59
|
+
if self.footer:
|
|
60
|
+
if _supports_color():
|
|
61
|
+
lines.append(f"\033[2m{self.footer}\033[0m") # Dimmed text
|
|
62
|
+
else:
|
|
63
|
+
lines.append(self.footer)
|
|
64
|
+
|
|
65
|
+
return "\n".join(lines)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _silent_error_handler(shell, etype, evalue, tb, tb_offset=None):
|
|
69
|
+
"""Display a styled error card using Bootstrap 5.3.0."""
|
|
70
|
+
try:
|
|
71
|
+
from IPython.display import HTML, display
|
|
72
|
+
except ImportError:
|
|
73
|
+
# Fallback to console output if IPython not available
|
|
74
|
+
print(str(evalue), file=sys.stderr)
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
footer_html = (
|
|
78
|
+
f'<div class="card-footer text-muted">{evalue.footer}</div>'
|
|
79
|
+
if evalue.footer
|
|
80
|
+
else ""
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
html = f"""
|
|
84
|
+
<!DOCTYPE html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<meta charset="utf-8">
|
|
88
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
89
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div class="container-fluid px-0">
|
|
93
|
+
<div class="card border-{evalue.level} mb-3 shadow-sm" style="max-width: 42rem;">
|
|
94
|
+
<div class="card-header bg-{evalue.level} text-white fw-bold">
|
|
95
|
+
{evalue.title}
|
|
96
|
+
</div>
|
|
97
|
+
<div class="card-body">
|
|
98
|
+
<div class="card-text">
|
|
99
|
+
{evalue.body}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
{footer_html}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
|
107
|
+
"""
|
|
108
|
+
display(HTML(html))
|
|
109
|
+
return [] # suppress traceback completely
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def install_silent_error_handler() -> bool:
|
|
113
|
+
"""Install a custom error handler for IPython notebooks that displays a styled error card."""
|
|
114
|
+
try:
|
|
115
|
+
from IPython import get_ipython
|
|
116
|
+
except ImportError:
|
|
117
|
+
return False
|
|
118
|
+
ip = get_ipython()
|
|
119
|
+
if ip is None or "pytest" in sys.modules:
|
|
120
|
+
return False
|
|
121
|
+
ip.set_custom_exc((DeepOriginException,), _silent_error_handler)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
install_silent_error_handler()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Platform client module.
|
|
2
|
+
|
|
3
|
+
Provides the `DeepOriginClient` used to interact with the Deep Origin platform.
|
|
4
|
+
|
|
5
|
+
This module supports configuration via keyword arguments or the following
|
|
6
|
+
environment variables when keywords are omitted:
|
|
7
|
+
|
|
8
|
+
- `DEEPORIGIN_TOKEN`
|
|
9
|
+
- `DEEPORIGIN_ENV` (defaults to "prod" if not provided)
|
|
10
|
+
- `DEEPORIGIN_ORG_KEY`
|
|
11
|
+
|
|
12
|
+
The client automatically caches instances based on (base_url, token, org_key, tag),
|
|
13
|
+
so calling `DeepOriginClient()` multiple times with the same parameters returns
|
|
14
|
+
the same cached instance, reusing connection pools.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
client = DeepOriginClient() # Uses singleton cache automatically
|
|
18
|
+
client.tag = "my-tag" # Set tag for all function runs
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from deeporigin_sdk.platform.client import DeepOriginClient
|
|
22
|
+
|
|
23
|
+
__all__ = ["DeepOriginClient"]
|