azureml-registry-tools 0.1.0a3__tar.gz → 0.1.0a5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {azureml_registry_tools-0.1.0a3/azureml_registry_tools.egg-info → azureml_registry_tools-0.1.0a5}/PKG-INFO +1 -1
  2. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/_cli/registry_syndication_cli.py +53 -1
  3. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/_rest_client/registry_management_client.py +1 -1
  4. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/mgmt/syndication_manifest.py +76 -0
  5. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/tools/create_or_update_assets.py +33 -15
  6. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5/azureml_registry_tools.egg-info}/PKG-INFO +1 -1
  7. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/setup.py +1 -1
  8. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/LICENSE.txt +0 -0
  9. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/__init__.py +0 -0
  10. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/__init__.py +0 -0
  11. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/_cli/__init__.py +0 -0
  12. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/_cli/repo2registry_cli.py +0 -0
  13. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/_rest_client/__init__.py +0 -0
  14. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/_rest_client/arm_client.py +0 -0
  15. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/_rest_client/base_rest_client.py +0 -0
  16. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/mgmt/__init__.py +0 -0
  17. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/mgmt/create_manifest.py +0 -0
  18. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/tools/__init__.py +0 -0
  19. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/tools/config.py +0 -0
  20. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/tools/registry_utils.py +0 -0
  21. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml/registry/tools/repo2registry_config.py +0 -0
  22. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml_registry_tools.egg-info/SOURCES.txt +0 -0
  23. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml_registry_tools.egg-info/dependency_links.txt +0 -0
  24. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml_registry_tools.egg-info/entry_points.txt +0 -0
  25. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml_registry_tools.egg-info/requires.txt +0 -0
  26. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/azureml_registry_tools.egg-info/top_level.txt +0 -0
  27. {azureml_registry_tools-0.1.0a3 → azureml_registry_tools-0.1.0a5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: azureml-registry-tools
3
- Version: 0.1.0a3
3
+ Version: 0.1.0a5
4
4
  Summary: AzureML Registry tools and CLI
5
5
  Author: Microsoft Corp
6
6
  License: https://aka.ms/azureml-sdk-license
@@ -3,10 +3,12 @@
3
3
  # ---------------------------------------------------------
4
4
  import argparse
5
5
  import json
6
+ import sys
7
+ from uuid import UUID
6
8
  from azureml.registry._rest_client.registry_management_client import RegistryManagementClient
7
9
  from azureml.registry._rest_client.arm_client import ArmClient
8
10
  from azureml.registry.mgmt.create_manifest import generate_syndication_manifest
9
- from azureml.registry.mgmt.syndication_manifest import SyndicationManifest
11
+ from azureml.registry.mgmt.syndication_manifest import SyndicationManifest, ResyncAssetsInManifestDto
10
12
 
11
13
 
12
14
  def syndication_manifest_show(registry_name: str) -> dict:
@@ -56,6 +58,31 @@ def syndication_manifest_delete(registry_name: str, dry_run: bool) -> None:
56
58
  RegistryManagementClient(registry_name=registry_name).delete_manifest()
57
59
 
58
60
 
61
+ def syndication_manifest_sync(registry_name: str, manifest_value: str, tenant_id: str, asset_ids: list, dry_run: bool) -> None:
62
+ """Sync assets in the syndication manifest for the specified registry.
63
+
64
+ Args:
65
+ registry_name (str): Name of the AzureML registry.
66
+ manifest_value (str): Manifest value as a string (JSON).
67
+ tenant_id (str): Tenant ID to use for all assets.
68
+ asset_ids (list): List of asset IDs to sync.
69
+ dry_run (bool): If True, do not perform any changes.
70
+ """
71
+ if manifest_value:
72
+ # for inline manifest value allow different casing of keys
73
+ sync_dto = ResyncAssetsInManifestDto.from_dict(json.loads(manifest_value), normalize_keys=True)
74
+ else:
75
+ # Create from tenant ID and asset IDs
76
+ sync_dto = ResyncAssetsInManifestDto.from_asset_list(UUID(tenant_id), asset_ids)
77
+
78
+ dto = sync_dto.to_dict()
79
+ if dry_run:
80
+ print(f"Dry run: Would sync assets for registry {registry_name} with data: {json.dumps(dto, indent=2)}")
81
+ else:
82
+ client = RegistryManagementClient(registry_name=registry_name)
83
+ client.sync_assets_in_manifest(dto)
84
+
85
+
59
86
  def syndication_target_show(registry_name: str) -> object:
60
87
  """Show the current syndication target(s) for the specified registry and region.
61
88
 
@@ -129,6 +156,12 @@ def main() -> None:
129
156
  # Delete the current manifest
130
157
  registry-mgmt syndication manifest delete --registry-name myreg
131
158
 
159
+ # Sync assets from a JSON value
160
+ registry-mgmt syndication manifest sync --registry-name myreg --value '{"AssetsToResync": [...]}'
161
+
162
+ # Sync assets from tenant ID and asset list
163
+ registry-mgmt syndication manifest sync --registry-name myreg --tenant-id "12345678-1234-1234-1234-123456789012" --asset-ids asset1 asset2 asset3
164
+
132
165
  # Show the current target
133
166
  registry-mgmt syndication target show --registry-name myreg
134
167
 
@@ -184,6 +217,13 @@ def main() -> None:
184
217
  manifest_delete_parser = manifest_subparsers.add_parser("delete", help="Delete the current manifest.")
185
218
  _add_common_args(manifest_delete_parser, [registry_name_arg, dry_run_arg])
186
219
 
220
+ manifest_sync_parser = manifest_subparsers.add_parser("sync", help="Sync assets in the manifest.")
221
+ _add_common_args(manifest_sync_parser, [registry_name_arg, dry_run_arg])
222
+ sync_group = manifest_sync_parser.add_mutually_exclusive_group(required=True)
223
+ sync_group.add_argument("-v", "--value", type=str, help="Manifest sync value as JSON string.")
224
+ sync_group.add_argument("-t", "--tenant-id", type=str, help="Tenant ID to use for all assets.")
225
+ manifest_sync_parser.add_argument("-a", "--asset-ids", type=str, nargs="+", help="List of asset IDs to sync (only valid with --tenant-id).")
226
+
187
227
  # syndication target
188
228
  target_parser = synd_subparsers.add_parser("target", help="Manage syndication target.")
189
229
  target_subparsers = target_parser.add_subparsers(dest="target_subcommand", required=True)
@@ -215,6 +255,18 @@ def main() -> None:
215
255
  syndication_manifest_delete(args.registry_name, args.dry_run)
216
256
  else:
217
257
  print("Manifest deletion cancelled.")
258
+ elif args.manifest_subcommand == "sync":
259
+ # Validate constraints for sync command
260
+ if args.tenant_id and not args.asset_ids:
261
+ print("Error: --asset-ids is required when using --tenant-id", file=sys.stderr)
262
+ sys.exit(1)
263
+ elif args.asset_ids and not args.tenant_id:
264
+ print("Error: --asset-ids can only be used with --tenant-id", file=sys.stderr)
265
+ sys.exit(1)
266
+ elif not args.value and not args.tenant_id:
267
+ print("Error: Either --value or --tenant-id must be specified", file=sys.stderr)
268
+ sys.exit(1)
269
+ syndication_manifest_sync(args.registry_name, args.value, args.tenant_id, args.asset_ids, args.dry_run)
218
270
  elif args.synd_subcommand == "target":
219
271
  if args.target_subcommand == "show":
220
272
  print(syndication_target_show(args.registry_name))
@@ -57,7 +57,7 @@ class RegistryManagementClient(BaseAzureRestClient):
57
57
  response = self.post(url, json=manifest_dto)
58
58
  return response.json()
59
59
 
60
- def resync_assets_in_manifest(self, resync_assets_dto: dict) -> dict:
60
+ def sync_assets_in_manifest(self, resync_assets_dto: dict) -> dict:
61
61
  """
62
62
  Resynchronize assets in the syndication manifest.
63
63
 
@@ -148,3 +148,79 @@ class SyndicationManifest:
148
148
  tenant_id=tenant_id,
149
149
  source_registries=source_registries
150
150
  )
