sap-ai-sdk-core 2.9.9__py3-none-any.whl → 3.0.8__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.
- ai_core_sdk/ai_core_v2_client.py +157 -0
- ai_core_sdk/cli.py +172 -0
- ai_core_sdk/credentials.py +196 -0
- ai_core_sdk/exception.py +11 -0
- ai_core_sdk/helpers/__init__.py +39 -0
- ai_core_sdk/helpers/constants.py +18 -0
- ai_core_sdk/helpers/logging.py +23 -0
- ai_core_sdk/models/__init__.py +33 -0
- ai_core_sdk/models/application.py +37 -0
- ai_core_sdk/models/application_query_response.py +30 -0
- ai_core_sdk/models/application_resource_sync_status.py +34 -0
- ai_core_sdk/models/application_source.py +34 -0
- ai_core_sdk/models/application_status.py +66 -0
- ai_core_sdk/models/base_models.py +62 -0
- ai_core_sdk/models/docker_registry_secret.py +23 -0
- ai_core_sdk/models/docker_registry_secret_query_response.py +30 -0
- ai_core_sdk/models/kpi.py +25 -0
- ai_core_sdk/models/object_store_secret.py +32 -0
- ai_core_sdk/models/object_store_secret_query_response.py +30 -0
- ai_core_sdk/models/repository.py +36 -0
- ai_core_sdk/models/repository_query_response.py +30 -0
- ai_core_sdk/models/repository_status.py +9 -0
- ai_core_sdk/models/resource_group.py +50 -0
- ai_core_sdk/models/resource_group_query_response.py +31 -0
- ai_core_sdk/models/resource_group_status.py +9 -0
- ai_core_sdk/models/secret.py +30 -0
- ai_core_sdk/models/secret_query_response.py +30 -0
- ai_core_sdk/resource_clients/__init__.py +13 -0
- ai_core_sdk/resource_clients/applications_client.py +173 -0
- ai_core_sdk/resource_clients/docker_registry_secrets_client.py +117 -0
- ai_core_sdk/resource_clients/internal_rest_client.py +52 -0
- ai_core_sdk/resource_clients/kpi_client.py +26 -0
- ai_core_sdk/resource_clients/metrics_client.py +131 -0
- ai_core_sdk/resource_clients/object_store_secrets_client.py +215 -0
- ai_core_sdk/resource_clients/repositories_client.py +116 -0
- ai_core_sdk/resource_clients/secrets_client.py +148 -0
- ai_core_sdk/tracking/__init__.py +2 -0
- ai_core_sdk/tracking/tracking.py +215 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.ai_core_v2_client.html +127 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.cli.html +59 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.credentials.html +209 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.exception.html +161 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.helpers.constants.html +90 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.helpers.html +52 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.helpers.logging.html +41 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.html +29 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.application.html +79 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.application_query_response.html +86 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.application_resource_sync_status.html +77 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.application_source.html +77 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.application_status.html +90 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.base_models.html +120 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.docker_registry_secret.html +85 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.docker_registry_secret_query_response.html +86 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.html +40 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.kpi.html +73 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.object_store_secret.html +71 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.object_store_secret_query_response.html +86 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.repository.html +77 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.repository_query_response.html +86 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.repository_status.html +69 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.resource_group.html +85 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.resource_group_query_response.html +86 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.resource_group_status.html +69 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.secret.html +76 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.models.secret_query_response.html +86 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.applications_client.html +186 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.docker_registry_secrets_client.html +147 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.html +29 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.internal_rest_client.html +181 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.kpi_client.html +87 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.metrics_client.html +189 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.object_store_secrets_client.html +205 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.repositories_client.html +148 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.resource_groups_client.html +156 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.resource_clients.secrets_client.html +165 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.tracking.html +22 -0
- sap_ai_sdk_core-3.0.8.data/data/docs/ai_core_sdk.tracking.tracking.html +224 -0
- sap_ai_sdk_core-3.0.8.dist-info/METADATA +253 -0
- sap_ai_sdk_core-3.0.8.dist-info/RECORD +84 -0
- {sap_ai_sdk_core-2.9.9.dist-info → sap_ai_sdk_core-3.0.8.dist-info}/WHEEL +1 -1
- sap_ai_sdk_core-3.0.8.dist-info/top_level.txt +1 -0
- sap_ai_sdk_core-2.9.9.dist-info/METADATA +0 -43
- sap_ai_sdk_core-2.9.9.dist-info/RECORD +0 -6
- sap_ai_sdk_core-2.9.9.dist-info/top_level.txt +0 -1
- {sap-ai-sdk-core → ai_core_sdk}/__init__.py +0 -0
- {sap_ai_sdk_core-2.9.9.dist-info/licenses → sap_ai_sdk_core-3.0.8.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from ai_core_sdk.helpers.logging import get_logger
|
|
2
|
+
from typing import Callable
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from ai_core_sdk.helpers import is_within_aicore
|
|
6
|
+
from ai_core_sdk.resource_clients import (
|
|
7
|
+
AIAPIV2Client,
|
|
8
|
+
ArtifactClient,
|
|
9
|
+
ConfigurationClient,
|
|
10
|
+
DeploymentClient,
|
|
11
|
+
ExecutableClient,
|
|
12
|
+
ExecutionClient,
|
|
13
|
+
RestClient,
|
|
14
|
+
ScenarioClient,
|
|
15
|
+
ResourceGroupsClient,
|
|
16
|
+
MetaClient,
|
|
17
|
+
ModelClient,
|
|
18
|
+
)
|
|
19
|
+
from ai_core_sdk.resource_clients.applications_client import ApplicationsClient
|
|
20
|
+
from ai_core_sdk.resource_clients.docker_registry_secrets_client import DockerRegistrySecretsClient
|
|
21
|
+
from ai_core_sdk.resource_clients.internal_rest_client import InternalRestClient
|
|
22
|
+
from ai_core_sdk.resource_clients.metrics_client import MetricsCoreClient
|
|
23
|
+
from ai_core_sdk.resource_clients.object_store_secrets_client import ObjectStoreSecretsClient
|
|
24
|
+
from ai_core_sdk.resource_clients.kpi_client import KpiClient
|
|
25
|
+
from ai_core_sdk.resource_clients.repositories_client import RepositoriesClient
|
|
26
|
+
from ai_core_sdk.resource_clients.secrets_client import SecretsClient
|
|
27
|
+
from ai_core_sdk.helpers.constants import Timeouts
|
|
28
|
+
from ai_core_sdk.credentials import fetch_credentials
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AICoreV2Client:
|
|
32
|
+
"""The AICoreV2Client is the class implemented to interact with the AI Core endpoints. The user can use its
|
|
33
|
+
attributes corresponding to the resources, for interacting with endpoints related to that resource. (i.e.,
|
|
34
|
+
aicoreclient.scenario)
|
|
35
|
+
|
|
36
|
+
:param base_url: Base URL of the AI Core. Should include the base path as well. (i.e., "<base_url>/lm/scenarios"
|
|
37
|
+
should work)
|
|
38
|
+
:type base_url: str
|
|
39
|
+
:param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token), defaults to
|
|
40
|
+
None
|
|
41
|
+
:type auth_url: str, optional
|
|
42
|
+
:param client_id: client id to be used for authorization, defaults to None
|
|
43
|
+
:type client_id: str, optional
|
|
44
|
+
:param client_secret: client secret to be used for authorization, defaults to None
|
|
45
|
+
:type client_secret: str, optional
|
|
46
|
+
:param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None
|
|
47
|
+
:type cert_str: str, optional
|
|
48
|
+
:param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None
|
|
49
|
+
:type key_str: str, optional
|
|
50
|
+
:param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter,
|
|
51
|
+
defaults to None
|
|
52
|
+
:type cert_file_path: str, optional
|
|
53
|
+
:param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter,
|
|
54
|
+
defaults to None
|
|
55
|
+
:type key_file_path: str, optional
|
|
56
|
+
:param token_creator: the function which returns the Bearer token, when called. Either this, or
|
|
57
|
+
auth_url & client_id & client_secret should be specified, defaults to None
|
|
58
|
+
:type token_creator: Callable[[], str], optional
|
|
59
|
+
:param resource_group: The default resource group which will be used while sending the requests to the server. If
|
|
60
|
+
not set, the resource_group should be specified with every request to the server, defaults to None
|
|
61
|
+
:type resource_group: str, optional
|
|
62
|
+
:param read_timeout: Read timeout for requests in seconds, defaults to 60s
|
|
63
|
+
:type read_timeout: int
|
|
64
|
+
:param connect_timeout: Connect timeout for requests in seconds, defaults to 60s
|
|
65
|
+
:type connect_timeout: int
|
|
66
|
+
:param num_request_retries: Number of retries for failing requests with http status code 429, 500, 502, 503 or 504,
|
|
67
|
+
defaults to 60s
|
|
68
|
+
:type num_request_retries: int
|
|
69
|
+
"""
|
|
70
|
+
logger = get_logger()
|
|
71
|
+
|
|
72
|
+
def __init__(self, base_url: str, auth_url: str = None, client_id: str = None, client_secret: str = None,
|
|
73
|
+
cert_str: str = None, key_str: str = None, cert_file_path: str = None, key_file_path: str = None,
|
|
74
|
+
token_creator: Callable[[], str] = None, resource_group: str = None,
|
|
75
|
+
read_timeout=Timeouts.READ_TIMEOUT.value, connect_timeout=Timeouts.CONNECT_TIMEOUT.value,
|
|
76
|
+
num_request_retries=Timeouts.NUM_REQUEST_RETRIES.value):
|
|
77
|
+
self.base_url: str = base_url
|
|
78
|
+
ai_api_base_url = f'{base_url}/lm'
|
|
79
|
+
token_creator = AIAPIV2Client._create_token_creator_if_does_not_exist(
|
|
80
|
+
token_creator=token_creator, auth_url=auth_url, client_id=client_id, client_secret=client_secret,
|
|
81
|
+
cert_str=cert_str, key_str=key_str, cert_file_path=cert_file_path, key_file_path=key_file_path)
|
|
82
|
+
|
|
83
|
+
client_type = "AI Core Python SDK"
|
|
84
|
+
|
|
85
|
+
ai_api_v2_client = AIAPIV2Client(base_url=ai_api_base_url, token_creator=token_creator,
|
|
86
|
+
resource_group=resource_group, read_timeout=read_timeout,
|
|
87
|
+
connect_timeout=connect_timeout, num_request_retries=num_request_retries,
|
|
88
|
+
client_type=client_type)
|
|
89
|
+
|
|
90
|
+
self.rest_client: RestClient = RestClient(base_url=base_url, get_token=token_creator,
|
|
91
|
+
resource_group=resource_group, read_timeout=read_timeout,
|
|
92
|
+
connect_timeout=connect_timeout,
|
|
93
|
+
num_request_retries=num_request_retries,
|
|
94
|
+
client_type=client_type)
|
|
95
|
+
self.artifact: ArtifactClient = ai_api_v2_client.artifact
|
|
96
|
+
self.configuration: ConfigurationClient = ai_api_v2_client.configuration
|
|
97
|
+
self.deployment: DeploymentClient = ai_api_v2_client.deployment
|
|
98
|
+
self.executable: ExecutableClient = ai_api_v2_client.executable
|
|
99
|
+
self.execution: ExecutionClient = ai_api_v2_client.execution
|
|
100
|
+
self.resource_groups: ResourceGroupsClient = ai_api_v2_client.resource_groups
|
|
101
|
+
self.meta: MetaClient = ai_api_v2_client.meta
|
|
102
|
+
self.model: ModelClient = ai_api_v2_client.model
|
|
103
|
+
# If the environment variables have AICORE_EXECUTION_ID and AICORE_TRACKING_ENDPOINT,
|
|
104
|
+
# it indicates the sdk is used within the training pod
|
|
105
|
+
# Initiating an internal rest client if within the training pod
|
|
106
|
+
# Else initiating the rest client from ai_api_v2_client
|
|
107
|
+
if is_within_aicore():
|
|
108
|
+
self.metrics: MetricsCoreClient = MetricsCoreClient(
|
|
109
|
+
rest_client=InternalRestClient(
|
|
110
|
+
client_type=client_type,
|
|
111
|
+
read_timeout=read_timeout,
|
|
112
|
+
connect_timeout=connect_timeout,
|
|
113
|
+
num_request_retries=num_request_retries,
|
|
114
|
+
),
|
|
115
|
+
execution_id=os.getenv("AICORE_EXECUTION_ID"),
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
self.metrics: MetricsCoreClient = MetricsCoreClient(rest_client=ai_api_v2_client.rest_client)
|
|
119
|
+
self.scenario: ScenarioClient = ai_api_v2_client.scenario
|
|
120
|
+
self.docker_registry_secrets: DockerRegistrySecretsClient = DockerRegistrySecretsClient(
|
|
121
|
+
rest_client=self.rest_client)
|
|
122
|
+
self.applications: ApplicationsClient = ApplicationsClient(rest_client=self.rest_client)
|
|
123
|
+
self.object_store_secrets: ObjectStoreSecretsClient = ObjectStoreSecretsClient(rest_client=self.rest_client)
|
|
124
|
+
self.secrets: SecretsClient = SecretsClient(rest_client=self.rest_client)
|
|
125
|
+
self.kpis: KpiClient = KpiClient(rest_client=self.rest_client)
|
|
126
|
+
self.repositories: RepositoriesClient = RepositoriesClient(rest_client=self.rest_client)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def from_env(profile_name: str = None,
|
|
130
|
+
**kwargs):
|
|
131
|
+
"""Alternative way to create an AICoreV2Client object.
|
|
132
|
+
Parameters for base_url, auth_url, client_id, client_secret, x.509 credentials (either as file path or string)
|
|
133
|
+
and resource_group can be passed as keyword or are pulled from environment variables.
|
|
134
|
+
It is also possible to use a profile, which is a json file in the config directory. The profile name can be
|
|
135
|
+
passed as keyword or is pulled from the environment variable AICORE_PROFILE. If no profile is specified,
|
|
136
|
+
the default profile is used.
|
|
137
|
+
A specific path to a config, that should be used, can be set via the environment variable AICORE_CONFIG.
|
|
138
|
+
The hierarchy of precedence is:
|
|
139
|
+
1. keyword argument
|
|
140
|
+
2. environment variable
|
|
141
|
+
3. configuration file
|
|
142
|
+
4. value from VCAP_SERVICES environment variable, if exists
|
|
143
|
+
|
|
144
|
+
:param profile_name: name of the profile to use, defaults to None. If None is passed, the profile is read from
|
|
145
|
+
the environment variable AICORE_PROFILE. If this is not set, the default profile is used.
|
|
146
|
+
The default profile is read from $AICORE_HOME/config.json.
|
|
147
|
+
:type profile_name: optional, str
|
|
148
|
+
**kwargs: check the parameters of the class constructor
|
|
149
|
+
"""
|
|
150
|
+
env_credentials = fetch_credentials(profile=profile_name, **kwargs)
|
|
151
|
+
|
|
152
|
+
# if cert_url is present in the fetched credentials, rename it to auth_url
|
|
153
|
+
if 'cert_url' in env_credentials.keys():
|
|
154
|
+
env_credentials['auth_url'] = env_credentials.pop('cert_url')
|
|
155
|
+
|
|
156
|
+
kwargs.update(env_credentials)
|
|
157
|
+
return AICoreV2Client(**kwargs)
|
ai_core_sdk/cli.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
from ai_core_sdk.credentials import CREDENTIAL_VALUES, get_nested_value
|
|
8
|
+
from ai_core_sdk.helpers import get_home
|
|
9
|
+
from ai_core_sdk.helpers.constants import AI_CORE_PREFIX
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
# Constants
|
|
14
|
+
MAX_TRIES = 5
|
|
15
|
+
OAUTH_TOKEN_SUFFIX = '/oauth/token'
|
|
16
|
+
API_V2_SUFFIX = '/v2'
|
|
17
|
+
DEFAULT_CONFIG = 'config.json'
|
|
18
|
+
DEFAULT_RESOURCE_GROUP = 'default'
|
|
19
|
+
DEFAULT_PROFILE = 'default'
|
|
20
|
+
|
|
21
|
+
# Utility Functions
|
|
22
|
+
def create_config(**kwargs):
|
|
23
|
+
return {f'{AI_CORE_PREFIX}_{k}'.upper(): v for k, v in kwargs.items() if v is not None}
|
|
24
|
+
|
|
25
|
+
def is_valid_url(url, path_forbidden=True):
|
|
26
|
+
try:
|
|
27
|
+
result = urlparse(url)
|
|
28
|
+
return all([result.scheme, result.netloc, not result.path if path_forbidden else True])
|
|
29
|
+
except ValueError:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def prompt_for_input(prompt_text, is_url=False, path_forbidden=True):
|
|
33
|
+
url = None
|
|
34
|
+
for _ in range(MAX_TRIES):
|
|
35
|
+
user_input = click.prompt(prompt_text, type=str).rstrip('/')
|
|
36
|
+
if is_url and not is_valid_url(user_input, path_forbidden):
|
|
37
|
+
click.echo('Input is not a valid URL.')
|
|
38
|
+
if path_forbidden:
|
|
39
|
+
click.echo('Enter URL without any additional path or trailing slash.')
|
|
40
|
+
else:
|
|
41
|
+
url = user_input
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
if url is None:
|
|
45
|
+
raise ValueError('Max tries reached!')
|
|
46
|
+
return url
|
|
47
|
+
|
|
48
|
+
# CLI Functions
|
|
49
|
+
@click.group()
|
|
50
|
+
@click.option('-p', '--profile', default=DEFAULT_PROFILE, type=str)
|
|
51
|
+
@click.pass_context
|
|
52
|
+
def cli(ctx, profile):
|
|
53
|
+
"""CLI group for the AI Core SDK"""
|
|
54
|
+
ctx.ensure_object(dict)
|
|
55
|
+
ctx.obj['profile'] = profile
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_service_key(service_key_json: str):
|
|
59
|
+
with pathlib.Path(service_key_json).open() as stream:
|
|
60
|
+
service_key = json.load(stream)
|
|
61
|
+
kwargs = {}
|
|
62
|
+
for value in CREDENTIAL_VALUES:
|
|
63
|
+
# In VCAP the service_key is nested under credentials
|
|
64
|
+
# We can reuse the vcap_name from the CREDENTIAL_VALUES
|
|
65
|
+
# when parsing the service_key
|
|
66
|
+
# skip if vcap_key not defined
|
|
67
|
+
if not value.vcap_key:
|
|
68
|
+
continue
|
|
69
|
+
try:
|
|
70
|
+
kwargs[value.name] = get_nested_value(service_key, value.vcap_key[1:])
|
|
71
|
+
except KeyError:
|
|
72
|
+
kwargs[value.name] = None
|
|
73
|
+
return kwargs
|
|
74
|
+
|
|
75
|
+
def get_auth_url(auth_url: Optional[str]=None):
|
|
76
|
+
auth_url = auth_url or prompt_for_input('Please enter the authorization URL', is_url=True, path_forbidden=False)
|
|
77
|
+
if auth_url.endswith(OAUTH_TOKEN_SUFFIX):
|
|
78
|
+
return auth_url
|
|
79
|
+
else:
|
|
80
|
+
return auth_url + OAUTH_TOKEN_SUFFIX
|
|
81
|
+
|
|
82
|
+
def get_base_url(base_url: Optional[str]=None):
|
|
83
|
+
base_url = base_url or prompt_for_input('Please enter the base API URL', is_url=True, path_forbidden=False)
|
|
84
|
+
if base_url.endswith(API_V2_SUFFIX):
|
|
85
|
+
return base_url
|
|
86
|
+
else:
|
|
87
|
+
return base_url + API_V2_SUFFIX
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_str_value(msg: str, value: Optional[str]=None):
|
|
91
|
+
return value or click.prompt(msg, type=str)
|
|
92
|
+
|
|
93
|
+
def confirm_resource_group(resource_group: Optional[str]=None):
|
|
94
|
+
if resource_group is None:
|
|
95
|
+
resource_group = click.prompt('Please confirm or enter the AICore resource group', default=DEFAULT_RESOURCE_GROUP, type=str)
|
|
96
|
+
return resource_group
|
|
97
|
+
|
|
98
|
+
def get_profile_config_path(profile: str):
|
|
99
|
+
profile = profile if profile != DEFAULT_PROFILE and profile is not None else None
|
|
100
|
+
home = pathlib.Path(get_home())
|
|
101
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
config_path = home / (DEFAULT_CONFIG if profile is None else f'config_{profile.lower()}.json')
|
|
103
|
+
if profile is not None:
|
|
104
|
+
click.echo(f'Remember to set `{AI_CORE_PREFIX}_PROFILE={profile.lower()}` to use your profile.')
|
|
105
|
+
return config_path
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_config_file(config_path: pathlib.Path, auth_url: str, client_id: str, client_secret: str,
|
|
109
|
+
cert_file_path: pathlib.Path, key_file_path: pathlib.Path, base_url: str, resource_group: str):
|
|
110
|
+
config = create_config(auth_url=auth_url, client_id=client_id, client_secret=client_secret,
|
|
111
|
+
cert_file_path=cert_file_path, key_file_path=key_file_path, base_url=base_url,
|
|
112
|
+
resource_group=resource_group)
|
|
113
|
+
if config_path.exists() and not click.confirm(f'A config file {config_path} already exists. Do you want to replace it?'):
|
|
114
|
+
exit()
|
|
115
|
+
click.echo(f'Creating new config {config_path}')
|
|
116
|
+
with config_path.open('w') as stream:
|
|
117
|
+
json.dump(config, stream, indent=4)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@cli.command()
|
|
121
|
+
@click.option('-a', '--auth-url', default=None, type=str)
|
|
122
|
+
@click.option('-s', '--client-secret', default=None, type=str)
|
|
123
|
+
@click.option('-i', '--client-id', default=None, type=str)
|
|
124
|
+
@click.option('-cf', '--cert-file-path', default=None, type=click.Path(exists=True, file_okay=True, dir_okay=False))
|
|
125
|
+
@click.option('-kf', '--key-file-path', default=None, type=click.Path(exists=True, file_okay=True, dir_okay=False))
|
|
126
|
+
@click.option('-u', '--base-url', default=None, type=str)
|
|
127
|
+
@click.option('-g', '--resource-group', default=None, type=str)
|
|
128
|
+
@click.option('-k', '--service-key-json', default=None, type=click.Path(exists=True, file_okay=True, dir_okay=False))
|
|
129
|
+
@click.pass_context
|
|
130
|
+
def configure(ctx, auth_url, client_secret, client_id, cert_file_path, key_file_path, base_url, resource_group,
|
|
131
|
+
service_key_json):
|
|
132
|
+
profile = ctx.obj['profile']
|
|
133
|
+
if service_key_json:
|
|
134
|
+
service_key_json = load_service_key(service_key_json)
|
|
135
|
+
else:
|
|
136
|
+
service_key_json = {}
|
|
137
|
+
|
|
138
|
+
base_url = get_base_url(service_key_json.get('base_url', base_url))
|
|
139
|
+
auth_url = get_auth_url(service_key_json.get('auth_url', auth_url))
|
|
140
|
+
cert_url = service_key_json.get('cert_url', None)
|
|
141
|
+
if cert_url:
|
|
142
|
+
auth_url = get_auth_url(cert_url)
|
|
143
|
+
client_id = get_str_value('Please enter the client ID', service_key_json.get('client_id', client_id))
|
|
144
|
+
|
|
145
|
+
client_secret = service_key_json.get('client_secret', client_secret)
|
|
146
|
+
cert_file_path = service_key_json.get('cert_file_path', cert_file_path)
|
|
147
|
+
key_file_path = service_key_json.get('key_file_path', key_file_path)
|
|
148
|
+
|
|
149
|
+
if not (client_secret is not None or (key_file_path is not None or cert_file_path is not None)):
|
|
150
|
+
client_secret = get_str_value(
|
|
151
|
+
'Please enter the client secret (skip, if you\'re going to provide X.509 credentials',
|
|
152
|
+
service_key_json.get('client_secret', client_secret))
|
|
153
|
+
cert_file_path = get_str_value('Please enter path to the X.509 certificate file',
|
|
154
|
+
service_key_json.get('cert_file_path', cert_file_path))
|
|
155
|
+
key_file_path = get_str_value('Please enter path to the X.509 key file',
|
|
156
|
+
service_key_json.get('key_file_path', key_file_path))
|
|
157
|
+
resource_group = confirm_resource_group(resource_group)
|
|
158
|
+
create_config_file(
|
|
159
|
+
config_path=get_profile_config_path(profile),
|
|
160
|
+
auth_url=auth_url,
|
|
161
|
+
client_id=client_id,
|
|
162
|
+
client_secret=client_secret,
|
|
163
|
+
cert_file_path=cert_file_path,
|
|
164
|
+
key_file_path=key_file_path,
|
|
165
|
+
base_url=base_url,
|
|
166
|
+
resource_group=resource_group
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
cli() #pylint: disable = no-value-for-parameter
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Final, List, Optional, Callable, Union, Tuple
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from ai_core_sdk.helpers import get_home
|
|
11
|
+
from ai_core_sdk.helpers.constants import (AI_CORE_PREFIX, AUTH_ENDPOINT_SUFFIX, CONFIG_FILE_ENV_VAR, PROFILE_ENV_VAR,
|
|
12
|
+
VCAP_AICORE_SERVICE_NAME, VCAP_SERVICES_ENV_VAR)
|
|
13
|
+
from ai_core_sdk.helpers.logging import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_nested_value(data_dict, keys: List[str]):
|
|
19
|
+
"""
|
|
20
|
+
Retrieve a nested value from a dictionary using a list of strings.
|
|
21
|
+
|
|
22
|
+
:param data_dict: The dictionary to search.
|
|
23
|
+
:param keys: A list of strings representing nested keys.
|
|
24
|
+
:return: The value associated with the nested keys, or None if not found.
|
|
25
|
+
"""
|
|
26
|
+
current_value = data_dict
|
|
27
|
+
for key in keys:
|
|
28
|
+
current_value = current_value[key]
|
|
29
|
+
return current_value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class VCAPEnvironment:
|
|
34
|
+
services: List[Service]
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_env(cls, env_var: Optional[str] = None):
|
|
38
|
+
env_var = env_var or VCAP_SERVICES_ENV_VAR
|
|
39
|
+
env = json.loads(os.environ.get(env_var, '{}'))
|
|
40
|
+
return cls.from_dict(env)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_dict(cls, env: Dict[str, Any]):
|
|
44
|
+
services = [Service(service) for services in env.values() for service in services]
|
|
45
|
+
return cls(services=services)
|
|
46
|
+
|
|
47
|
+
def __getitem__(self, name) -> Service:
|
|
48
|
+
return self.get_service(name, exactly_one=True)
|
|
49
|
+
|
|
50
|
+
def get_service(self, label, exactly_one: bool = True) -> Service:
|
|
51
|
+
services = [s for s in self.services if s.label == label]
|
|
52
|
+
if exactly_one:
|
|
53
|
+
if len(services) == 0:
|
|
54
|
+
raise KeyError(f"No service found with label '{label}'.")
|
|
55
|
+
return services[0]
|
|
56
|
+
else:
|
|
57
|
+
return services
|
|
58
|
+
|
|
59
|
+
def get_service_by_name(self, name, exactly_one: bool = True) -> Service:
|
|
60
|
+
services = [s for s in self.services if s.name == name]
|
|
61
|
+
if exactly_one:
|
|
62
|
+
if len(services) == 0:
|
|
63
|
+
raise KeyError(f"No service found with name '{name}'.")
|
|
64
|
+
return services[0]
|
|
65
|
+
else:
|
|
66
|
+
return services
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
NoDefault = object()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Service:
|
|
73
|
+
|
|
74
|
+
def __init__(self, env: Dict[str, Any]):
|
|
75
|
+
self._env = env
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def label(self) -> Optional[str]:
|
|
79
|
+
return self._env.get('label')
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def name(self) -> Optional[str]:
|
|
83
|
+
return self._env.get('name')
|
|
84
|
+
|
|
85
|
+
def __getitem__(self, key):
|
|
86
|
+
return self.get(key)
|
|
87
|
+
|
|
88
|
+
def get(self, key, default=NoDefault):
|
|
89
|
+
if isinstance(key, str):
|
|
90
|
+
key_splitted = key.split('.')
|
|
91
|
+
else:
|
|
92
|
+
key_splitted = key
|
|
93
|
+
try:
|
|
94
|
+
return get_nested_value(self._env, key_splitted) or default
|
|
95
|
+
except KeyError:
|
|
96
|
+
if default is NoDefault:
|
|
97
|
+
raise KeyError(f"Key '{key}' not found in service '{self.name}'.")
|
|
98
|
+
return default
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class CredentialsValue:
|
|
103
|
+
name: str
|
|
104
|
+
vcap_key: Optional[Tuple[str, ...]] = None
|
|
105
|
+
default: Optional[str] = None
|
|
106
|
+
transform_fn: Optional[Callable] = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
CREDENTIAL_VALUES: Final[List[CredentialsValue]] = [
|
|
110
|
+
CredentialsValue(name='client_id', vcap_key=('credentials', 'clientid')),
|
|
111
|
+
CredentialsValue(name='client_secret', vcap_key=('credentials', 'clientsecret')),
|
|
112
|
+
CredentialsValue(name='auth_url',
|
|
113
|
+
vcap_key=('credentials', 'url'),
|
|
114
|
+
transform_fn=lambda url: url.rstrip('/') +
|
|
115
|
+
('' if url.endswith(AUTH_ENDPOINT_SUFFIX) else AUTH_ENDPOINT_SUFFIX)),
|
|
116
|
+
CredentialsValue(name='base_url',
|
|
117
|
+
vcap_key=('credentials', 'serviceurls', 'AI_API_URL'),
|
|
118
|
+
transform_fn=lambda url: url.rstrip('/') + ('' if url.endswith('/v2') else '/v2')),
|
|
119
|
+
CredentialsValue(name='resource_group'),
|
|
120
|
+
CredentialsValue(name='cert_url', vcap_key=('credentials', 'certurl'),
|
|
121
|
+
transform_fn=lambda url: url.rstrip('/') +
|
|
122
|
+
('' if url.endswith(AUTH_ENDPOINT_SUFFIX) else AUTH_ENDPOINT_SUFFIX)),
|
|
123
|
+
# Even though the certificate and key in VCAP_SERVICES are not file paths, the names are defined this way in order
|
|
124
|
+
# to keep it compatible with the config names. It'll be handled in fetch_credentials function.
|
|
125
|
+
CredentialsValue(name='cert_file_path'),
|
|
126
|
+
CredentialsValue(name='key_file_path'),
|
|
127
|
+
CredentialsValue(name='cert_str', vcap_key=('credentials', 'certificate'),
|
|
128
|
+
transform_fn=lambda cert_str: cert_str.replace('\\n', '\n')),
|
|
129
|
+
CredentialsValue(name='key_str', vcap_key=('credentials', 'key'),
|
|
130
|
+
transform_fn=lambda key_str: key_str.replace('\\n', '\n'))
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def init_conf(profile: str = None):
|
|
135
|
+
# Read configuration from ${AICORE_HOME}/config_<profile>.json.
|
|
136
|
+
home = pathlib.Path(get_home())
|
|
137
|
+
profile = profile or os.environ.get(PROFILE_ENV_VAR)
|
|
138
|
+
profile_config_file = f'config_{profile}.json'
|
|
139
|
+
direct_config_file = pathlib.Path(os.getenv(CONFIG_FILE_ENV_VAR)) if os.getenv(CONFIG_FILE_ENV_VAR) else None
|
|
140
|
+
path_to_config = (direct_config_file or
|
|
141
|
+
(home / ('config.json' if profile in ('default', '', None) else profile_config_file)))
|
|
142
|
+
config = {}
|
|
143
|
+
if path_to_config.exists():
|
|
144
|
+
logger.debug('Config file path %s', path_to_config)
|
|
145
|
+
try:
|
|
146
|
+
with path_to_config.open(encoding='utf-8') as f:
|
|
147
|
+
return json.load(f)
|
|
148
|
+
except json.decoder.JSONDecodeError:
|
|
149
|
+
raise KeyError(f'{path_to_config} is not a valid json file. Please fix or remove it!')
|
|
150
|
+
elif profile:
|
|
151
|
+
raise FileNotFoundError(f"Unable to locate profile config file '{profile_config_file}' "
|
|
152
|
+
f"in AICORE_HOME '{home}')")
|
|
153
|
+
return config
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def fetch_credentials(profile: str = None, **kwargs) -> Dict[str, str]:
|
|
157
|
+
config = init_conf(profile=profile)
|
|
158
|
+
try:
|
|
159
|
+
vcap_service = VCAPEnvironment.from_env()[VCAP_AICORE_SERVICE_NAME]
|
|
160
|
+
except KeyError:
|
|
161
|
+
vcap_service = None
|
|
162
|
+
credentials = {}
|
|
163
|
+
cred_value: CredentialsValue
|
|
164
|
+
for cred_value in CREDENTIAL_VALUES:
|
|
165
|
+
value = resolve_params(config, cred_value, vcap_service, **kwargs)
|
|
166
|
+
if value is not None:
|
|
167
|
+
if cred_value.transform_fn:
|
|
168
|
+
value = cred_value.transform_fn(value)
|
|
169
|
+
credentials[cred_value.name] = value
|
|
170
|
+
return credentials
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def resolve_params(config, cred_value, vcap_service, **kwargs):
|
|
174
|
+
"""
|
|
175
|
+
Resolves the parameter value by checking multiple sources in order:
|
|
176
|
+
kwargs, environment variables, config, VCAP services, and defaults.
|
|
177
|
+
Logs the source of the resolved value.
|
|
178
|
+
"""
|
|
179
|
+
env_var_name = f'{AI_CORE_PREFIX}_{cred_value.name.upper()}'
|
|
180
|
+
sources = {
|
|
181
|
+
"kwargs": kwargs.get(cred_value.name),
|
|
182
|
+
"environment variable": os.getenv(env_var_name),
|
|
183
|
+
"config file": config.get(env_var_name),
|
|
184
|
+
"VCAP service": vcap_service.get(cred_value.vcap_key, None) if vcap_service and cred_value.vcap_key else None,
|
|
185
|
+
"default value": cred_value.default,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Find the first valid source and log it
|
|
189
|
+
for source, value in sources.items():
|
|
190
|
+
if value is not None:
|
|
191
|
+
logger.debug('Using source %s for %s', source, cred_value.name)
|
|
192
|
+
return value
|
|
193
|
+
|
|
194
|
+
# Default case (unlikely due to default in sources)
|
|
195
|
+
logger.debug('Using source %s for %s', 'default value', cred_value.name)
|
|
196
|
+
return cred_value.default
|
ai_core_sdk/exception.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from ai_api_client_sdk.exception import AIAPIAuthenticatorException, AIAPIAuthorizationException, \
|
|
2
|
+
AIAPIInvalidRequestException, AIAPINotFoundException, AIAPIPreconditionFailedException, \
|
|
3
|
+
AIAPIServerException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AICoreSDKException(Exception):
|
|
7
|
+
"""Base Exception class for AI Core SDK exceptions"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AICoreInvalidInputException(AICoreSDKException):
|
|
11
|
+
"""Exception thrown in case of invalid input"""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from ai_api_client_sdk.helpers.authenticator import Authenticator
|
|
5
|
+
from .constants import DEFAULT_HOME_PATH, HOME_PATH_ENV_VAR
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def form_top_skip_params(top: int = None, skip: int = None) -> Dict[str, int]:
|
|
9
|
+
"""
|
|
10
|
+
Frame query param
|
|
11
|
+
|
|
12
|
+
:param top: Number of objects to be retrieved, defaults to None
|
|
13
|
+
:type top: int, optional
|
|
14
|
+
:param skip: Number of objects to be skipped, from the list of the queried objects,
|
|
15
|
+
defaults to None
|
|
16
|
+
:type skip: int, optional
|
|
17
|
+
"""
|
|
18
|
+
params = {}
|
|
19
|
+
if top:
|
|
20
|
+
params['$top'] = top
|
|
21
|
+
if skip:
|
|
22
|
+
params['$skip'] = skip
|
|
23
|
+
if not params:
|
|
24
|
+
params = None
|
|
25
|
+
return params
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_within_aicore() -> bool:
|
|
29
|
+
"""[summary]
|
|
30
|
+
Function to check whether the sdk is used within or out of aicore cluster
|
|
31
|
+
Returns:
|
|
32
|
+
bool: True if the ai-core-sdk is used within aicore cluster
|
|
33
|
+
False if the ai-core-sdk is used outside aicore cluster
|
|
34
|
+
"""
|
|
35
|
+
return os.getenv('AICORE_EXECUTION_ID') and os.getenv('AICORE_TRACKING_ENDPOINT')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_home() -> str:
|
|
39
|
+
return os.environ.get(HOME_PATH_ENV_VAR, DEFAULT_HOME_PATH)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
AI_CORE_PREFIX = 'AICORE'
|
|
5
|
+
AUTH_ENDPOINT_SUFFIX = '/oauth/token'
|
|
6
|
+
CONFIG_FILE_ENV_VAR = f'{AI_CORE_PREFIX}_CONFIG'
|
|
7
|
+
DEBUG_ENV_VAR_NAME = "DEBUG"
|
|
8
|
+
DEFAULT_HOME_PATH = os.path.join(os.path.expanduser('~'), '.aicore')
|
|
9
|
+
HOME_PATH_ENV_VAR = f'{AI_CORE_PREFIX}_HOME'
|
|
10
|
+
PROFILE_ENV_VAR = f'{AI_CORE_PREFIX}_PROFILE'
|
|
11
|
+
VCAP_AICORE_SERVICE_NAME = 'aicore'
|
|
12
|
+
VCAP_SERVICES_ENV_VAR = 'VCAP_SERVICES'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Timeouts(Enum):
|
|
16
|
+
READ_TIMEOUT = 60
|
|
17
|
+
CONNECT_TIMEOUT = 60
|
|
18
|
+
NUM_REQUEST_RETRIES = 3
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from ai_core_sdk.helpers.constants import DEBUG_ENV_VAR_NAME
|
|
5
|
+
|
|
6
|
+
BASE_LOGGER_NAME = "ai_core_sdk"
|
|
7
|
+
DEFAULT_LOG_LEVEL = logging.INFO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_logger(name: str = None):
|
|
11
|
+
# Use a hierarchical logger structure to allow for more granular control
|
|
12
|
+
logger_name = f"{name}" if name else BASE_LOGGER_NAME
|
|
13
|
+
return logging.getLogger(logger_name)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_log_level(logger: logging.Logger, default_level=DEFAULT_LOG_LEVEL):
|
|
17
|
+
# Check if DEBUG is set to "true" (case-insensitive)
|
|
18
|
+
debug_env = os.getenv(DEBUG_ENV_VAR_NAME)
|
|
19
|
+
debug = debug_env is not None and debug_env.lower() == 'true'
|
|
20
|
+
logger.setLevel(logging.DEBUG if debug else default_level)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
set_log_level(get_logger())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from ai_api_client_sdk.models.artifact import Artifact
|
|
2
|
+
from ai_api_client_sdk.models.base_models import (
|
|
3
|
+
BasicResponse,
|
|
4
|
+
KeyValue,
|
|
5
|
+
Name,
|
|
6
|
+
NameValue,
|
|
7
|
+
Order,
|
|
8
|
+
QueryResponse,
|
|
9
|
+
)
|
|
10
|
+
from ai_api_client_sdk.models.configuration import Configuration
|
|
11
|
+
from ai_api_client_sdk.models.deployment import Deployment
|
|
12
|
+
from ai_api_client_sdk.models.executable import Executable
|
|
13
|
+
from ai_api_client_sdk.models.execution import Execution
|
|
14
|
+
from ai_api_client_sdk.models.healthz_status import HealthzStatus
|
|
15
|
+
from ai_api_client_sdk.models.input_artifact import InputArtifact
|
|
16
|
+
from ai_api_client_sdk.models.input_artifact_binding import InputArtifactBinding
|
|
17
|
+
from ai_api_client_sdk.models.label import Label
|
|
18
|
+
from ai_api_client_sdk.models.metric import Metric
|
|
19
|
+
from ai_api_client_sdk.models.metric_custom_info import MetricCustomInfo
|
|
20
|
+
from ai_api_client_sdk.models.metric_label import MetricLabel
|
|
21
|
+
from ai_api_client_sdk.models.metric_resource import MetricResource
|
|
22
|
+
from ai_api_client_sdk.models.metric_tag import MetricTag
|
|
23
|
+
from ai_api_client_sdk.models.metrics_query_response import MetricsQueryResponse
|
|
24
|
+
from ai_api_client_sdk.models.model import Model
|
|
25
|
+
from ai_api_client_sdk.models.model_query_response import ModelQueryResponse
|
|
26
|
+
from ai_api_client_sdk.models.model_version import ModelVersion
|
|
27
|
+
from ai_api_client_sdk.models.output_artifact import OutputArtifact
|
|
28
|
+
from ai_api_client_sdk.models.parameter import Parameter
|
|
29
|
+
from ai_api_client_sdk.models.parameter_binding import ParameterBinding
|
|
30
|
+
from ai_api_client_sdk.models.scenario import Scenario
|
|
31
|
+
from ai_api_client_sdk.models.status import Status
|
|
32
|
+
from ai_api_client_sdk.models.target_status import TargetStatus
|
|
33
|
+
from ai_api_client_sdk.models.version import Version
|