about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation')
-rw-r--r--.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/__init__.py18
-rw-r--r--.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/core.py531
-rw-r--r--.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/path_aware_schema.py53
-rw-r--r--.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/remote.py162
-rw-r--r--.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/schema.py156
5 files changed, 920 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/__init__.py b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/__init__.py
new file mode 100644
index 00000000..29ba05c5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/__init__.py
@@ -0,0 +1,18 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+
+
+from .core import MutableValidationResult, ValidationResult, ValidationResultBuilder
+from .path_aware_schema import PathAwareSchemaValidatableMixin
+from .remote import RemoteValidatableMixin
+from .schema import SchemaValidatableMixin
+
+__all__ = [
+    "SchemaValidatableMixin",
+    "PathAwareSchemaValidatableMixin",
+    "RemoteValidatableMixin",
+    "MutableValidationResult",
+    "ValidationResult",
+    "ValidationResultBuilder",
+]
diff --git a/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/core.py b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/core.py
new file mode 100644
index 00000000..a7516c1d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/core.py
@@ -0,0 +1,531 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+
+# pylint: disable=protected-access
+
+import copy
+import json
+import logging
+import os.path
+import typing
+from os import PathLike
+from pathlib import Path
+from typing import IO, Any, AnyStr, Dict, List, Optional, Tuple, Union, cast
+
+import pydash
+import strictyaml
+from marshmallow import ValidationError
+
+module_logger = logging.getLogger(__name__)
+
+
+class _ValidationStatus:
+    """Validation status class.
+
+    Validation status is used to indicate the status of an validation result. It can be one of the following values:
+    Succeeded, Failed.
+    """
+
+    SUCCEEDED = "Succeeded"
+    """Succeeded."""
+    FAILED = "Failed"
+    """Failed."""
+
+
+class Diagnostic(object):
+    """Represents a diagnostic of an asset validation error with the location info."""
+
+    def __init__(self, yaml_path: str, message: Optional[str], error_code: Optional[str]) -> None:
+        """Init Diagnostic.
+
+        :keyword yaml_path: A dash path from root to the target element of the diagnostic. jobs.job_a.inputs.input_str
+        :paramtype yaml_path: str
+        :keyword message: Error message of diagnostic.
+        :paramtype message: str
+        :keyword error_code: Error code of diagnostic.
+        :paramtype error_code: str
+        """
+        self.yaml_path = yaml_path
+        self.message = message
+        self.error_code = error_code
+        self.local_path, self.value = None, None
+
+    def __repr__(self) -> str:
+        """The asset friendly name and error message.
+
+        :return: The formatted diagnostic
+        :rtype: str
+        """
+        return "{}: {}".format(self.yaml_path, self.message)
+
+    @classmethod
+    def create_instance(
+        cls,
+        yaml_path: str,
+        message: Optional[str] = None,
+        error_code: Optional[str] = None,
+    ) -> "Diagnostic":
+        """Create a diagnostic instance.
+
+        :param yaml_path: A dash path from root to the target element of the diagnostic. jobs.job_a.inputs.input_str
+        :type yaml_path: str
+        :param message: Error message of diagnostic.
+        :type message: str
+        :param error_code: Error code of diagnostic.
+        :type error_code: str
+        :return: The created instance
+        :rtype: Diagnostic
+        """
+        return cls(
+            yaml_path=yaml_path,
+            message=message,
+            error_code=error_code,
+        )
+
+
+class ValidationResult(object):
+    """Represents the result of job/asset validation.
+
+    This class is used to organize and parse diagnostics from both client & server side before expose them. The result
+    is immutable.
+    """
+
+    def __init__(self) -> None:
+        self._target_obj: Optional[Dict] = None
+        self._errors: List = []
+        self._warnings: List = []
+
+    @property
+    def error_messages(self) -> Dict:
+        """
+        Return all messages of errors in the validation result.
+
+        :return: A dictionary of error messages. The key is the yaml path of the error, and the value is the error
+            message.
+        :rtype: dict
+
+        .. admonition:: Example:
+
+            .. literalinclude:: ../samples/ml_samples_misc.py
+                :start-after: [START validation_result]
+                :end-before: [END validation_result]
+                :language: markdown
+                :dedent: 8
+        """
+        messages = {}
+        for diagnostic in self._errors:
+            if diagnostic.yaml_path not in messages:
+                messages[diagnostic.yaml_path] = diagnostic.message
+            else:
+                messages[diagnostic.yaml_path] += "; " + diagnostic.message
+        return messages
+
+    @property
+    def passed(self) -> bool:
+        """Returns boolean indicating whether any errors were found.
+
+        :return: True if the validation passed, False otherwise.
+        :rtype: bool
+        """
+        return not self._errors
+
+    def _to_dict(self) -> typing.Dict[str, typing.Any]:
+        result: Dict = {
+            "result": _ValidationStatus.SUCCEEDED if self.passed else _ValidationStatus.FAILED,
+        }
+        for diagnostic_type, diagnostics in [
+            ("errors", self._errors),
+            ("warnings", self._warnings),
+        ]:
+            messages = []
+            for diagnostic in diagnostics:
+                message = {
+                    "message": diagnostic.message,
+                    "path": diagnostic.yaml_path,
+                    "value": pydash.get(self._target_obj, diagnostic.yaml_path, diagnostic.value),
+                }
+                if diagnostic.local_path:
+                    message["location"] = str(diagnostic.local_path)
+                messages.append(message)
+            if messages:
+                result[diagnostic_type] = messages
+        return result
+
+    def __repr__(self) -> str:
+        """Get the string representation of the validation result.
+
+        :return: The string representation
+        :rtype: str
+        """
+        return json.dumps(self._to_dict(), indent=2)
+
+
+class MutableValidationResult(ValidationResult):
+    """Used by the client side to construct a validation result.
+
+    The result is mutable and should not be exposed to the user.
+    """
+
+    def __init__(self, target_obj: Optional[Dict] = None):
+        super().__init__()
+        self._target_obj = target_obj
+
+    def merge_with(
+        self,
+        target: ValidationResult,
+        field_name: Optional[str] = None,
+        condition_skip: Optional[typing.Callable] = None,
+        overwrite: bool = False,
+    ) -> "MutableValidationResult":
+        """Merge errors & warnings in another validation results into current one.
+
+        Will update current validation result.
+        If field_name is not None, then yaml_path in the other validation result will be updated accordingly.
+        * => field_name, jobs.job_a => field_name.jobs.job_a e.g.. If None, then no update.
+
+        :param target: Validation result to merge.
+        :type target: ValidationResult
+        :param field_name: The base field name for the target to merge.
+        :type field_name: str
+        :param condition_skip: A function to determine whether to skip the merge of a diagnostic in the target.
+        :type condition_skip: typing.Callable
+        :param overwrite: Whether to overwrite the current validation result. If False, all diagnostics will be kept;
+            if True, current diagnostics with the same yaml_path will be dropped.
+        :type overwrite: bool
+        :return: The current validation result.
+        :rtype: MutableValidationResult
+        """
+        for source_diagnostics, target_diagnostics in [
+            (target._errors, self._errors),
+            (target._warnings, self._warnings),
+        ]:
+            if overwrite:
+                keys_to_remove = set(map(lambda x: x.yaml_path, source_diagnostics))
+                target_diagnostics[:] = [
+                    diagnostic for diagnostic in target_diagnostics if diagnostic.yaml_path not in keys_to_remove
+                ]
+            for diagnostic in source_diagnostics:
+                if condition_skip and condition_skip(diagnostic):
+                    continue
+                new_diagnostic = copy.deepcopy(diagnostic)
+                if field_name:
+                    if new_diagnostic.yaml_path == "*":
+                        new_diagnostic.yaml_path = field_name
+                    else:
+                        new_diagnostic.yaml_path = field_name + "." + new_diagnostic.yaml_path
+                target_diagnostics.append(new_diagnostic)
+        return self
+
+    def try_raise(
+        self,
+        raise_error: Optional[bool] = True,
+        *,
+        error_func: Optional[typing.Callable[[str, str], Exception]] = None,
+    ) -> "MutableValidationResult":
+        """Try to raise an error from the validation result.
+
+        If the validation is passed or raise_error is False, this method
+        will return the validation result.
+
+        :param raise_error: Whether to raise the error.
+        :type raise_error: bool
+        :keyword error_func: A function to create the error. If None, a marshmallow.ValidationError will be created.
+                             The first parameter of the function is the string representation of the validation result,
+                             and the second parameter is the error message without personal data.
+        :type error_func: typing.Callable[[str, str], Exception]
+        :return: The current validation result.
+        :rtype: MutableValidationResult
+        """
+        # pylint: disable=logging-not-lazy
+        if raise_error is False:
+            return self
+
+        if self._warnings:
+            module_logger.warning("Warnings: %s" % str(self._warnings))
+
+        if not self.passed:
+            if error_func is None:
+
+                def error_func(msg: Union[str, list, dict], _: Any) -> ValidationError:
+                    return ValidationError(message=msg)
+
+            raise error_func(
+                self.__repr__(),
+                "validation failed on the following fields: " + ", ".join(self.error_messages),
+            )
+        return self
+
+    def append_error(
+        self,
+        yaml_path: str = "*",
+        message: Optional[str] = None,
+        error_code: Optional[str] = None,
+    ) -> "MutableValidationResult":
+        """Append an error to the validation result.
+
+        :param yaml_path: The yaml path of the error.
+        :type yaml_path: str
+        :param message: The message of the error.
+        :type message: str
+        :param error_code: The error code of the error.
+        :type error_code: str
+        :return: The current validation result.
+        :rtype: MutableValidationResult
+        """
+        self._errors.append(
+            Diagnostic.create_instance(
+                yaml_path=yaml_path,
+                message=message,
+                error_code=error_code,
+            )
+        )
+        return self
+
+    def resolve_location_for_diagnostics(self, source_path: str, resolve_value: bool = False) -> None:
+        """Resolve location/value for diagnostics based on the source path where the validatable object is loaded.
+
+        Location includes local path of the exact file (can be different from the source path) & line number of the
+        invalid field. Value of a diagnostic is resolved from the validatable object in transfering to a dict by
+        default; however, when the validatable object is not available for the validation result, validation result is
+        created from marshmallow.ValidationError.messages e.g., it can be resolved from the source path.
+
+        :param source_path: The path of the source file.
+        :type source_path: str
+        :param resolve_value: Whether to resolve the value of the invalid field from source file.
+        :type resolve_value: bool
+        """
+        resolver = _YamlLocationResolver(source_path)
+        for diagnostic in self._errors + self._warnings:
+            res = resolver.resolve(diagnostic.yaml_path)
+            if res is not None:
+                diagnostic.local_path, value = res
+                if value is not None and resolve_value:
+                    diagnostic.value = value
+
+    def append_warning(
+        self,
+        yaml_path: str = "*",
+        message: Optional[str] = None,
+        error_code: Optional[str] = None,
+    ) -> "MutableValidationResult":
+        """Append a warning to the validation result.
+
+        :param yaml_path: The yaml path of the warning.
+        :type yaml_path: str
+        :param message: The message of the warning.
+        :type message: str
+        :param error_code: The error code of the warning.
+        :type error_code: str
+        :return: The current validation result.
+        :rtype: MutableValidationResult
+        """
+        self._warnings.append(
+            Diagnostic.create_instance(
+                yaml_path=yaml_path,
+                message=message,
+                error_code=error_code,
+            )
+        )
+        return self
+
+
+class ValidationResultBuilder:
+    """A helper class to create a validation result."""
+
+    UNKNOWN_MESSAGE = "Unknown field."
+
+    def __init__(self) -> None:
+        pass
+
+    @classmethod
+    def success(cls) -> MutableValidationResult:
+        """Create a validation result with success status.
+
+        :return: A validation result
+        :rtype: MutableValidationResult
+        """
+        return MutableValidationResult()
+
+    @classmethod
+    def from_single_message(
+        cls, singular_error_message: Optional[str] = None, yaml_path: str = "*", data: Optional[dict] = None
+    ) -> MutableValidationResult:
+        """Create a validation result with only 1 diagnostic.
+
+        :param singular_error_message: diagnostic.message.
+        :type singular_error_message: Optional[str]
+        :param yaml_path: diagnostic.yaml_path.
+        :type yaml_path: str
+        :param data: serializedvalidation target.
+        :type data: Optional[Dict]
+        :return: The validation result
+        :rtype: MutableValidationResult
+        """
+        obj = MutableValidationResult(target_obj=data)
+        if singular_error_message:
+            obj.append_error(message=singular_error_message, yaml_path=yaml_path)
+        return obj
+
+    @classmethod
+    def from_validation_error(
+        cls,
+        error: ValidationError,
+        *,
+        source_path: Optional[Union[str, PathLike, IO[AnyStr]]] = None,
+        error_on_unknown_field: bool = False,
+    ) -> MutableValidationResult:
+        """Create a validation result from a ValidationError, which will be raised in marshmallow.Schema.load. Please
+        use this function only for exception in loading file.
+
+        :param error: ValidationError raised by marshmallow.Schema.load.
+        :type error: ValidationError
+        :keyword source_path: The path to the source file.
+        :paramtype source_path: Optional[Union[str, PathLike, IO[AnyStr]]]
+        :keyword error_on_unknown_field: whether to raise error if there are unknown field diagnostics.
+        :paramtype error_on_unknown_field: bool
+        :return: The validation result
+        :rtype: MutableValidationResult
+        """
+        obj = cls.from_validation_messages(
+            error.messages, data=error.data, error_on_unknown_field=error_on_unknown_field
+        )
+        if source_path:
+            obj.resolve_location_for_diagnostics(cast(str, source_path), resolve_value=True)
+        return obj
+
+    @classmethod
+    def from_validation_messages(
+        cls, errors: typing.Dict, data: typing.Dict, *, error_on_unknown_field: bool = False
+    ) -> MutableValidationResult:
+        """Create a validation result from error messages, which will be returned by marshmallow.Schema.validate.
+
+        :param errors: error message returned by marshmallow.Schema.validate.
+        :type errors: dict
+        :param data: serialized data to validate
+        :type data: dict
+        :keyword error_on_unknown_field: whether to raise error if there are unknown field diagnostics.
+        :paramtype error_on_unknown_field: bool
+        :return: The validation result
+        :rtype: MutableValidationResult
+        """
+        instance = MutableValidationResult(target_obj=data)
+        errors = copy.deepcopy(errors)
+        cls._from_validation_messages_recursively(errors, [], instance, error_on_unknown_field=error_on_unknown_field)
+        return instance
+
+    @classmethod
+    def _from_validation_messages_recursively(
+        cls,
+        errors: typing.Union[typing.Dict, typing.List, str],
+        path_stack: typing.List[str],
+        instance: MutableValidationResult,
+        error_on_unknown_field: bool,
+    ) -> None:
+        cur_path = ".".join(path_stack) if path_stack else "*"
+        # single error message
+        if isinstance(errors, dict) and "_schema" in errors:
+            instance.append_error(
+                message=";".join(errors["_schema"]),
+                yaml_path=cur_path,
+            )
+        # errors on attributes
+        elif isinstance(errors, dict):
+            for field, msgs in errors.items():
+                # fields.Dict
+                if field in ["key", "value"]:
+                    cls._from_validation_messages_recursively(msgs, path_stack, instance, error_on_unknown_field)
+                else:
+                    # Todo: Add hack logic here to deal with error message in nested TypeSensitiveUnionField in
+                    #  DataTransfer: will be a nested dict with None field as dictionary key.
+                    #  open a item to track: https://msdata.visualstudio.com/Vienna/_workitems/edit/2244262/
+                    if field is None:
+                        cls._from_validation_messages_recursively(msgs, path_stack, instance, error_on_unknown_field)
+                    else:
+                        path_stack.append(field)
+                        cls._from_validation_messages_recursively(msgs, path_stack, instance, error_on_unknown_field)
+                        path_stack.pop()
+
+        # detailed error message
+        elif isinstance(errors, list) and all(isinstance(msg, str) for msg in errors):
+            if cls.UNKNOWN_MESSAGE in errors and not error_on_unknown_field:
+                # Unknown field is not a real error, so we should remove it and append a warning.
+                errors.remove(cls.UNKNOWN_MESSAGE)
+                instance.append_warning(message=cls.UNKNOWN_MESSAGE, yaml_path=cur_path)
+            if errors:
+                instance.append_error(message=";".join(errors), yaml_path=cur_path)
+        # union field
+        elif isinstance(errors, list):
+
+            def msg2str(msg: Any) -> Any:
+                if isinstance(msg, str):
+                    return msg
+                if isinstance(msg, dict) and len(msg) == 1 and "_schema" in msg and len(msg["_schema"]) == 1:
+                    return str(msg["_schema"][0])
+
+                return str(msg)
+
+            instance.append_error(message="; ".join([msg2str(x) for x in errors]), yaml_path=cur_path)
+        # unknown error
+        else:
+            instance.append_error(message=str(errors), yaml_path=cur_path)
+
+
+class _YamlLocationResolver:
+    def __init__(self, source_path: str):
+        self._source_path = source_path
+
+    def resolve(self, yaml_path: str, source_path: Optional[str] = None) -> Optional[Tuple]:
+        """Resolve the location & value of a yaml path starting from source_path.
+
+        :param yaml_path: yaml path.
+        :type yaml_path: str
+        :param source_path: source path.
+        :type source_path: str
+        :return: the location & value of the yaml path based on source_path.
+        :rtype: Tuple[str, str]
+        """
+        source_path = source_path or self._source_path
+        if source_path is None or not os.path.isfile(source_path):
+            return None, None
+        if yaml_path is None or yaml_path == "*":
+            return source_path, None
+
+        attrs = yaml_path.split(".")
+        attrs.reverse()
+
+        res: Optional[Tuple] = self._resolve_recursively(attrs, Path(source_path))
+        return res
+
+    def _resolve_recursively(self, attrs: List[str], source_path: Path) -> Optional[Tuple]:
+        with open(source_path, encoding="utf-8") as f:
+            try:
+                loaded_yaml = strictyaml.load(f.read())
+            except Exception as e:  # pylint: disable=W0718
+                msg = "Can't load source file %s as a strict yaml:\n%s" % (source_path, str(e))
+                module_logger.debug(msg)
+                return None, None
+
+        while attrs:
+            attr = attrs[-1]
+            if loaded_yaml.is_mapping() and attr in loaded_yaml:
+                loaded_yaml = loaded_yaml.get(attr)
+                attrs.pop()
+            else:
+                try:
+                    # if current object is a path of a valid yaml file, try to resolve location in new source file
+                    next_path = Path(loaded_yaml.value)
+                    if not next_path.is_absolute():
+                        next_path = source_path.parent / next_path
+                    if next_path.is_file():
+                        return self._resolve_recursively(attrs, source_path=next_path)
+                except OSError:
+                    pass
+                except TypeError:
+                    pass
+                # if not, return current section
+                break
+        return (
+            f"{source_path.resolve().absolute()}#line {loaded_yaml.start_line}",
+            None if attrs else loaded_yaml.value,
+        )
diff --git a/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/path_aware_schema.py b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/path_aware_schema.py
new file mode 100644
index 00000000..959de310
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/path_aware_schema.py
@@ -0,0 +1,53 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+import typing
+from os import PathLike
+from pathlib import Path
+
+from azure.ai.ml.constants._common import BASE_PATH_CONTEXT_KEY
+
+from ..._schema import PathAwareSchema
+from .._job.pipeline._attr_dict import try_get_non_arbitrary_attr
+from .._util import convert_ordered_dict_to_dict
+from .schema import SchemaValidatableMixin
+
+
+class PathAwareSchemaValidatableMixin(SchemaValidatableMixin):
+    """The mixin class for schema validation. Entity classes inheriting from this class should have a base path
+    and a schema of PathAwareSchema.
+    """
+
+    @property
+    def __base_path_for_validation(self) -> typing.Union[str, PathLike]:
+        """Get the base path of the resource.
+
+        It will try to return self.base_path, then self._base_path, then Path.cwd() if above attrs are non-existent or
+        `None.
+
+        :return: The base path of the resource
+        :rtype: typing.Union[str, os.PathLike]
+        """
+        return (
+            try_get_non_arbitrary_attr(self, BASE_PATH_CONTEXT_KEY)
+            or try_get_non_arbitrary_attr(self, f"_{BASE_PATH_CONTEXT_KEY}")
+            or Path.cwd()
+        )
+
+    def _default_context(self) -> dict:
+        # Note that, although context can be passed, nested.schema will be initialized only once
+        # base_path works well because it's fixed after loaded
+        return {BASE_PATH_CONTEXT_KEY: self.__base_path_for_validation}
+
+    @classmethod
+    def _create_schema_for_validation(cls, context: typing.Any) -> PathAwareSchema:
+        raise NotImplementedError()
+
+    @classmethod
+    def _create_validation_error(cls, message: str, no_personal_data_message: str) -> Exception:
+        raise NotImplementedError()
+
+    def _dump_for_validation(self) -> typing.Dict:
+        # this is not a necessary step but to keep the same behavior as before
+        # empty items will be removed when converting to dict
+        return typing.cast(dict, convert_ordered_dict_to_dict(super()._dump_for_validation()))
diff --git a/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/remote.py b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/remote.py
new file mode 100644
index 00000000..06f022a0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/remote.py
@@ -0,0 +1,162 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+
+import logging
+import typing
+
+import msrest
+
+from azure.ai.ml._vendor.azure_resources.models import (
+    Deployment,
+    DeploymentProperties,
+    DeploymentValidateResult,
+    ErrorResponse,
+)
+from azure.ai.ml.entities._mixins import RestTranslatableMixin
+
+from .core import MutableValidationResult, ValidationResultBuilder
+
+module_logger = logging.getLogger(__name__)
+
+
+class PreflightResource(msrest.serialization.Model):
+    """Specified resource.
+
+    Variables are only populated by the server, and will be ignored when sending a request.
+
+    :ivar id: Resource ID.
+    :vartype id: str
+    :ivar name: Resource name.
+    :vartype name: str
+    :ivar type: Resource type.
+    :vartype type: str
+    :param location: Resource location.
+    :type location: str
+    :param tags: A set of tags. Resource tags.
+    :type tags: dict[str, str]
+    """
+
+    _attribute_map = {
+        "type": {"key": "type", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "location": {"key": "location", "type": "str"},
+        "api_version": {"key": "apiversion", "type": "str"},
+        "properties": {"key": "properties", "type": "object"},
+    }
+
+    def __init__(self, **kwargs: typing.Any):
+        super(PreflightResource, self).__init__(**kwargs)
+        self.name = kwargs.get("name", None)
+        self.type = kwargs.get("type", None)
+        self.location = kwargs.get("location", None)
+        self.properties = kwargs.get("properties", None)
+        self.api_version = kwargs.get("api_version", None)
+
+
+class ValidationTemplateRequest(msrest.serialization.Model):
+    """Export resource group template request parameters.
+
+    :param resources: The rest objects to be validated.
+    :type resources: list[_models.Resource]
+    :param options: The export template options. A CSV-formatted list containing zero or more of
+     the following: 'IncludeParameterDefaultValue', 'IncludeComments',
+     'SkipResourceNameParameterization', 'SkipAllParameterization'.
+    :type options: str
+    """
+
+    _attribute_map = {
+        "resources": {"key": "resources", "type": "[PreflightResource]"},
+        "content_version": {"key": "contentVersion", "type": "str"},
+        "parameters": {"key": "parameters", "type": "object"},
+        "_schema": {
+            "key": "$schema",
+            "type": "str",
+            "default": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+        },
+    }
+
+    def __init__(self, **kwargs: typing.Any):
+        super(ValidationTemplateRequest, self).__init__(**kwargs)
+        self._schema = kwargs.get("_schema", None)
+        self.content_version = kwargs.get("content_version", None)
+        self.parameters = kwargs.get("parameters", None)
+        self.resources = kwargs.get("resources", None)
+
+
+class RemoteValidatableMixin(RestTranslatableMixin):
+    @classmethod
+    def _get_resource_type(cls) -> str:
+        """Return resource type to be used in remote validation.
+
+        Should be overridden by subclass.
+
+        :return: The resource type
+        :rtype: str
+        """
+        raise NotImplementedError()
+
+    def _get_resource_name_version(self) -> typing.Tuple:
+        """Return resource name and version to be used in remote validation.
+
+        Should be overridden by subclass.
+
+        :return: The name and version
+        :rtype: typing.Tuple[str, str]
+        """
+        raise NotImplementedError()
+
+    def _to_preflight_resource(self, location: str, workspace_name: str) -> PreflightResource:
+        """Return the preflight resource to be used in remote validation.
+
+        :param location: The location of the resource.
+        :type location: str
+        :param workspace_name: The workspace name
+        :type workspace_name: str
+        :return: The preflight resource
+        :rtype: PreflightResource
+        """
+        name, version = self._get_resource_name_version()
+        return PreflightResource(
+            type=self._get_resource_type(),
+            name=f"{workspace_name}/{name}/{version}",
+            location=location,
+            properties=self._to_rest_object().properties,
+            api_version="2023-03-01-preview",
+        )
+
+    def _build_rest_object_for_remote_validation(self, location: str, workspace_name: str) -> Deployment:
+        return Deployment(
+            properties=DeploymentProperties(
+                mode="Incremental",
+                template=ValidationTemplateRequest(
+                    _schema="https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+                    content_version="1.0.0.0",
+                    parameters={},
+                    resources=[self._to_preflight_resource(location=location, workspace_name=workspace_name)],
+                ),
+            )
+        )
+
+    @classmethod
+    def _build_validation_result_from_rest_object(cls, rest_obj: DeploymentValidateResult) -> MutableValidationResult:
+        """Create a validation result from a rest object. Note that the created validation result does not have
+        target_obj so should only be used for merging.
+
+        :param rest_obj: The Deployment Validate REST obj
+        :type rest_obj: DeploymentValidateResult
+        :return: The validation result created from rest_obj
+        :rtype: MutableValidationResult
+        """
+        if not rest_obj.error or not rest_obj.error.details:
+            return ValidationResultBuilder.success()
+        result = MutableValidationResult(target_obj=None)
+        details: typing.List[ErrorResponse] = rest_obj.error.details
+        for detail in details:
+            result.append_error(
+                message=detail.message,
+                yaml_path=detail.target.replace("/", "."),
+                error_code=detail.code,
+                # will always be UserError for now, not sure if innerError can be passed back
+            )
+        return result
diff --git a/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/schema.py b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/schema.py
new file mode 100644
index 00000000..9e34173d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/azure/ai/ml/entities/_validation/schema.py
@@ -0,0 +1,156 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+
+import json
+import logging
+import typing
+
+from marshmallow import Schema, ValidationError
+
+from .core import MutableValidationResult, ValidationResultBuilder
+
+module_logger = logging.getLogger(__name__)
+
+
+class SchemaValidatableMixin:
+    """The mixin class for schema validation."""
+
+    @classmethod
+    def _create_empty_validation_result(cls) -> MutableValidationResult:
+        """Simply create an empty validation result
+
+        To reduce _ValidationResultBuilder importing, which is a private class.
+
+        :return: An empty validation result
+        :rtype: MutableValidationResult
+        """
+        return ValidationResultBuilder.success()
+
+    @classmethod
+    def _load_with_schema(
+        cls, data: typing.Any, *, context: typing.Any, raise_original_exception: bool = False, **kwargs: typing.Any
+    ) -> typing.Any:
+        schema = cls._create_schema_for_validation(context=context)
+
+        try:
+            return schema.load(data, **kwargs)
+        except ValidationError as e:
+            if raise_original_exception:
+                raise e
+            msg = "Trying to load data with schema failed. Data:\n%s\nError: %s" % (
+                json.dumps(data, indent=4) if isinstance(data, dict) else data,
+                json.dumps(e.messages, indent=4),
+            )
+            raise cls._create_validation_error(
+                message=msg,
+                no_personal_data_message=str(e),
+            ) from e
+
+    @classmethod
+    # pylint: disable-next=docstring-missing-param
+    def _create_schema_for_validation(cls, context: typing.Any) -> Schema:
+        """Create a schema of the resource with specific context. Should be overridden by subclass.
+
+        :return: The schema of the resource.
+        :rtype: Schema.
+        """
+        raise NotImplementedError()
+
+    def _default_context(self) -> dict:
+        """Get the default context for schema validation. Should be overridden by subclass.
+
+        :return: The default context for schema validation
+        :rtype: dict
+        """
+        raise NotImplementedError()
+
+    @property
+    def _schema_for_validation(self) -> Schema:
+        """Return the schema of this Resource with default context. Do not override this method.
+        Override _create_schema_for_validation instead.
+
+        :return: The schema of the resource.
+        :rtype: Schema.
+        """
+        return self._create_schema_for_validation(context=self._default_context())
+
+    def _dump_for_validation(self) -> typing.Dict:
+        """Convert the resource to a dictionary.
+
+        :return: Converted dictionary
+        :rtype: typing.Dict
+        """
+        res: dict = self._schema_for_validation.dump(self)
+        return res
+
+    @classmethod
+    def _create_validation_error(cls, message: str, no_personal_data_message: str) -> Exception:
+        """The function to create the validation exception to raise in _try_raise and _validate when
+        raise_error is True.
+
+        Should be overridden by subclass.
+
+        :param message: The error message containing detailed information
+        :type message: str
+        :param no_personal_data_message: The error message without personal data
+        :type no_personal_data_message: str
+        :return: The validation exception to raise
+        :rtype: Exception
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def _try_raise(
+        cls, validation_result: MutableValidationResult, *, raise_error: typing.Optional[bool] = True
+    ) -> MutableValidationResult:
+        return validation_result.try_raise(raise_error=raise_error, error_func=cls._create_validation_error)
+
+    def _validate(self, raise_error: typing.Optional[bool] = False) -> MutableValidationResult:
+        """Validate the resource. If raise_error is True, raise ValidationError if validation fails and log warnings if
+        applicable; Else, return the validation result.
+
+        :param raise_error: Whether to raise ValidationError if validation fails.
+        :type raise_error: bool
+        :return: The validation result
+        :rtype: MutableValidationResult
+        """
+        result = self.__schema_validate()
+        result.merge_with(self._customized_validate())
+        return self._try_raise(result, raise_error=raise_error)
+
+    def _customized_validate(self) -> MutableValidationResult:
+        """Validate the resource with customized logic.
+
+        Override this method to add customized validation logic.
+
+        :return: The customized validation result
+        :rtype: MutableValidationResult
+        """
+        return self._create_empty_validation_result()
+
+    @classmethod
+    def _get_skip_fields_in_schema_validation(
+        cls,
+    ) -> typing.List[str]:
+        """Get the fields that should be skipped in schema validation.
+
+        Override this method to add customized validation logic.
+
+        :return: The fields to skip in schema validation
+        :rtype: typing.List[str]
+        """
+        return []
+
+    def __schema_validate(self) -> MutableValidationResult:
+        """Validate the resource with the schema.
+
+        :return: The validation result
+        :rtype: MutableValidationResult
+        """
+        data = self._dump_for_validation()
+        messages = self._schema_for_validation.validate(data)
+        for skip_field in self._get_skip_fields_in_schema_validation():
+            if skip_field in messages:
+                del messages[skip_field]
+        return ValidationResultBuilder.from_validation_messages(messages, data=data)