151
+
152
+
153
+ @dataclass
154
+ class AssetToResync:
155
+ """Represents an asset to resync with its associated tenant ID."""
156
+
157
+ asset_id: str
158
+ tenant_id: UUID
159
+
160
+ def to_dict(self) -> dict:
161
+ """Convert the AssetToResync instance to a dictionary."""
162
+ return {
163
+ "AssetId": self.asset_id,
164
+ "TenantId": str(self.tenant_id)
165
+ }
166
+
167
+ @staticmethod
168
+ def from_dict(d: dict, normalize_keys=False) -> "AssetToResync":
169
+ """Create an AssetToResync instance from a dictionary."""
170
+ asset_id = _get_key(d, "AssetId", "asset_id", normalize_keys=normalize_keys)
171
+ if asset_id is None:
172
+ raise ValueError("Missing required field 'AssetId' in dictionary.")
173
+
174
+ tenant_id_str = _get_key(d, "TenantId", "tenant_id", normalize_keys=normalize_keys)
175
+ if tenant_id_str is None:
176
+ raise ValueError("Missing required field 'TenantId' in dictionary.")
177
+
178
+ try:
179
+ tenant_id = UUID(str(tenant_id_str))
180
+ except ValueError:
181
+ raise ValueError("TenantId must be a valid UUID string.")
182
+
183
+ return AssetToResync(
184
+ asset_id=str(asset_id),
185
+ tenant_id=tenant_id
186
+ )
187
+
188
+
189
+ @dataclass
190
+ class ResyncAssetsInManifestDto:
191
+ """Represents a collection of assets to resync in a manifest."""
192
+
193
+ assets_to_resync: List[AssetToResync]
194
+
195
+ def to_dict(self) -> dict:
196
+ """Convert the ResyncAssetsInManifestDto instance to a dictionary."""
197
+ return {
198
+ "AssetsToResync": [asset.to_dict() for asset in self.assets_to_resync]
199
+ }
200
+
201
+ @staticmethod
202
+ def from_dict(d: dict, normalize_keys=False) -> "ResyncAssetsInManifestDto":
203
+ """Create a ResyncAssetsInManifestDto instance from a dictionary."""
204
+ assets_list = _get_key(d, "AssetsToResync", "assets_to_resync", normalize_keys=normalize_keys)
205
+ if not isinstance(assets_list, list):
206
+ raise ValueError("AssetsToResync must be a list.")
207
+ return ResyncAssetsInManifestDto(
208
+ assets_to_resync=[AssetToResync.from_dict(asset, normalize_keys=normalize_keys) for asset in assets_list]
209
+ )
210
+
211
+ @staticmethod
212
+ def from_asset_list(tenant_id: UUID, asset_ids: List[str]) -> "ResyncAssetsInManifestDto":
213
+ """Create a ResyncAssetsInManifestDto instance from a tenant ID and list of asset ID strings.
214
+
215
+ Args:
216
+ tenant_id (UUID): The tenant ID to use for all assets.
217
+ asset_ids (List[str]): List of asset ID strings.
218
+
219
+ Returns:
220
+ ResyncAssetsInManifestDto: Instance with assets created from the provided IDs and tenant ID.
221
+ """
222
+ assets_to_resync = [
223
+ AssetToResync(asset_id=asset_id, tenant_id=tenant_id)
224
+ for asset_id in asset_ids
225
+ ]
226
+ return ResyncAssetsInManifestDto(assets_to_resync=assets_to_resync)
@@ -5,6 +5,7 @@
5
5
  """Create or update assets."""
