sap-ai-sdk-core 2.9.9__py3-none-any.whl → 3.0.3__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.
Files changed (87) hide show
  1. ai_core_sdk/ai_core_v2_client.py +157 -0
  2. ai_core_sdk/cli.py +172 -0
  3. ai_core_sdk/credentials.py +196 -0
  4. ai_core_sdk/exception.py +11 -0
  5. ai_core_sdk/helpers/__init__.py +39 -0
  6. ai_core_sdk/helpers/constants.py +18 -0
  7. ai_core_sdk/helpers/logging.py +23 -0
  8. ai_core_sdk/models/__init__.py +33 -0
  9. ai_core_sdk/models/application.py +37 -0
  10. ai_core_sdk/models/application_query_response.py +30 -0
  11. ai_core_sdk/models/application_resource_sync_status.py +34 -0
  12. ai_core_sdk/models/application_source.py +34 -0
  13. ai_core_sdk/models/application_status.py +66 -0
  14. ai_core_sdk/models/base_models.py +62 -0
  15. ai_core_sdk/models/docker_registry_secret.py +23 -0
  16. ai_core_sdk/models/docker_registry_secret_query_response.py +30 -0
  17. ai_core_sdk/models/kpi.py +25 -0
  18. ai_core_sdk/models/object_store_secret.py +32 -0
  19. ai_core_sdk/models/object_store_secret_query_response.py +30 -0
  20. ai_core_sdk/models/repository.py +36 -0
  21. ai_core_sdk/models/repository_query_response.py +30 -0
  22. ai_core_sdk/models/repository_status.py +9 -0
  23. ai_core_sdk/models/resource_group.py +50 -0
  24. ai_core_sdk/models/resource_group_query_response.py +31 -0
  25. ai_core_sdk/models/resource_group_status.py +9 -0
  26. ai_core_sdk/models/secret.py +30 -0
  27. ai_core_sdk/models/secret_query_response.py +30 -0
  28. ai_core_sdk/resource_clients/__init__.py +13 -0
  29. ai_core_sdk/resource_clients/applications_client.py +173 -0
  30. ai_core_sdk/resource_clients/docker_registry_secrets_client.py +117 -0
  31. ai_core_sdk/resource_clients/internal_rest_client.py +52 -0
  32. ai_core_sdk/resource_clients/kpi_client.py +26 -0
  33. ai_core_sdk/resource_clients/metrics_client.py +131 -0
  34. ai_core_sdk/resource_clients/object_store_secrets_client.py +215 -0
  35. ai_core_sdk/resource_clients/repositories_client.py +116 -0
  36. ai_core_sdk/resource_clients/secrets_client.py +148 -0
  37. ai_core_sdk/tracking/__init__.py +2 -0
  38. ai_core_sdk/tracking/tracking.py +215 -0
  39. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.ai_core_v2_client.html +127 -0
  40. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.cli.html +59 -0
  41. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.credentials.html +209 -0
  42. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.exception.html +161 -0
  43. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.helpers.constants.html +90 -0
  44. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.helpers.html +52 -0
  45. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.helpers.logging.html +41 -0
  46. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.html +29 -0
  47. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.application.html +79 -0
  48. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.application_query_response.html +86 -0
  49. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.application_resource_sync_status.html +77 -0
  50. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.application_source.html +77 -0
  51. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.application_status.html +90 -0
  52. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.base_models.html +120 -0
  53. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.docker_registry_secret.html +85 -0
  54. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.docker_registry_secret_query_response.html +86 -0
  55. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.html +40 -0
  56. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.kpi.html +73 -0
  57. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.object_store_secret.html +71 -0
  58. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.object_store_secret_query_response.html +86 -0
  59. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.repository.html +77 -0
  60. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.repository_query_response.html +86 -0
  61. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.repository_status.html +69 -0
  62. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.resource_group.html +85 -0
  63. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.resource_group_query_response.html +86 -0
  64. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.resource_group_status.html +69 -0
  65. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.secret.html +76 -0
  66. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.models.secret_query_response.html +86 -0
  67. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.applications_client.html +186 -0
  68. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.docker_registry_secrets_client.html +147 -0
  69. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.html +29 -0
  70. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.internal_rest_client.html +181 -0
  71. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.kpi_client.html +87 -0
  72. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.metrics_client.html +189 -0
  73. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.object_store_secrets_client.html +205 -0
  74. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.repositories_client.html +148 -0
  75. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.resource_groups_client.html +156 -0
  76. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.resource_clients.secrets_client.html +165 -0
  77. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.tracking.html +22 -0
  78. sap_ai_sdk_core-3.0.3.data/data/docs/ai_core_sdk.tracking.tracking.html +224 -0
  79. sap_ai_sdk_core-3.0.3.dist-info/METADATA +253 -0
  80. sap_ai_sdk_core-3.0.3.dist-info/RECORD +84 -0
  81. {sap_ai_sdk_core-2.9.9.dist-info → sap_ai_sdk_core-3.0.3.dist-info}/WHEEL +1 -1
  82. sap_ai_sdk_core-3.0.3.dist-info/top_level.txt +1 -0
  83. sap_ai_sdk_core-2.9.9.dist-info/METADATA +0 -43
  84. sap_ai_sdk_core-2.9.9.dist-info/RECORD +0 -6
  85. sap_ai_sdk_core-2.9.9.dist-info/top_level.txt +0 -1
  86. {sap-ai-sdk-core → ai_core_sdk}/__init__.py +0 -0
  87. {sap_ai_sdk_core-2.9.9.dist-info/licenses → sap_ai_sdk_core-3.0.3.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
@@ -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