diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/_exception_helper.py b/sdk/ml/azure-ai-ml/azure/ai/ml/_exception_helper.py index f707a27c9842..5d849e6fe01c 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/_exception_helper.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/_exception_helper.py @@ -154,32 +154,32 @@ def format_errors_and_resolutions_sections(entity_type: str, error_types: Dict[s if error_types[ValidationErrorType.INVALID_VALUE]: errors += f"\n{count}) One or more fields are invalid" resolutions += f"Double-check that all specified parameters are of the correct types and formats \ - prescribed by the {entity_type} schema.\n" + prescribed by the {entity_type} schema." count += 1 if error_types[ValidationErrorType.UNKNOWN_FIELD]: errors += f"\n{count}) A least one unrecognized parameter is specified" - resolutions += f"Remove any parameters not prescribed by the {entity_type} schema.\n" + resolutions += f"Remove any parameters not prescribed by the {entity_type} schema." count += 1 if error_types[ValidationErrorType.MISSING_FIELD]: errors += f"\n{count}) At least one required parameter is missing" - resolutions += f"Ensure all parameters required by the {entity_type} schema are specified.\n" + resolutions += f"Ensure all parameters required by the {entity_type} schema are specified." count += 1 if error_types[ValidationErrorType.FILE_OR_FOLDER_NOT_FOUND]: errors += f"\n{count}) One or more files or folders do not exist.\n" - resolutions += "Double-check the directory paths you provided and enter the correct paths.\n" + resolutions += "Double-check the directory paths you provided and enter the correct paths." count += 1 if error_types[ValidationErrorType.CANNOT_SERIALIZE]: errors += f"\n{count}) One or more fields cannot be serialized.\n" resolutions += f"Double-check that all specified parameters are of the correct types and formats \ - prescribed by the {entity_type} schema.\n" + prescribed by the {entity_type} schema." count += 1 if error_types[ValidationErrorType.CANNOT_PARSE]: errors += f"\n{count}) YAML file cannot be parsed.\n" - resolutions += "Double-check your YAML file for syntax and formatting errors.\n" + resolutions += "Double-check your YAML file for syntax and formatting errors." count += 1 if error_types[ValidationErrorType.RESOURCE_NOT_FOUND]: errors += f"\n{count}) Resource was not found.\n" - resolutions += "Double-check that the resource has been specified correctly and that you have access to it.\n" + resolutions += "Double-check that the resource has been specified correctly and that you have access to it." count += 1 return errors, resolutions @@ -191,10 +191,50 @@ def format_create_validation_error( """ Formats a detailed error message for validation errors. """ + from azure.ai.ml.entities._util import REF_DOC_ERROR_MESSAGE_MAP + from azure.ai.ml._schema.assets.data import DataSchema + from azure.ai.ml._schema._datastore import ( + AzureBlobSchema, + AzureDataLakeGen1Schema, + AzureDataLakeGen2Schema, + AzureFileSchema, + ) + from azure.ai.ml._schema.job import CommandJobSchema + from azure.ai.ml._schema._sweep import SweepJobSchema + from azure.ai.ml._schema.assets.environment import EnvironmentSchema + from azure.ai.ml._schema.assets.model import ModelSchema + entity_type, details = get_entity_type(error) error_types, details = format_details_section(error, details, entity_type) errors, resolutions = format_errors_and_resolutions_sections(entity_type, error_types) - description = YAML_CREATION_ERROR_DESCRIPTION.format(entity_type=entity_type) if yaml_operation else "" + + if yaml_operation: + description = YAML_CREATION_ERROR_DESCRIPTION.format(entity_type=entity_type) + + if entity_type == ErrorTarget.MODEL: + schema_type = ModelSchema + elif entity_type == ErrorTarget.DATA: + schema_type = DataSchema + elif entity_type == ErrorTarget.COMMAND_JOB: + schema_type = CommandJobSchema + elif entity_type == ErrorTarget.SWEEP_JOB: + schema_type = SweepJobSchema + elif entity_type in [ErrorTarget.BLOB_DATASTORE, ErrorTarget.DATASTORE]: + schema_type = AzureBlobSchema + elif entity_type == ErrorTarget.GEN1_DATASTORE: + schema_type = AzureDataLakeGen1Schema + elif entity_type == ErrorTarget.GEN2_DATASTORE: + schema_type = AzureDataLakeGen2Schema + elif entity_type == ErrorTarget.FILE_DATASTORE: + schema_type = AzureFileSchema + elif entity_type == ErrorTarget.ENVIRONMENT: + schema_type = EnvironmentSchema + + resolutions += " " + REF_DOC_ERROR_MESSAGE_MAP.get(schema_type, "") + else: + description = "" + + resolutions += "\n" formatted_error = SCHEMA_VALIDATION_ERROR_TEMPLATE.format( description=description, error_msg=errors, diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/constants/_common.py b/sdk/ml/azure-ai-ml/azure/ai/ml/constants/_common.py index 9ecd32da8d83..2628287d8f6c 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/constants/_common.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/constants/_common.py @@ -102,7 +102,7 @@ EXPERIMENTAL_LINK_MESSAGE = ( "and may change at any time. Please see https://aka.ms/azuremlexperimental for more information." ) -REF_DOC_YAML_SCHEMA_ERROR_MSG_FORMAT = "For a more detailed breakdown of the {} schema, please see: {}." +REF_DOC_YAML_SCHEMA_ERROR_MSG_FORMAT = "Visit this link to refer to the {} schema if needed: {}." STORAGE_AUTH_MISMATCH_ERROR = "AuthorizationPermissionMismatch" SWEEP_JOB_BEST_CHILD_RUN_ID_PROPERTY_NAME = "best_child_run_id" BATCH_JOB_CHILD_RUN_NAME = "batchscoring" diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/exceptions.py b/sdk/ml/azure-ai-ml/azure/ai/ml/exceptions.py index cb0384f47396..b01a3a3a537a 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/exceptions.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/exceptions.py @@ -64,6 +64,10 @@ class ErrorTarget: ONLINE_ENDPOINT = "OnlineEndpoint" ASSET = "Asset" DATASTORE = "Datastore" + BLOB_DATASTORE = "BlobDatastore" + FILE_DATASTORE = "FileDatastore" + GEN1_DATASTORE = "Gen1Datastore" + GEN2_DATASTORE = "Gen2Datastore" WORKSPACE = "Workspace" COMPUTE = "Compute" DEPLOYMENT = "Deployment" diff --git a/sdk/ml/azure-ai-ml/tests/dataset/unittests/test_data_operations.py b/sdk/ml/azure-ai-ml/tests/dataset/unittests/test_data_operations.py index 0a124ab21c66..83f79aa20ee9 100644 --- a/sdk/ml/azure-ai-ml/tests/dataset/unittests/test_data_operations.py +++ b/sdk/ml/azure-ai-ml/tests/dataset/unittests/test_data_operations.py @@ -13,7 +13,12 @@ DatasetVersionDetails, ) from azure.ai.ml._scope_dependent_operations import OperationConfig, OperationScope -from azure.ai.ml.constants._common import AssetTypes +from azure.ai.ml.constants._common import ( + REF_DOC_YAML_SCHEMA_ERROR_MSG_FORMAT, + AssetTypes, + YAMLRefDocLinks, + YAMLRefDocSchemaNames, +) from azure.ai.ml.entities._assets import Data from azure.ai.ml.entities._assets._artifacts.artifact import ArtifactStorageInfo from azure.ai.ml.exceptions import ErrorTarget @@ -170,13 +175,17 @@ def test_create_or_update_missing_path(self, mock_data_operations: DataOperation Expect to raise ValidationException for missing path """ name = "random_name" - data = Data(name=name, version="1", description="this is an mltable dataset", type=AssetTypes.MLTABLE) + data1 = Data(name=name, version="1", description="this is an mltable dataset", type=AssetTypes.MLTABLE) with pytest.raises(Exception) as ex: - mock_data_operations.create_or_update(data) + mock_data_operations.create_or_update(data1) assert "At least one required parameter is missing" in str(ex.value) mock_data_operations._operation.create_or_update.assert_not_called() + with pytest.raises(Exception) as ex: + load_data("tests/test_configs/dataset/data_missing_path_test.yml") + assert REF_DOC_YAML_SCHEMA_ERROR_MSG_FORMAT.format(YAMLRefDocSchemaNames.DATA, YAMLRefDocLinks.DATA) in str(ex.value) + @patch("azure.ai.ml.operations._data_operations.read_local_mltable_metadata_contents") @patch("azure.ai.ml.operations._data_operations.read_remote_mltable_metadata_contents") @patch("azure.ai.ml.operations._data_operations.download_mltable_metadata_schema") diff --git a/sdk/ml/azure-ai-ml/tests/internal_utils/unittests/test_asset_utils.py b/sdk/ml/azure-ai-ml/tests/internal_utils/unittests/test_asset_utils.py index 6c0c250805df..9dd7712f1d7d 100644 --- a/sdk/ml/azure-ai-ml/tests/internal_utils/unittests/test_asset_utils.py +++ b/sdk/ml/azure-ai-ml/tests/internal_utils/unittests/test_asset_utils.py @@ -64,7 +64,11 @@ def link_file_path( link_file_name = "link_file_rand_name.txt" link_file = Path(os.path.join(os.path.abspath(storage_test_directory), link_file_name)) - os.symlink(target_file_path, link_file) + try: + os.symlink(target_file_path, link_file) + except FileExistsError: + pass + assert os.path.islink(link_file) link_file = convert_windows_path_to_unix(link_file) yield link_file