6
6
 
7
7
  import azure
8
+ import copy
8
9
  import json
9
10
  import os
10
11
  import re
@@ -14,7 +15,8 @@ from git import InvalidGitRepositoryError, Repo
14
15
  from pathlib import Path
15
16
  from typing import List, Union
16
17
 
17
- from azure.ai.ml import MLClient, load_data, load_model
18
+ from azure.ai.ml import MLClient, load_component, load_data, load_model
19
+ from azure.ai.ml.entities._component.component import Component
18
20
  from azure.ai.ml.entities._assets._artifacts.data import Data
19
21
  from azure.ai.ml.entities._assets._artifacts.model import Model
20
22
  from azure.identity import DefaultAzureCredential
@@ -121,38 +123,52 @@ def find_assets(input_dir: Path,
121
123
 
122
124
 
123
125
  def merge_yamls(existing_asset_file_name: str,
124
- updated_asset_file_name: str) -> dict:
125
- """Overwrite values from updated asset to existing asset with the exception of storage path.
126
+ updated_asset: AssetSpec) -> dict:
127
+ """Preserve storage value from existing asset to updated asset.
126
128
 
127
129
  Args:
128
130
  existing_asset_file_name (str): Name of file containing details of asset currently in registry.
129
- updated_asset_file_name (str): Name of file containing updated asset.
130
-
131
+ updated_asset (AssetSpec): AssetSpec containing updated asset info.
131
132
  Returns:
132
133
  dict: Merged asset data.
133
134
  """
134
- with open(existing_asset_file_name, "r") as existing_asset_file, open(updated_asset_file_name, "r") as updated_asset_file:
135
- existing_asset_yaml = yaml.safe_load(existing_asset_file)
136
- updated_asset_yaml = yaml.safe_load(updated_asset_file)
137
- merged_asset = {**existing_asset_yaml, **updated_asset_yaml}
138
- merged_asset["path"] = existing_asset_yaml["path"]
135
+ with open(existing_asset_file_name, "r") as existing_asset_file, open(updated_asset.file_name_with_path, "r") as updated_asset_file:
136
+ existing_asset_dict = yaml.safe_load(existing_asset_file)
137
+ updated_asset_dict = yaml.safe_load(updated_asset_file)
138
+
139
+ merged_asset = copy.deepcopy(updated_asset_dict)
140
+
141
+ if updated_asset.type == AssetType.MODEL or updated_asset.type == AssetType.DATA:
142
+ storage_key = "path"
143
+ # "path" is always required for models, use the existing value
144
+ if existing_asset_dict.get(storage_key) != updated_asset_dict.get(storage_key):
145
+ print(f'"{storage_key}" is immutable, using existing value from asset in registry: {existing_asset_dict[storage_key]}')
146
+ merged_asset[storage_key] = existing_asset_dict[storage_key]
147
+ elif updated_asset.type == AssetType.COMPONENT:
148
+ storage_key = "code"
149
+ if existing_asset_dict.get(storage_key) is None and updated_asset_dict.get(storage_key) is not None:
150
+ print(f'"{storage_key}" is immutable and cannot be added. Please create a new version to include "{storage_key}"')
151
+ elif existing_asset_dict.get(storage_key) != updated_asset_dict.get(storage_key):
152
+ print(f'"{storage_key}" is immutable, using existing value from asset in registry: {existing_asset_dict[storage_key]}')
153
+ merged_asset[storage_key] = existing_asset_dict[storage_key]
139
154
 
140
155
  return merged_asset
141
156
 
142
157
 
143
- def merge_assets(existing_asset: Union[Data, Model],
158
+ def merge_assets(existing_asset: Union[Component, Data, Model],
144
159
  asset: AssetSpec):
145
- """Overwrite values from updated to existing asset while preserving existing asset path.
160
+ """Prepare asset for update by preserving storage value from existing asset.
146
161
 
147
162
  Args:
148
- existing_asset (Union[Data, Model]): Asset currently in registry.
163
+ existing_asset (Union[Component, Data, Model]): Asset currently in registry.
149
164
  asset (AssetSpec): AssetSpec containing updated asset info.
150
165
  """
151
166
  with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as existing_asset_temp_file:
152
167
  existing_asset_temp_file_name = existing_asset_temp_file.name
153
168
 
154
169
  existing_asset.dump(existing_asset_temp_file_name)
155
- merged_result = merge_yamls(existing_asset_temp_file_name, asset.file_name_with_path)
170
+
171
+ merged_result = merge_yamls(existing_asset_temp_file_name, asset)
156
172
 
157
173
  with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as merged_asset_temp_file:
158
174
  merged_asset_temp_file_name = merged_asset_temp_file.name
@@ -164,6 +180,8 @@ def merge_assets(existing_asset: Union[Data, Model],
164
180
  merged_asset = load_model(merged_asset_temp_file_name)
165
181
  elif asset.type == AssetType.DATA:
166
182
  merged_asset = load_data(merged_asset_temp_file_name)
183
+ elif asset.type == AssetType.COMPONENT:
184
+ merged_asset = load_component(merged_asset_temp_file_name)
167
185
 
168
186
  asset.set_asset_obj_to_create_or_update(merged_asset)
169
187
 
@@ -289,7 +307,7 @@ def create_or_update_assets(input_dir: Path,
289
307
  raise
290
308
 
291
309
  if existing_asset:
292
- if asset.type == AssetType.MODEL or asset.type == AssetType.DATA:
310
+ if asset.type in [AssetType.COMPONENT, AssetType.DATA, AssetType.MODEL]:
293
311
  try:
294
312
  merge_assets(existing_asset, asset)
295
313
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: azureml-registry-tools
3
- Version: 0.1.0a3
3
+ Version: 0.1.0a5
4
4
  Summary: AzureML Registry tools and CLI
5
5
  Author: Microsoft Corp
6
6
  License: https://aka.ms/azureml-sdk-license
@@ -18,7 +18,7 @@ exclude_list = ["*.tests"]
18
18
 
19
19
  setup(
20
20
  name='azureml-registry-tools',
21
- version="0.1.0a3",
21
+ version="0.1.0a5",
22
22
  description='AzureML Registry tools and CLI',
23
23
  author='Microsoft Corp',
24
24
  license="https://aka.ms/azureml-sdk-license",