# -----------------------------------------------------------------------------------------
# (C) Copyright IBM Corp. 2023-2024.
# https://opensource.org/licenses/BSD-3-Clause
# -----------------------------------------------------------------------------------------
from __future__ import annotations
from typing import Any, TYPE_CHECKING
from warnings import warn
import json
import ibm_watsonx_ai._wrappers.requests as requests
from ibm_watsonx_ai.lifecycle import SpecStates
from ibm_watsonx_ai.metanames import SwSpecMetaNames
from ibm_watsonx_ai.utils import SW_SPEC_DETAILS_TYPE
from ibm_watsonx_ai.utils.utils import _get_id_from_deprecated_uid
from ibm_watsonx_ai.wml_client_error import WMLClientError, ResourceIdByNameNotFound
from ibm_watsonx_ai.wml_resource import WMLResource
_DEFAULT_LIST_LENGTH = 50
if TYPE_CHECKING:
from ibm_watsonx_ai import APIClient
from pandas import DataFrame
[docs]
class SwSpec(WMLResource):
"""Store and manage software specs."""
ConfigurationMetaNames = SwSpecMetaNames()
"""MetaNames for Software Specification creation."""
def __init__(self, client: APIClient):
WMLResource.__init__(self, __name__, client)
self.software_spec_list = None
[docs]
def get_details(
self, sw_spec_id: str | None = None, state_info: bool = False, **kwargs: Any
) -> dict[str, Any]:
"""Get software specification details. If no sw_spec_id is passed, details for all software specifications
are returned.
:param sw_spec_id: ID of the software specification
:type sw_spec_id: str, optional
:param state_info: works only when `sw_spec_id` is None, instead of returning details of software specs, it returns
the state of the software specs information (supported, unsupported, deprecated), containing suggested replacement
in case of unsupported or deprecated software specs
:type sw_spec_id: bool
:return: metadata of the stored software specification(s)
:rtype:
- **dict** - if `sw_spec_uid` is not None
- **{"resources": [dict]}** - if `sw_spec_uid` is None
**Examples**
.. code-block:: python
sw_spec_details = client.software_specifications.get_details(sw_spec_uid)
sw_spec_details = client.software_specifications.get_details()
sw_spec_state_details = client.software_specifications.get_details(state_info=True)
"""
sw_spec_id = _get_id_from_deprecated_uid(
kwargs, sw_spec_id, "sw_spec", can_be_none=True
)
SwSpec._validate_type(sw_spec_id, "sw_spec_id", str, False)
if sw_spec_id:
response = requests.get(
self._client.service_instance._href_definitions.get_sw_spec_href(
sw_spec_id
),
params=self._client._params(skip_space_project_chk=True),
headers=self._client._get_headers(),
)
if response.status_code == 200:
return self._get_required_element_from_response(
self._handle_response(200, "get sw spec details", response)
)
else:
return self._handle_response(200, "get sw spec details", response)
else:
if state_info:
response = requests.get(
self._client.service_instance._href_definitions.get_software_specifications_list_href(),
params=self._client._params(),
headers=self._client._get_headers(),
)
return {
"resources": self._handle_response(
200, "get sw specs details", response
)["results"]
}
else:
response = requests.get(
self._client.service_instance._href_definitions.get_sw_specs_href(),
params=self._client._params(
skip_space_project_chk=self._client.ICP_PLATFORM_SPACES
),
headers=self._client._get_headers(),
)
return {
"resources": [
self._get_required_element_from_response(x)
for x in self._handle_response(
200, "get sw specs details", response
)["resources"]
]
}
[docs]
def store(self, meta_props: dict) -> dict:
"""Create a software specification.
:param meta_props: metadata of the space configuration. To see available meta names, use:
.. code-block:: python
client.software_specifications.ConfigurationMetaNames.get()
:type meta_props: dict
:return: metadata of the stored space
:rtype: dict
**Example:**
.. code-block:: python
meta_props = {
client.software_specifications.ConfigurationMetaNames.NAME: "skl_pipeline_heart_problem_prediction",
client.software_specifications.ConfigurationMetaNames.DESCRIPTION: "description scikit-learn_0.20",
client.software_specifications.ConfigurationMetaNames.PACKAGE_EXTENSIONS_UID: [],
client.software_specifications.ConfigurationMetaNames.SOFTWARE_CONFIGURATIONS: {},
client.software_specifications.ConfigurationMetaNames.BASE_SOFTWARE_SPECIFICATION_ID: "guid"
}
sw_spec_details = client.software_specifications.store(meta_props)
"""
SwSpec._validate_type(meta_props, "meta_props", dict, True)
sw_spec_meta = self.ConfigurationMetaNames._generate_resource_metadata(
meta_props, with_validation=True, client=self._client
)
sw_spec_meta_json = json.dumps(sw_spec_meta)
href = self._client.service_instance._href_definitions.get_sw_specs_href()
creation_response = requests.post(
href,
params=self._client._params(),
headers=self._client._get_headers(),
data=sw_spec_meta_json,
)
sw_spec_details = self._handle_response(
201, "creating sofware specifications", creation_response
)
return sw_spec_details
[docs]
def list(self, limit: int | None = None) -> DataFrame:
"""List software specifications in a table format.
:param limit: limit number of fetched records
:type limit: int, optional
:return: pandas.DataFrame with listed software specifications
:rtype: pandas.DataFrame
**Example:**
.. code-block:: python
client.software_specifications.list()
"""
href = self._client.service_instance._href_definitions.get_sw_specs_href()
response = requests.get(
href, params=self._client._params(), headers=self._client._get_headers()
)
asset_details = self._handle_response(200, "list assets", response)["resources"]
if self._client.CPD_version >= 4.8 or self._client.CLOUD_PLATFORM_SPACES:
sw_spec_values = [
(
m["metadata"]["name"],
m["metadata"]["asset_id"],
m["entity"]["software_specification"].get("type", "derived"),
self._get_spec_state(m),
m["metadata"].get("life_cycle", {}).get("replacement_name", ""),
)
for m in asset_details
]
table = self._list(
sw_spec_values,
["NAME", "ID", "TYPE", "STATE", "REPLACEMENT"],
limit,
_DEFAULT_LIST_LENGTH,
)
else:
href = (
self._client.service_instance._href_definitions.get_software_specifications_list_href()
)
response = requests.get(
href, params=self._client._params(), headers=self._client._get_headers()
)
spec_state_dict = {
el["id"]: el
for el in self._handle_response(
200, "list sw_specs with spec_state info", response
)["results"]
}
sw_spec_values = [
(
m["metadata"]["name"],
m["metadata"]["asset_id"],
m["entity"]["software_specification"].get("type", "derived"),
spec_state_dict.get(m["metadata"]["asset_id"], {}).get(
"state", "not_provided"
),
spec_state_dict.get(m["metadata"]["asset_id"], {}).get(
"replacement", ""
),
)
for m in asset_details
]
table = self._list(
sw_spec_values,
["NAME", "ID", "TYPE", "STATE", "REPLACEMENT"],
limit,
_DEFAULT_LIST_LENGTH,
)
return table
[docs]
@staticmethod
def get_id(sw_spec_details: dict) -> str:
"""Get the unique ID of a software specification.
:param sw_spec_details: metadata of the software specification
:type sw_spec_details: dict
:return: unique ID of the software specification
:rtype: str
**Example:**
.. code-block:: python
asset_id = client.software_specifications.get_id(sw_spec_details)
"""
SwSpec._validate_type(sw_spec_details, "sw_spec_details", object, True)
SwSpec._validate_type_of_details(sw_spec_details, SW_SPEC_DETAILS_TYPE)
return WMLResource._get_required_element_from_dict(
sw_spec_details, "sw_spec_details", ["metadata", "asset_id"]
)
[docs]
@staticmethod
def get_uid(sw_spec_details: dict) -> str:
"""Get the unique ID of a software specification.
*Deprecated:* Use ``get_id(sw_spec_details)`` instead.
:param sw_spec_details: metadata of the software specification
:type sw_spec_details: dict
:return: unique ID of the software specification
:rtype: str
**Example:**
.. code-block:: python
asset_uid = client.software_specifications.get_uid(sw_spec_details)
"""
warn(
"This method is deprecated, please use `get_id(sw_spec_details)` instead",
category=DeprecationWarning,
)
return SwSpec.get_id(sw_spec_details)
[docs]
def get_id_by_name(self, sw_spec_name: str) -> str:
"""Get the unique ID of a software specification.
:param sw_spec_name: name of the software specification
:type sw_spec_name: str
:return: unique ID of the software specification
:rtype: str
**Example:**
.. code-block:: python
asset_uid = client.software_specifications.get_id_by_name(sw_spec_name)
"""
SwSpec._validate_type(sw_spec_name, "sw_spec_name", str, True)
parameters = self._client._params(skip_space_project_chk=True)
parameters.update(name=sw_spec_name)
response = requests.get(
self._client.service_instance._href_definitions.get_sw_specs_href(),
params=parameters,
headers=self._client._get_headers(),
)
total_values = self._handle_response(200, "list assets", response)[
"total_results"
]
if total_values != 0:
sw_spec_details = self._handle_response(200, "list assets", response)[
"resources"
]
return sw_spec_details[0]["metadata"]["asset_id"]
else:
raise ResourceIdByNameNotFound(sw_spec_name, "software spec")
[docs]
def get_uid_by_name(self, sw_spec_name: str) -> str:
"""Get the unique ID of a software specification.
*Deprecated:* Use ``get_id_by_name(self, sw_spec_name)`` instead.
:param sw_spec_name: name of the software specification
:type sw_spec_name: str
:return: unique ID of the software specification
:rtype: str
**Example:**
.. code-block:: python
asset_uid = client.software_specifications.get_uid_by_name(sw_spec_name)
"""
warn(
"This method is deprecated, please use `get_id_by_name(sw_spec_name)` instead",
category=DeprecationWarning,
)
return SwSpec.get_id_by_name(self, sw_spec_name)
[docs]
@staticmethod
def get_href(sw_spec_details: dict) -> str:
"""Get the URL of a software specification.
:param sw_spec_details: details of the software specification
:type sw_spec_details: dict
:return: href of the software specification
:rtype: str
**Example:**
.. code-block:: python
sw_spec_details = client.software_specifications.get_details(sw_spec_id)
sw_spec_href = client.software_specifications.get_href(sw_spec_details)
"""
SwSpec._validate_type(sw_spec_details, "sw_spec_details", object, True)
SwSpec._validate_type_of_details(sw_spec_details, SW_SPEC_DETAILS_TYPE)
return WMLResource._get_required_element_from_dict(
sw_spec_details, "sw_spec_details", ["metadata", "href"]
)
[docs]
def delete(self, sw_spec_id: str | None = None, **kwargs: Any) -> str:
"""Delete a software specification.
:param sw_spec_id: unique ID of the software specification
:type sw_spec_id: str
:return: status ("SUCCESS" or "FAILED")
:rtype: str
**Example:**
.. code-block:: python
client.software_specifications.delete(sw_spec_id)
"""
sw_spec_id = _get_id_from_deprecated_uid(
kwargs, sw_spec_id, "sw_spec", can_be_none=False
)
SwSpec._validate_type(sw_spec_id, "sw_spec_id", str, True)
response = requests.delete(
self._client.service_instance._href_definitions.get_sw_spec_href(
sw_spec_id
),
params=self._client._params(),
headers=self._client._get_headers(),
)
if response.status_code == 200:
return self._get_required_element_from_response(response.json()) # type: ignore
else:
return self._handle_response(204, "delete software specification", response)
[docs]
def add_package_extension(
self,
sw_spec_id: str | None = None,
pkg_extn_id: str | None = None,
**kwargs: Any,
) -> str:
"""Add a package extension to a software specification's existing metadata.
:param sw_spec_id: unique ID of the software specification to be updated
:type sw_spec_id: str
:param pkg_extn_id: unique ID of the package extension to be added to the software specification
:type pkg_extn_id: str
:return: status
:rtype: str
**Example:**
.. code-block:: python
client.software_specifications.add_package_extension(sw_spec_id, pkg_extn_id)
"""
if pkg_extn_id is None:
raise TypeError(
"add_package_extension() missing 1 required positional argument: 'pkg_extn_id'"
)
sw_spec_id = _get_id_from_deprecated_uid(
kwargs, sw_spec_id, "sw_spec", can_be_none=False
)
##For CP4D, check if either spce or project ID is set
self._client._check_if_either_is_set()
self._validate_type(sw_spec_id, "sw_spec_id", str, True)
self._validate_type(pkg_extn_id, "pkg_extn_id", str, True)
url = self._client.service_instance._href_definitions.get_sw_spec_href(
sw_spec_id
)
url = url + "/package_extensions/" + pkg_extn_id
response = requests.put(
url, params=self._client._params(), headers=self._client._get_headers()
)
if response.status_code == 204:
print("SUCCESS")
return "SUCCESS"
else:
return self._handle_response(204, "pkg spec add", response, False)
[docs]
def delete_package_extension(
self,
sw_spec_id: str | None = None,
pkg_extn_id: str | None = None,
**kwargs: Any,
) -> str:
"""Delete a package extension from a software specification's existing metadata.
:param sw_spec_id: unique ID of the software specification to be updated
:type sw_spec_id: str
:param pkg_extn_id: unique ID of the package extension to be deleted from the software specification
:type pkg_extn_id: str
:return: status
:rtype: str
**Example:**
.. code-block:: python
client.software_specifications.delete_package_extension(sw_spec_uid, pkg_extn_id)
"""
if pkg_extn_id is None:
raise TypeError(
"add_package_extension() missing 1 required positional argument: 'pkg_extn_id'"
)
sw_spec_id = _get_id_from_deprecated_uid(
kwargs, sw_spec_id, "sw_spec", can_be_none=False
)
##For CP4D, check if either spce or project ID is set
self._client._check_if_either_is_set()
self._validate_type(sw_spec_id, "sw_spec_id", str, True)
self._validate_type(pkg_extn_id, "pkg_extn_id", str, True)
url = self._client.service_instance._href_definitions.get_sw_spec_href(
sw_spec_id
)
url = url + "/package_extensions/" + pkg_extn_id
response = requests.delete(
url, params=self._client._params(), headers=self._client._get_headers()
)
return self._handle_response(204, "pkg spec delete", response, False)
@staticmethod
def _get_spec_state(asset: dict) -> str:
if "life_cycle" in asset["metadata"]:
if SpecStates.RETIRED.value in asset["metadata"]["life_cycle"]:
return SpecStates.RETIRED.value
elif SpecStates.CONSTRICTED.value in asset["metadata"]["life_cycle"]:
return SpecStates.CONSTRICTED.value
elif SpecStates.DEPRECATED.value in asset["metadata"]["life_cycle"]:
return SpecStates.DEPRECATED.value
else:
return SpecStates.SUPPORTED.value
return "not_provided"
def _get_required_element_from_response(
self, response_data: dict[str, Any]
) -> dict:
WMLResource._validate_type(response_data, "sw_spec_response", dict)
try:
new_el = {
"metadata": {
"name": response_data["metadata"]["name"],
"asset_id": response_data["metadata"]["asset_id"],
"href": response_data["metadata"]["href"],
"asset_type": response_data["metadata"]["asset_type"],
"created_at": response_data["metadata"]["created_at"],
},
"entity": response_data["entity"],
}
if "life_cycle" in response_data["metadata"]:
new_el["metadata"]["life_cycle"] = response_data["metadata"][
"life_cycle"
]
if "href" in response_data["metadata"]:
href_without_host = response_data["metadata"]["href"].split(".com")[-1]
new_el["metadata"].update({"href": href_without_host})
return new_el
except Exception:
raise WMLClientError(
"Failed to read Response from down-stream service: "
+ str(response_data)
)