diff --git a/sdk/ml/azure-ai-ml/CHANGELOG.md b/sdk/ml/azure-ai-ml/CHANGELOG.md index d3a3c3401922..56dbeea0e152 100644 --- a/sdk/ml/azure-ai-ml/CHANGELOG.md +++ b/sdk/ml/azure-ai-ml/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features Added ### Bugs Fixed +- `load_component` now accepts a `default` value for asset-type inputs (`uri_file`, `uri_folder`, `mltable`, `mlflow_model`, `custom_model`), matching the public CLI v2 YAML schema. Previously this raised `UserErrorException: Non-primitive type Input has no default value.` ## 1.35.0 (2026-09-08) diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/constants/_component.py b/sdk/ml/azure-ai-ml/azure/ai/ml/constants/_component.py index 44cb0b9b3488..9b9b81188afa 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/constants/_component.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/constants/_component.py @@ -108,6 +108,7 @@ class ComponentParameterTypes: class IOConstants: + ASSET_INPUT_TYPES = ("uri_file", "uri_folder", "mltable", "mlflow_model", "custom_model") PRIMITIVE_STR_2_TYPE = { ComponentParameterTypes.INTEGER: int, ComponentParameterTypes.STRING: str, @@ -133,11 +134,11 @@ class IOConstants: } # For validation, indicates specific parameters combination for each type INPUT_TYPE_COMBINATION = { - "uri_folder": ["path", "mode"], - "uri_file": ["path", "mode"], - "mltable": ["path", "mode"], - "mlflow_model": ["path", "mode"], - "custom_model": ["path", "mode"], + "uri_folder": ["path", "mode", "default"], + "uri_file": ["path", "mode", "default"], + "mltable": ["path", "mode", "default"], + "mlflow_model": ["path", "mode", "default"], + "custom_model": ["path", "mode", "default"], "integer": ["default", "min", "max"], "number": ["default", "min", "max"], "string": ["default"], diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/dsl/_component_func.py b/sdk/ml/azure-ai-ml/azure/ai/ml/dsl/_component_func.py index 69547cd17bfc..f1014224c701 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/dsl/_component_func.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/dsl/_component_func.py @@ -6,10 +6,12 @@ from typing import Any, Callable, List, Mapping +from azure.ai.ml.constants._component import IOConstants from azure.ai.ml.dsl._dynamic import KwParameter, create_kw_function_from_parameters from azure.ai.ml.entities import Component as ComponentEntity from azure.ai.ml.entities._builders import Command from azure.ai.ml.entities._component.datatransfer_component import DataTransferImportComponent +from azure.ai.ml.entities._inputs_outputs import Input def get_dynamic_input_parameter(inputs: Mapping) -> List: @@ -24,8 +26,13 @@ def get_dynamic_input_parameter(inputs: Mapping) -> List: KwParameter( name=name, annotation=input._get_python_builtin_type_str(), - default=None, + default=( + Input(type=input.type, path=input.default, mode=input.mode) + if input.type in IOConstants.ASSET_INPUT_TYPES and input.default is not None + else None + ), _type=input._get_python_builtin_type_str(), + _copy_default=input.type in IOConstants.ASSET_INPUT_TYPES and input.default is not None, ) for name, input in inputs.items() ] diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/dsl/_dynamic.py b/sdk/ml/azure-ai-ml/azure/ai/ml/dsl/_dynamic.py index 3fcb42fb8176..fc50ebdfdc0e 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/dsl/_dynamic.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/dsl/_dynamic.py @@ -1,6 +1,7 @@ # --------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- +import copy import logging import types from inspect import Parameter, Signature @@ -24,14 +25,23 @@ class KwParameter(Parameter): :type _type: str :param _optional: Indicates if the parameter is optional, defaults to False. :type _optional: bool + :param _copy_default: Indicates if the default should be copied for each call, defaults to False. + :type _copy_default: bool """ def __init__( - self, name: str, default: Any, annotation: Any = Parameter.empty, _type: str = "str", _optional: bool = False + self, + name: str, + default: Any, + annotation: Any = Parameter.empty, + _type: str = "str", + _optional: bool = False, + _copy_default: bool = False, ) -> None: super().__init__(name, Parameter.KEYWORD_ONLY, default=default, annotation=annotation) self._type = _type self._optional = _optional + self._copy_default = _copy_default def _replace_function_name(func: types.FunctionType, new_name: str) -> types.FunctionType: @@ -165,12 +175,18 @@ def create_kw_function_from_parameters( target=ErrorTarget.COMPONENT, ) default_kwargs = {p.name: p.default for p in parameters} + copied_default_keys = { + p.name for p in parameters if isinstance(p, KwParameter) and p._copy_default # pylint: disable=protected-access + } def f(**kwargs: Any) -> Any: # We need to make sure all keys of kwargs are valid. # Merge valid group keys with original keys. _assert_arg_valid(kwargs, [*list(default_kwargs.keys()), *flattened_group_keys], func_name=func_name) # We need to put the default args to the kwargs before invoking the original function. + for key in copied_default_keys: + if key not in kwargs: + kwargs[key] = copy.deepcopy(default_kwargs[key]) _update_dct_if_not_exist(kwargs, default_kwargs) return func(**kwargs) diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_component/pipeline_component.py b/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_component/pipeline_component.py index 043359affb71..fd5f27e8d6f5 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_component/pipeline_component.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_component/pipeline_component.py @@ -21,7 +21,7 @@ from azure.ai.ml._utils._asset_utils import get_object_hash from azure.ai.ml._utils.utils import hash_dict, is_data_binding_expression from azure.ai.ml.constants._common import ARM_ID_PREFIX, ASSET_ARM_ID_REGEX_FORMAT, COMPONENT_TYPE -from azure.ai.ml.constants._component import ComponentSource, NodeType +from azure.ai.ml.constants._component import ComponentSource, IOConstants, NodeType from azure.ai.ml.constants._job.pipeline import ValidationErrorCode from azure.ai.ml.entities._builders import BaseNode, Command from azure.ai.ml.entities._builders.control_flow_node import ControlFlowNode, LoopNode @@ -36,6 +36,9 @@ module_logger = logging.getLogger(__name__) +_ASSET_INPUT_DEFAULTS_PROPERTY = "_azureml_asset_input_defaults" + + class PipelineComponent(Component): """Pipeline component, currently used to store components in an azure.ai.ml.dsl.pipeline. @@ -452,6 +455,18 @@ def _get_telemetry_values(self, *args: Any, **kwargs: Any) -> Dict: @classmethod def _from_rest_object_to_init_params(cls, obj: ComponentVersion) -> Dict: + serialized_defaults = (obj.properties.properties or {}).get(_ASSET_INPUT_DEFAULTS_PROPERTY) + if serialized_defaults: + try: + asset_input_defaults = json.loads(serialized_defaults) + if not isinstance(asset_input_defaults, dict): + raise ValueError("Asset input defaults must be a mapping.") + for input_name, default in asset_input_defaults.items(): + input_spec = obj.properties.component_spec.get("inputs", {}).get(input_name) + if input_spec is not None and input_spec.get("default") is None: + input_spec["default"] = default + except (TypeError, ValueError): + module_logger.warning("Failed to restore pipeline component asset input defaults.") # Pop jobs to avoid it goes with schema load jobs = obj.properties.component_spec.pop("jobs", None) init_params_dict: dict = super()._from_rest_object_to_init_params(obj) @@ -509,11 +524,21 @@ def _to_rest_object(self) -> ComponentVersion: # hack while full pass through supported is worked on for IPP fields component.pop("intellectual_property") component["intellectualProperty"] = self._intellectual_property._to_rest_object() + properties = dict(self.properties) if self.properties else {} + asset_input_defaults = { + input_name: component_input.default + for input_name, component_input in self.inputs.items() + if component_input.type in IOConstants.ASSET_INPUT_TYPES and component_input.default is not None + } + if asset_input_defaults: + properties[_ASSET_INPUT_DEFAULTS_PROPERTY] = json.dumps(asset_input_defaults) + else: + properties.pop(_ASSET_INPUT_DEFAULTS_PROPERTY, None) properties = ComponentVersionProperties( component_spec=component, description=self.description, is_anonymous=self._is_anonymous, - properties=self.properties, + properties=properties, tags=self.tags, ) result = ComponentVersion(properties=properties) diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_inputs_outputs/input.py b/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_inputs_outputs/input.py index 4a9451084bc3..7b2ba7d5f833 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_inputs_outputs/input.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_inputs_outputs/input.py @@ -373,6 +373,17 @@ def _update_default(self, default_value: Any) -> None: msg_prefix = f"Default value of Input {name}" if not self._is_primitive_type and default_value is not None: + # Asset-type inputs accept a string default value (e.g. "azureml:", "https://", + # local path) matching the public CLI v2 YAML schema. + if not self._multiple_types and self.type in IOConstants.ASSET_INPUT_TYPES: + if isinstance(default_value, str): + self.default: Any = default_value + return + msg = ( + f"{msg_prefix}cannot be set: default for type '{self.type}' must be a " + f"string asset reference, got '{type(default_value)}'." + ) + raise UserErrorException(msg) msg = f"{msg_prefix}cannot be set: Non-primitive type Input has no default value." raise UserErrorException(msg) if isinstance(default_value, float) and not math.isfinite(default_value): diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_job/pipeline/_io/mixin.py b/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_job/pipeline/_io/mixin.py index 1a22b9814016..981cadfb7479 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_job/pipeline/_io/mixin.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_job/pipeline/_io/mixin.py @@ -12,7 +12,7 @@ from azure.ai.ml._restclient.arm_ml_service.models import ( JobOutput as RestJobOutput, ) -from azure.ai.ml.constants._component import ComponentJobConstants +from azure.ai.ml.constants._component import ComponentJobConstants, IOConstants from azure.ai.ml.entities._inputs_outputs import GroupInput, Input, Output from azure.ai.ml.entities._util import copy_output_setting from azure.ai.ml.exceptions import ErrorTarget, ValidationErrorType, ValidationException @@ -118,6 +118,8 @@ def _build_output(self, name: str, meta: Optional[Output], data: Optional[Union[ # pylint: disable=unused-argument def _get_default_input_val(self, val: Any): # type: ignore + if isinstance(val, Input) and val.type in IOConstants.ASSET_INPUT_TYPES and val.default is not None: + return val.default # use None value as data placeholder for unfilled inputs. # server side will fill the default value return None @@ -644,6 +646,8 @@ def _get_default_input_val(self, val: Any): # type: ignore if isinstance(val, GroupInput): # Copy default value dict for group return copy.deepcopy(val.default) + if isinstance(val, Input) and val.type in IOConstants.ASSET_INPUT_TYPES and val.default is not None: + return Input(type=val.type, path=val.default, mode=val.mode) return val.default def _update_output_types(self, rest_data_outputs: Dict) -> None: diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/operations/_component_operations.py b/sdk/ml/azure-ai-ml/azure/ai/ml/operations/_component_operations.py index 82edf499ca85..6ccd66009ac5 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/operations/_component_operations.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/operations/_component_operations.py @@ -673,9 +673,21 @@ def create_or_update( ) if not result: - component = self.get(name=name, version=version) + created_component = self.get(name=name, version=version) else: - component = Component._from_rest_object(result) + created_component = Component._from_rest_object(result) + + if isinstance(component, PipelineComponent) and isinstance(created_component, PipelineComponent): + for input_name, source_input in component.inputs.items(): + created_input = created_component.inputs.get(input_name) + if ( + created_input + and not source_input._is_primitive_type + and source_input.default is not None + and created_input.default is None + ): + created_input.default = source_input.default + component = created_component self._resolve_azureml_id( component=component, diff --git a/sdk/ml/azure-ai-ml/tests/component/unittests/test_component_operations.py b/sdk/ml/azure-ai-ml/tests/component/unittests/test_component_operations.py index 50b8c886ee85..9fefd904895b 100644 --- a/sdk/ml/azure-ai-ml/tests/component/unittests/test_component_operations.py +++ b/sdk/ml/azure-ai-ml/tests/component/unittests/test_component_operations.py @@ -3,6 +3,7 @@ import pytest +from azure.ai.ml import Input from azure.ai.ml._restclient.arm_ml_service.models import ( ComponentContainer as ComponentContainerData, ComponentContainerProperties as ComponentContainerDetails, @@ -11,6 +12,7 @@ SystemData, ) from azure.ai.ml._scope_dependent_operations import OperationConfig, OperationScope +from azure.ai.ml.entities import PipelineComponent from azure.ai.ml.entities._component.command_component import CommandComponent from azure.ai.ml.entities._component.component import Component from azure.ai.ml.operations import ComponentOperations @@ -84,6 +86,84 @@ def test_create_skip_validation(self, mock_component_operation: ComponentOperati mock_component_operation.create_or_update(component) mock_thing.assert_called_once() + def test_create_preserves_pipeline_asset_input_default(self, mock_component_operation: ComponentOperations) -> None: + component = PipelineComponent( + name="pipeline_component", + version="1", + inputs={"default_data": Input(type="uri_file", default="azureml:test_data:1")}, + jobs={}, + ) + service_component = PipelineComponent( + name="pipeline_component", + version="1", + inputs={"default_data": Input(type="uri_file")}, + jobs={}, + ) + + with patch.object(ComponentOperations, "_resolve_arm_id_or_upload_dependencies"), patch( + "azure.ai.ml.operations._component_operations.Component._from_rest_object", + return_value=service_component, + ): + created_component = mock_component_operation.create_or_update(component) + + assert created_component.inputs["default_data"].default == "azureml:test_data:1" + node = created_component() + built_input = node._build_inputs()["default_data"] + assert built_input.type == "uri_file" + assert built_input.path == "azureml:test_data:1" + assert node._to_rest_inputs()["default_data"] == { + "uri": "azureml:test_data:1", + "job_input_type": "uri_file", + } + assert node._to_job()._to_rest_object().properties.inputs["default_data"].as_dict() == { + "uri": "azureml:test_data:1", + "jobInputType": "uri_file", + } + + second_node = created_component() + assert node._build_inputs()["default_data"] is not second_node._build_inputs()["default_data"] + node._build_inputs()["default_data"].path = "azureml:mutated:1" + assert second_node._build_inputs()["default_data"].path == "azureml:test_data:1" + + def test_create_preserves_pipeline_asset_input_default_after_get( + self, mock_component_operation: ComponentOperations + ) -> None: + component = PipelineComponent( + name="pipeline_component", + version="1", + inputs={"default_data": Input(type="uri_file", default="azureml:test_data:1")}, + jobs={}, + ) + service_component = PipelineComponent( + name="pipeline_component", + version="1", + inputs={"default_data": Input(type="uri_file")}, + jobs={}, + ) + + with patch.object(ComponentOperations, "_resolve_arm_id_or_upload_dependencies"), patch.object( + ComponentOperations, "_create_or_update_component_version", return_value=None + ), patch.object(ComponentOperations, "get", return_value=service_component): + created_component = mock_component_operation.create_or_update(component) + + assert created_component.inputs["default_data"].default == "azureml:test_data:1" + + def test_get_restores_pipeline_asset_input_default(self, mock_component_operation: ComponentOperations) -> None: + component = PipelineComponent( + name="pipeline_component", + version="1", + inputs={"default_data": Input(type="uri_file", default="azureml:test_data:1")}, + jobs={}, + ) + rest_component = component._to_rest_object() + rest_component.properties.component_spec["inputs"]["default_data"].pop("default") + mock_component_operation._version_operation.get.return_value = rest_component + + fetched_component = mock_component_operation.get(name="pipeline_component", version="1") + + assert fetched_component.inputs["default_data"].default == "azureml:test_data:1" + assert fetched_component()._build_inputs()["default_data"].path == "azureml:test_data:1" + def test_create_autoincrement( self, mock_component_operation: ComponentOperations, mock_component_from_rest ) -> None: diff --git a/sdk/ml/azure-ai-ml/tests/component/unittests/test_pipeline_component_entity.py b/sdk/ml/azure-ai-ml/tests/component/unittests/test_pipeline_component_entity.py index 9472d2390961..2525bf1982c8 100644 --- a/sdk/ml/azure-ai-ml/tests/component/unittests/test_pipeline_component_entity.py +++ b/sdk/ml/azure-ai-ml/tests/component/unittests/test_pipeline_component_entity.py @@ -13,6 +13,7 @@ from azure.ai.ml.entities import Component, PipelineComponent, PipelineJob from azure.ai.ml.entities._inputs_outputs import GroupInput from azure.ai.ml.entities._job.pipeline._io import PipelineInput, _GroupAttrDict +from azure.ai.ml.exceptions import UserErrorException from azure.ai.ml.operations import ComponentOperations from .._util import _COMPONENT_TIMEOUT_SECOND @@ -465,3 +466,92 @@ def get_layer_node_name_set(layer): # all leaf nodes in last layer # 2 leaf node of the same node name assert get_layer_node_name_set(layers[2]) == {"command_component", "component_a_job"} + + def test_pipeline_component_asset_type_defaults(self) -> None: + """Asset-type input defaults can be loaded from pipeline component YAML.""" + component_path = "./tests/test_configs/components/pipeline_component_with_asset_defaults.yml" + component: PipelineComponent = load_component(source=component_path) + inputs = component.inputs + + # uri_file with azureml: reference + assert inputs["spaceship_data"].type == "uri_file" + assert inputs["spaceship_data"].default == "azureml:test_dataset:1" + + # uri_folder with https:// URI + assert inputs["folder_data"].type == "uri_folder" + assert inputs["folder_data"].default == "https://example.com/data" + + # mltable + assert inputs["table_data"].type == "mltable" + assert inputs["table_data"].default == "azureml:test_mltable:1" + + # mlflow_model + assert inputs["model_input"].type == "mlflow_model" + assert inputs["model_input"].default == "azureml:test_model:1" + + # custom_model + assert inputs["custom_model_input"].type == "custom_model" + assert inputs["custom_model_input"].default == "azureml:test_custom_model:1" + + # input without default is unaffected + assert inputs["pilot_data"].default is None + + def test_pipeline_component_asset_default_round_trip(self) -> None: + """Dumping a loaded component with asset-type defaults must preserve the default values.""" + component_path = "./tests/test_configs/components/pipeline_component_with_asset_defaults.yml" + component: PipelineComponent = load_component(source=component_path) + component_dict = component._to_dict() + + assert component_dict["inputs"]["spaceship_data"]["default"] == "azureml:test_dataset:1" + assert component_dict["inputs"]["folder_data"]["default"] == "https://example.com/data" + assert component_dict["inputs"]["table_data"]["default"] == "azureml:test_mltable:1" + assert component_dict["inputs"]["model_input"]["default"] == "azureml:test_model:1" + assert component_dict["inputs"]["custom_model_input"]["default"] == "azureml:test_custom_model:1" + assert "default" not in component_dict["inputs"]["pilot_data"] + + def test_pipeline_component_asset_default_rest_round_trip(self) -> None: + """Asset defaults survive when the service omits them from the component spec.""" + component_path = "./tests/test_configs/components/pipeline_component_with_asset_defaults.yml" + component: PipelineComponent = load_component(source=component_path) + + rest_obj = component._to_rest_object() + for input_spec in rest_obj.properties.component_spec["inputs"].values(): + input_spec.pop("default", None) + restored = PipelineComponent._from_rest_object(rest_obj) + + assert restored.inputs["spaceship_data"].default == "azureml:test_dataset:1" + assert restored.inputs["folder_data"].default == "https://example.com/data" + assert restored.inputs["table_data"].default == "azureml:test_mltable:1" + + def test_pipeline_component_asset_default_inline(self) -> None: + """Input() constructor must accept a string default for asset types.""" + uri_file_input = Input(type="uri_file", default="azureml:my_dataset:1", mode="ro_mount") + assert uri_file_input.default == "azureml:my_dataset:1" + + uri_folder_input = Input(type="uri_folder", default="https://example.com/folder") + assert uri_folder_input.default == "https://example.com/folder" + + mltable_input = Input(type="mltable", default="azureml:my_table:2") + assert mltable_input.default == "azureml:my_table:2" + + mlflow_input = Input(type="mlflow_model", default="azureml:my_model:3") + assert mlflow_input.default == "azureml:my_model:3" + + custom_input = Input(type="custom_model", default="azureml:my_custom:4") + assert custom_input.default == "azureml:my_custom:4" + + def test_pipeline_component_asset_default_invalid_type_raises(self) -> None: + """Passing a non-string, non-Input value as default for an asset type must still raise.""" + with pytest.raises(UserErrorException, match="default for type 'uri_file' must be"): + Input(type="uri_file", default=42) # type: ignore[arg-type] + + def test_pipeline_component_non_asset_default_still_raises(self) -> None: + """Non-asset non-primitive types must still raise the original error.""" + # Provide a type that is not primitive and not in _ASSET_TYPES. + # We simulate this by creating an Input with an unknown type via kwargs. + inp = Input.__new__(Input) + inp.type = "unknown_type" + inp._port_name = "test_input" + + with pytest.raises(UserErrorException, match="Non-primitive type Input has no default value"): + inp._update_default("some_value") # type: ignore[arg-type] diff --git a/sdk/ml/azure-ai-ml/tests/pipeline_job/e2etests/test_pipeline_job.py b/sdk/ml/azure-ai-ml/tests/pipeline_job/e2etests/test_pipeline_job.py index 1552aac23d64..3cc8118f29ff 100644 --- a/sdk/ml/azure-ai-ml/tests/pipeline_job/e2etests/test_pipeline_job.py +++ b/sdk/ml/azure-ai-ml/tests/pipeline_job/e2etests/test_pipeline_job.py @@ -17,6 +17,7 @@ from azure.ai.ml.entities._builders.parallel import Parallel from azure.ai.ml.entities._builders.spark import Spark from azure.ai.ml.exceptions import JobException +from azure.ai.ml.operations._run_history_constants import JobStatus from azure.core.exceptions import HttpResponseError from azure.core.serialization import as_attribute_dict @@ -2078,6 +2079,36 @@ def test_pipeline_job_with_flow( class TestPipelineJobLongRunning: """Long-running tests that require pipeline job completed.""" + def test_pipeline_component_uri_file_default(self, client: MLClient, randstr: Callable[[str], str]) -> None: + """A registered pipeline component can run with an omitted uri_file input default.""" + data = load_data( + source="./tests/test_configs/dataset/data_file.yaml", + params_override=[{"name": randstr("pipeline_component_default_data")}], + ) + data_asset = client.data.create_or_update(data) + data_asset_reference = f"azureml:{data_asset.name}:{data_asset.version}" + + component = load_component( + source="./tests/test_configs/components/pipeline_component_with_uri_file_default.yml", + params_override=[ + {"name": randstr("pipeline_component_with_uri_file_default")}, + {"inputs.default_data.default": data_asset_reference}, + ], + ) + registered_component = client.components.create_or_update(component) + fetched_component = client.components.get(name=registered_component.name, version=registered_component.version) + + pipeline_job = fetched_component() + pipeline_job.settings.default_compute = "cpu-cluster" + job = client.jobs.create_or_update(pipeline_job) + status = wait_until_done(client, job, timeout=_PIPELINE_JOB_LONG_RUNNING_TIMEOUT_SECOND) + + print("Data asset:", data_asset.id) + print("Pipeline component:", registered_component.id) + print("Pipeline job:", job.studio_url) + print("Final status:", status) + assert status == JobStatus.COMPLETED + def test_pipeline_job_get_child_run(self, client: MLClient, randstr: Callable[[str], str]): pipeline_job = load_job( source="./tests/test_configs/pipeline_jobs/helloworld_pipeline_job_quick_with_output.yml", diff --git a/sdk/ml/azure-ai-ml/tests/test_configs/components/pipeline_component_with_asset_defaults.yml b/sdk/ml/azure-ai-ml/tests/test_configs/components/pipeline_component_with_asset_defaults.yml new file mode 100644 index 000000000000..6435222d7028 --- /dev/null +++ b/sdk/ml/azure-ai-ml/tests/test_configs/components/pipeline_component_with_asset_defaults.yml @@ -0,0 +1,32 @@ +$schema: https://azuremlschemas.azureedge.net/latest/pipelineComponent.schema.json +name: test_pipeline_component_asset_defaults +type: pipeline +display_name: "Test Pipeline Component with Asset-type Defaults" +inputs: + year: + type: integer + spaceship_data: + type: uri_file + mode: ro_mount + default: azureml:test_dataset:1 + folder_data: + type: uri_folder + mode: ro_mount + default: https://example.com/data + table_data: + type: mltable + default: azureml:test_mltable:1 + model_input: + type: mlflow_model + default: azureml:test_model:1 + custom_model_input: + type: custom_model + default: azureml:test_custom_model:1 + pilot_data: + type: uri_file + mode: ro_mount +outputs: + pilot_rosta: + type: uri_file + mode: rw_mount +jobs: {} diff --git a/sdk/ml/azure-ai-ml/tests/test_configs/components/pipeline_component_with_uri_file_default.yml b/sdk/ml/azure-ai-ml/tests/test_configs/components/pipeline_component_with_uri_file_default.yml new file mode 100644 index 000000000000..235c382b2d21 --- /dev/null +++ b/sdk/ml/azure-ai-ml/tests/test_configs/components/pipeline_component_with_uri_file_default.yml @@ -0,0 +1,17 @@ +$schema: https://azuremlschemas.azureedge.net/latest/pipelineComponent.schema.json +type: pipeline +name: pipeline_component_with_uri_file_default +version: 1 +inputs: + default_data: + type: uri_file + default: azureml:placeholder:1 +jobs: + read_default_data: + type: command + command: >- + python -c "from pathlib import Path; path = Path(r'${{inputs.default_data}}'); + assert path.is_file(); print(path.read_text()[:20])" + environment: azureml:AzureML-sklearn-1.0-ubuntu20.04-py38-cpu@latest + inputs: + default_data: ${{parent.inputs.default_data}} \ No newline at end of file