From 5aa25e18e7f98d6b398fee870e6322bac7369864 Mon Sep 17 00:00:00 2001 From: Saanika Gupta Date: Mon, 27 Jul 2026 14:50:25 +0530 Subject: [PATCH] Fix mltable load of registered data asset: restore DataVersionEntity.additional_properties The dataset_dataplane TypeSpec (hybrid) migration (#45389) dropped the msrest additional_properties attribute from DataVersionEntity. The mltable package's local data-asset resolution path (MLClient.jobs._dataset_dataplane_operations._operation.get) reads additional_properties['isV2'|'legacyDataflow'], so loading a registered MLTable data asset via mltable.load('azureml://.../data//versions/') raised AttributeError. Restored the attribute as a regeneration-safe compat shim in models/_patch.py returning the un-modeled wire keys. Wire contract unchanged. --- sdk/ml/azure-ai-ml/CHANGELOG.md | 1 + .../dataset_dataplane/models/_patch.py | 36 ++++++++++++ ...dataset_dataplane_additional_properties.py | 57 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 sdk/ml/azure-ai-ml/tests/dataset/unittests/test_dataset_dataplane_additional_properties.py diff --git a/sdk/ml/azure-ai-ml/CHANGELOG.md b/sdk/ml/azure-ai-ml/CHANGELOG.md index 1b140b337aaf..45ddd2fef195 100644 --- a/sdk/ml/azure-ai-ml/CHANGELOG.md +++ b/sdk/ml/azure-ai-ml/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed `deployment_templates.list(name=...)` raising `AttributeError: 'str' object has no attribute 'request_timeout'`. In the list response, `requestSettings` / `livenessProbe` / `readinessProbe` arrive as stringified dicts nested under `properties`; these are now parsed before conversion, giving `list()` parity with `models.list()` / `environments.list()`. - Fixed `deployment_templates.get(name)` failing with a 404 (`DeploymentTemplate {name}:latest not found`) when no version was supplied, because the literal string `"latest"` was sent as the version. The latest version is now resolved client-side (the service exposes no `latest` label and no server-side ordering), and `get()` accepts a `label` keyword (`label="latest"` resolves to the latest version) mirroring `models.get()`. `delete(name)` resolves the latest version the same way. - Fixed `models.get(name, label="latest")` returning a `Model` whose `default_deployment_template` / `allowed_deployment_templates` references had `asset_id=None`. Label resolution goes through the version list endpoint (`top=1`), whose items omit the deployment-template references; for registry models the resolved version is now re-fetched through the get endpoint, so the label path hydrates these references identically to the explicit `version=` path. +- Fixed loading a registered MLTable data asset via `mltable.load("azureml://.../data//versions/")` failing with `AttributeError: 'DataVersionEntity' object has no attribute 'additional_properties'`. When the `dataset_dataplane` client was migrated to the TypeSpec (hybrid) model, `DataVersionEntity` stopped exposing the msrest `additional_properties` attribute that the `mltable` package reads (`isV2` / `legacyDataflow`) on the local resolution path. The attribute is now restored as a compatibility shim returning the un-modeled wire keys, so the on-the-wire contract is unchanged. ### Other Changes - Migrated SDK entities and their consumers off the per-version msrest REST clients onto the shared `arm_ml_service` hybrid client. This is an internal change; the on-the-wire request/response contract is unchanged. diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/_restclient/dataset_dataplane/models/_patch.py b/sdk/ml/azure-ai-ml/azure/ai/ml/_restclient/dataset_dataplane/models/_patch.py index ea765788358a..e538277fa9f5 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/_restclient/dataset_dataplane/models/_patch.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/_restclient/dataset_dataplane/models/_patch.py @@ -8,9 +8,45 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ +from typing import Any, Dict + +from ._models import DataVersionEntity + __all__: list[str] = [] # Add all objects you want publicly available to users at this package level +def _additional_properties(self: DataVersionEntity) -> Dict[str, Any]: + """Backwards-compatibility shim for the removed msrest ``additional_properties`` attribute. + + When this client was migrated to TypeSpec, ``DataVersionEntity`` became a hybrid + (``MutableMapping``) model, which does not expose the msrest ``additional_properties`` + attribute. External consumers still depend on it -- notably the ``mltable`` package's local + data-asset resolution path (``MLClient.jobs._dataset_dataplane_operations._operation.get``), + which reads ``data_version.additional_properties['isV2' | 'legacyDataflow']``. Losing the + attribute raised ``AttributeError`` when loading an ``azureml://`` MLTable data asset. + + This restores the original contract by returning the un-modeled wire keys, i.e. the keys + present on the wire payload that are not backed by a declared ``rest_field`` on the model. + + :return: The wire properties not declared as fields on the model. + :rtype: dict[str, typing.Any] + """ + # Wire names of the declared fields (e.g. ``dataVersion``, ``entityMetadata``). + modeled = { + getattr(rf, "_rest_name", None) # pylint: disable=protected-access + for rf in getattr(self, "_attr_to_rest_field", {}).values() + } + raw = getattr(self, "_data", None) # pylint: disable=protected-access + items = raw.items() if isinstance(raw, dict) else self.items() + return {key: value for key, value in items if key not in modeled} + + +# Attach only when the generated model does not already provide it, so a future TypeSpec +# regeneration that restores ``additional_properties`` natively takes precedence. +if not hasattr(DataVersionEntity, "additional_properties"): + DataVersionEntity.additional_properties = property(_additional_properties) # type: ignore[attr-defined] + + def patch_sdk(): """Do not remove from this file. diff --git a/sdk/ml/azure-ai-ml/tests/dataset/unittests/test_dataset_dataplane_additional_properties.py b/sdk/ml/azure-ai-ml/tests/dataset/unittests/test_dataset_dataplane_additional_properties.py new file mode 100644 index 000000000000..3eb169d797db --- /dev/null +++ b/sdk/ml/azure-ai-ml/tests/dataset/unittests/test_dataset_dataplane_additional_properties.py @@ -0,0 +1,57 @@ +import pytest + +from azure.ai.ml._restclient.dataset_dataplane.models import DataVersionEntity + + +@pytest.mark.unittest +@pytest.mark.data_experiences_test +class TestDataVersionEntityAdditionalProperties: + """Regression guard for the ``mltable`` local data-asset resolution path. + + ``mltable.load("azureml://.../data//versions/")`` fetches a ``DataVersionEntity`` + via ``MLClient.jobs._dataset_dataplane_operations._operation.get`` and reads + ``data_version.additional_properties['isV2' | 'legacyDataflow']``. The TypeSpec (hybrid) model + dropped the msrest ``additional_properties`` attribute; the ``models/_patch.py`` shim restores it. + """ + + def _wire(self): + return { + "dataVersion": {"dataUri": "azureml://datastores/x/paths/y", "dataType": "mltable"}, + "entityMetadata": {"etag": "abc"}, + "isV2": True, + "legacyDataflow": "some-legacy-dataflow-yaml", + } + + def test_additional_properties_exposes_unmodeled_wire_keys(self): + entity = DataVersionEntity._deserialize(self._wire(), []) + + # mltable reads these exact keys off ``additional_properties``. + assert entity.additional_properties["isV2"] is True + assert entity.additional_properties["legacyDataflow"] == "some-legacy-dataflow-yaml" + + def test_additional_properties_excludes_modeled_fields(self): + entity = DataVersionEntity._deserialize(self._wire(), []) + + # Declared fields must not leak into ``additional_properties`` (msrest parity). + assert "dataVersion" not in entity.additional_properties + assert "entityMetadata" not in entity.additional_properties + + # Declared fields remain accessible via their model attributes. + assert entity.data_version.data_uri == "azureml://datastores/x/paths/y" + assert entity.data_version.data_type == "mltable" + + def test_additional_properties_empty_when_no_extra_keys(self): + entity = DataVersionEntity._deserialize( + {"dataVersion": {"dataUri": "azureml://x", "dataType": "uri_folder"}}, [] + ) + + assert entity.additional_properties == {} + + def test_missing_key_raises_key_error(self): + # mltable relies on ``KeyError`` (caught) when ``legacyDataflow`` is absent. + entity = DataVersionEntity._deserialize( + {"dataVersion": {"dataUri": "azureml://x", "dataType": "mltable"}, "isV2": False}, [] + ) + + with pytest.raises(KeyError): + _ = entity.additional_properties["legacyDataflow"]