aboutsummaryrefslogtreecommitdiff
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------


import abc
import os
from os import PathLike
from pathlib import Path
from typing import IO, Any, AnyStr, Dict, List, Optional, Tuple, Union, cast

from msrest import Serializer

from azure.ai.ml._restclient.v2022_10_01 import models
from azure.ai.ml._telemetry.logging_handler import in_jupyter_notebook
from azure.ai.ml._utils.utils import dump_yaml

from ..constants._common import BASE_PATH_CONTEXT_KEY
from ._system_data import SystemData


class Resource(abc.ABC):
    """Base class for entity classes.

    Resource is an abstract object that serves as a base for creating resources. It contains common properties and
    methods for all resources.

    This class should not be instantiated directly. Instead, use one of its subclasses.

    :param name: The name of the resource.
    :type name: str
    :param description: The description of the resource.
    :type description: Optional[str]
    :param tags: Tags can be added, removed, and updated.
    :type tags: Optional[dict]
    :param properties: The resource's property dictionary.
    :type properties: Optional[dict]
    :keyword print_as_yaml: Specifies if the the resource should print out as a YAML-formatted object. If False,
        the resource will print out in a more-compact style. By default, the YAML output is only used in Jupyter
        notebooks. Be aware that some bookkeeping values are shown only in the non-YAML output.
    :paramtype print_as_yaml: bool
    """

    def __init__(
        self,
        name: Optional[str],
        description: Optional[str] = None,
        tags: Optional[Dict] = None,
        properties: Optional[Dict] = None,
        **kwargs: Any,
    ) -> None:
        self.name = name
        self.description = description
        self.tags: Optional[Dict] = dict(tags) if tags else {}
        self.properties = dict(properties) if properties else {}
        # Conditional assignment to prevent entity bloat when unused.
        self._print_as_yaml = kwargs.pop("print_as_yaml", False)

        # Hide read only properties in kwargs
        self._id = kwargs.pop("id", None)
        self.__source_path: Union[str, PathLike] = kwargs.pop("source_path", "")
        self._base_path = kwargs.pop(BASE_PATH_CONTEXT_KEY, None) or os.getcwd()  # base path should never be None
        self._creation_context: Optional[SystemData] = kwargs.pop("creation_context", None)
        client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)}
        self._serialize = Serializer(client_models)
        self._serialize.client_side_validation = False
        super().__init__(**kwargs)

    @property
    def _source_path(self) -> Union[str, PathLike]:
        # source path is added to display file location for validation error messages
        # usually, base_path = Path(source_path).parent if source_path else os.getcwd()
        return self.__source_path

    @_source_path.setter
    def _source_path(self, value: Union[str, PathLike]) -> None:
        self.__source_path = Path(value).as_posix()

    @property
    def id(self) -> Optional[str]:
        """The resource ID.

        :return: The global ID of the resource, an Azure Resource Manager (ARM) ID.
        :rtype: Optional[str]
        """
        if self._id is None:
            return None
        return str(self._id)

    @property
    def creation_context(self) -> Optional[SystemData]:
        """The creation context of the resource.

        :return: The creation metadata for the resource.
        :rtype: Optional[~azure.ai.ml.entities.SystemData]
        """
        return cast(Optional[SystemData], self._creation_context)

    @property
    def base_path(self) -> str:
        """The base path of the resource.

        :return: The base path of the resource.
        :rtype: str
        """
        return self._base_path

    @abc.abstractmethod
    def dump(self, dest: Union[str, PathLike, IO[AnyStr]], **kwargs: Any) -> Any:
        """Dump the object content into a file.

        :param dest: The local path or file stream to write the YAML content to.
            If dest is a file path, a new file will be created.
            If dest is an open file, the file will be written to directly.
        :type dest: Union[PathLike, str, IO[AnyStr]]
        """

    @classmethod
    # pylint: disable=unused-argument
    def _resolve_cls_and_type(cls, data: Dict, params_override: Optional[List[Dict]] = None) -> Tuple:
        """Resolve the class to use for deserializing the data. Return current class if no override is provided.

        :param data: Data to deserialize.
        :type data: dict
        :param params_override: Parameters to override, defaults to None
        :type params_override: typing.Optional[list]
        :return: Class to use for deserializing the data & its "type". Type will be None if no override is provided.
        :rtype: tuple[class, typing.Optional[str]]
        """
        return cls, None

    @classmethod
    @abc.abstractmethod
    def _load(
        cls,
        data: Optional[Dict] = None,
        yaml_path: Optional[Union[PathLike, str]] = None,
        params_override: Optional[list] = None,
        **kwargs: Any,
    ) -> "Resource":
        """Construct a resource object from a file. @classmethod.

        :param cls: Indicates that this is a class method.
        :type cls: class
        :param data: Path to a local file as the source, defaults to None
        :type data: typing.Optional[typing.Dict]
        :param yaml_path: Path to a yaml file as the source, defaults to None
        :type yaml_path: typing.Optional[typing.Union[typing.PathLike, str]]
        :param params_override: Parameters to override, defaults to None
        :type params_override: typing.Optional[list]
        :return: Resource
        :rtype: Resource
        """

    # pylint: disable:unused-argument
    def _get_arm_resource(
        self,
        # pylint: disable=unused-argument
        **kwargs: Any,
    ) -> Dict:
        """Get arm resource.

        :return: Resource
        :rtype: dict
        """
        from azure.ai.ml._arm_deployments.arm_helper import get_template

        # pylint: disable=no-member
        template = get_template(resource_type=self._arm_type)  # type: ignore
        # pylint: disable=no-member
        template["copy"]["name"] = f"{self._arm_type}Deployment"  # type: ignore
        return dict(template)

    def _get_arm_resource_and_params(self, **kwargs: Any) -> List:
        """Get arm resource and parameters.

        :return: Resource and parameters
        :rtype: dict
        """
        resource = self._get_arm_resource(**kwargs)
        # pylint: disable=no-member
        param = self._to_arm_resource_param(**kwargs)  # type: ignore
        return [(resource, param)]

    def __repr__(self) -> str:
        var_dict = {k.strip("_"): v for (k, v) in vars(self).items()}
        return f"{self.__class__.__name__}({var_dict})"

    def __str__(self) -> str:
        if self._print_as_yaml or in_jupyter_notebook():
            # pylint: disable=no-member
            yaml_serialized = self._to_dict()  # type: ignore
            return str(dump_yaml(yaml_serialized, default_flow_style=False))
        return self.__repr__()