azureml-registry-tools 0.1.0a5__py3-none-any.whl → 0.1.0a7__py3-none-any.whl

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.
@@ -0,0 +1,119 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """Validate model variant schema."""
6
+
7
+ import argparse
8
+ import yaml
9
+ import sys
10
+ import jsonschema
11
+ from pathlib import Path
12
+ from typing import List
13
+
14
+ import azureml.assets as assets
15
+ import azureml.assets.util as util
16
+ from azureml.assets.util import logger
17
+
18
+
19
+ def validate_model_schema(input_dirs: List[Path],
20
+ schema_file: Path,
21
+ asset_config_filename: str) -> bool:
22
+ """Validate model variant schema.
23
+
24
+ Args:
25
+ input_dirs (List[Path]): Directories containing assets.
26
+ schema_file (Path): File containing model variant schema.
27
+ asset_config_filename (str): Asset config filename to search for.
28
+
29
+ Returns:
30
+ bool: True on success.
31
+ """
32
+ # Load model schema from file
33
+ loaded_schema = {}
34
+ with open(schema_file, 'r') as file:
35
+ loaded_schema = yaml.safe_load(file)
36
+
37
+ # Create validator instance for collecting all errors
38
+ validator = jsonschema.Draft7Validator(loaded_schema)
39
+
40
+ asset_count = 0
41
+ model_count = 0
42
+ error_count = 0
43
+ for input_dir in input_dirs:
44
+ # Recursively find all files with the name matching asset_config_filename
45
+ for asset_config in util.find_assets(input_dir, asset_config_filename):
46
+ asset_count += 1
47
+ file_path = asset_config.spec_with_path
48
+ if asset_config.type == assets.AssetType.MODEL:
49
+ model_count += 1
50
+ # Validate the file against the schema
51
+ try:
52
+ with open(file_path, "r") as f:
53
+ spec_config = yaml.safe_load(f)
54
+
55
+ # Collect all validation errors
56
+ errors = list(validator.iter_errors(spec_config))
57
+
58
+ if not errors:
59
+ logger.print(f"{file_path} is valid.")
60
+ else:
61
+ logger.log_error(f"\n‼️{file_path} has {len(errors)} validation error(s):")
62
+
63
+ for e in errors:
64
+ # Get detailed error information for each error
65
+ error_path = '.'.join(str(p) for p in e.path) if e.path else "root"
66
+ line_info = ""
67
+
68
+ # Get line number from jsonschema error if available
69
+ if hasattr(e, 'lineno') and e.lineno is not None:
70
+ line_info = f" at line {e.lineno}"
71
+ else:
72
+ # Try to find line number by looking at the path and instance
73
+ try:
74
+ with open(file_path, "r") as f:
75
+ yaml_content = f.readlines()
76
+ yaml_lines = []
77
+ for idx, line in enumerate(yaml_content):
78
+ if error_path in line:
79
+ yaml_lines.append(f"line {idx+1}: {line.strip()}")
80
+ if yaml_lines:
81
+ line_info = "\nPossible location(s):\n " + "\n ".join(yaml_lines)
82
+ except Exception:
83
+ pass
84
+
85
+ schema_path = '.'.join(str(p) for p in e.schema_path)
86
+ logger.print(f"⚠️ {file_path} is invalid at path '{error_path}'{line_info}:")
87
+ logger.print(f" Error: {e.message}")
88
+ logger.print(f" Instance: {e.instance}")
89
+ logger.print(f" Schema path: {schema_path}")
90
+ error_count += 1
91
+ except Exception as e:
92
+ logger.log_error(f"Error processing {file_path}: {str(e)}")
93
+ error_count += 1
94
+
95
+ logger.print(f"Found {asset_count} total asset(s).")
96
+ logger.print(f"Found {error_count} model(s) with error(s) out of {model_count} total model(s)")
97
+ return error_count == 0
98
+
99
+
100
+ if __name__ == "__main__":
101
+ # Handle command-line args
102
+ parser = argparse.ArgumentParser()
103
+ parser.add_argument("-i", "--input-dirs", required=True,
104
+ help="Comma-separated list of directories containing assets")
105
+ parser.add_argument("-m", "--schema-file", required=True, type=Path, help="Model Schema file")
106
+ parser.add_argument("-a", "--asset-config-filename", default=assets.DEFAULT_ASSET_FILENAME,
107
+ help="Asset config file name to search for")
108
+ args = parser.parse_args()
109
+
110
+ # Convert comma-separated values to lists
111
+ input_dirs = [Path(d) for d in args.input_dirs.split(",")]
112
+
113
+ # Validate against model schema
114
+ success = validate_model_schema(input_dirs=input_dirs,
115
+ schema_file=args.schema_file,
116
+ asset_config_filename=args.asset_config_filename)
117
+
118
+ if not success:
119
+ sys.exit(1)
@@ -0,0 +1,85 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """Validate model variant schema."""
6
+
7
+ import argparse
8
+ import yaml
9
+ import sys
10
+ import jsonschema
11
+ from jsonschema import validate
12
+ from pathlib import Path
13
+ from typing import List
14
+
15
+ import azureml.assets as assets
16
+ import azureml.assets.util as util
17
+ from azureml.assets.util import logger
18
+
19
+
20
+ def validate_model_variant_schema(input_dirs: List[Path],
21
+ model_variant_schema_file: Path,
22
+ asset_config_filename: str) -> bool:
23
+ """Validate model variant schema.
24
+
25
+ Args:
26
+ input_dirs (List[Path]): Directories containing assets.
27
+ model_variant_schema_file (Path): File containing model variant schema.
28
+ asset_config_filename (str): Asset config filename to search for.
29
+
30
+ Returns:
31
+ bool: True on success.
32
+ """
33
+ # Load variantInfo schema from file
34
+ model_variant_info_schema = {}
35
+ with open(model_variant_schema_file, 'r') as file:
36
+ model_variant_info_schema = yaml.safe_load(file)
37
+
38
+ asset_count = 0
39
+ model_count = 0
40
+ error_count = 0
41
+ for input_dir in input_dirs:
42
+ for asset_config in util.find_assets(input_dir, asset_config_filename):
43
+ asset_count += 1
44
+ if asset_config.type == assets.AssetType.MODEL:
45
+ model_count += 1
46
+ # Extract model variant info from spec
47
+ variant_info = None
48
+ with open(asset_config.spec_with_path, "r") as f:
49
+ spec_config = yaml.safe_load(f)
50
+ variant_info = spec_config.get("variantInfo")
51
+
52
+ if variant_info is not None:
53
+ logger.print(f"Found variantInfo in spec {asset_config.spec_with_path}. "
54
+ f"Validating variantInfo against schema: {variant_info}")
55
+ # Validate data
56
+ try:
57
+ validate(instance=variant_info, schema=model_variant_info_schema)
58
+ logger.print("variantInfo is valid.")
59
+ except jsonschema.exceptions.ValidationError as e:
60
+ logger.log_error(f"variantInfo is invalid for {asset_config.spec_with_path}: {e.message}")
61
+ error_count += 1
62
+
63
+ logger.print(f"Found {asset_count} total asset(s).")
64
+ logger.print(f"Found {error_count} model(s) with error(s) out of {model_count} total model(s)")
65
+ return error_count == 0
66
+
67
+
68
+ if __name__ == "__main__":
69
+ # Handle command-line args
70
+ parser = argparse.ArgumentParser()
71
+ parser.add_argument("-i", "--input-dirs", required=True, help="Comma-separated list of directories containing assets")
72
+ parser.add_argument("-m", "--model-variant-schema-file", required=True, type=Path, help="Model Variant Schema file")
73
+ parser.add_argument("-a", "--asset-config-filename", default=assets.DEFAULT_ASSET_FILENAME, help="Asset config file name to search for")
74
+ args = parser.parse_args()
75
+
76
+ # Convert comma-separated values to lists
77
+ input_dirs = [Path(d) for d in args.input_dirs.split(",")]
78
+
79
+ # Validate variantInfo against model variant schema
80
+ success = validate_model_variant_schema(input_dirs=input_dirs,
81
+ model_variant_schema_file=args.model_variant_schema_file,
82
+ asset_config_filename=args.asset_config_filename)
83
+
84
+ if not success:
85
+ sys.exit(1)
@@ -0,0 +1,260 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ """Asset management commands for registry-mgmt CLI."""
5
+
6
+ import sys
7
+ import shutil
8
+ import tempfile
9
+ import yaml
10
+ from datetime import datetime, timedelta
11
+ from pathlib import Path
12
+ from typing import List
13
+
14
+ from azure.ai.ml import MLClient
15
+ from azure.identity import DefaultAzureCredential
16
+
17
+ from azureml.registry.data.validate_model_schema import validate_model_schema
18
+ from azureml.registry.data.validate_model_variant_schema import validate_model_variant_schema
19
+ from azureml.registry.mgmt.registry_config import RegistryConfig
20
+
21
+ # Windows compatibility patch - must be applied before importing azureml.assets
22
+ from subprocess import run
23
+
24
+
25
+ def patched_run_command(cmd: List[str]):
26
+ """Run command with shell=True for Windows compatibility."""
27
+ result = run(cmd, capture_output=True, encoding=sys.stdout.encoding, errors="ignore", shell=True)
28
+ return result
29
+
30
+
31
+ # Apply patch before importing azureml.assets
32
+ import azureml.assets.publish_utils as publish_utils # noqa: E402
33
+ publish_utils.run_command = patched_run_command
34
+
35
+ import azureml.assets as assets # noqa: E402
36
+ import azureml.assets.util as util # noqa: E402
37
+ from azureml.assets.config import AssetConfig, AssetType, AzureBlobstoreAssetPath # noqa: E402
38
+ from azureml.assets.publish_utils import create_asset # noqa: E402
39
+ from azureml.assets.validate_assets import validate_assets # noqa: E402
40
+
41
+
42
+ def validate_model(asset_path: Path) -> bool:
43
+ """Validate model.
44
+
45
+ Args:
46
+ asset_path (Path): Path to the asset folder to validate
47
+
48
+ Returns:
49
+ bool: True if validation passes, False otherwise
50
+ """
51
+ errors = 0
52
+
53
+ print("⚙️ [VALIDATION #1]: Validate assets...")
54
+ if not validate_assets(asset_path, assets.DEFAULT_ASSET_FILENAME):
55
+ print("❌ [FAILED] Validation #1: validate_assets\n\n")
56
+ errors += 1
57
+ else:
58
+ print("✅ [PASSED] Validation #1: validate_assets passed\n")
59
+
60
+ # Model variant schema validation
61
+ model_variant_schema_file = Path(__file__).parent.parent / "data" / "model-variant.schema.json"
62
+
63
+ print("⚙️ [VALIDATION #2]: Validating model variant schema...")
64
+ if not validate_model_variant_schema(input_dirs=[asset_path], model_variant_schema_file=model_variant_schema_file,
65
+ asset_config_filename=assets.DEFAULT_ASSET_FILENAME):
66
+ print("❌ [FAILED] Validation #2: validate_model_variant_schema\n")
67
+ errors += 1
68
+ else:
69
+ print("✅ [PASSED] Validation #2: validate_model_variant_schema passed\n")
70
+
71
+ # Model schema validation
72
+ model_schema_file = Path(__file__).parent.parent / "data" / "model.schema.json"
73
+
74
+ print("⚙️ [VALIDATION #3]: Validating model schema...")
75
+ if not validate_model_schema(input_dirs=[asset_path], schema_file=model_schema_file,
76
+ asset_config_filename=assets.DEFAULT_ASSET_FILENAME):
77
+ print("❌ [FAILED] Validation #3: validate_model_schema\n")
78
+ errors += 1
79
+ else:
80
+ print("✅ [PASSED] Validation #3: validate_model_schema passed\n")
81
+
82
+ if errors != 0:
83
+ return False
84
+
85
+ print("🎉 [VALIDATION COMPLETE] All validations passed!\n")
86
+ return True
87
+
88
+
89
+ def set_storage_and_sas(asset: AssetConfig, storage_config: dict):
90
+ """Use storage configuration and generate/set SAS token.
91
+
92
+ Args:
93
+ asset (AssetConfig): Asset configuration object to modify
94
+ storage_config (dict): Storage configuration dictionary
95
+ """
96
+ if not storage_config:
97
+ # No storage overrides provided, skip storage configuration
98
+ return
99
+
100
+ print("Overriding storage configuration with provided values...")
101
+ extra_config = asset.extra_config_as_object()
102
+ extra_config._path = AzureBlobstoreAssetPath(
103
+ storage_name=storage_config["storage_name"],
104
+ container_name=storage_config["container_name"],
105
+ container_path=storage_config["container_path"]
106
+ )
107
+ _ = extra_config.path.get_uri(token_expiration=timedelta(hours=1))
108
+
109
+
110
+ def build_mutable_asset(base_asset: AssetConfig, mutable_asset_dir: str) -> AssetConfig:
111
+ """Build a mutable copy of the asset in a temporary directory.
112
+
113
+ Args:
114
+ base_asset (AssetConfig): Base asset configuration to copy
115
+ mutable_asset_dir (str): Directory path for the mutable asset copy
116
+
117
+ Returns:
118
+ AssetConfig: Mutable asset configuration object
119
+ """
120
+ common_dir, _ = util.find_common_directory(base_asset.release_paths)
121
+
122
+ # Convert string paths to Path objects and ensure they're absolute
123
+ common_dir = Path(common_dir).resolve()
124
+ mutable_asset_dir = Path(mutable_asset_dir).resolve()
125
+ base_asset_file = base_asset.file_name_with_path.resolve()
126
+ base_spec_file = base_asset.spec_with_path.resolve()
127
+
128
+ shutil.copytree(common_dir, mutable_asset_dir, dirs_exist_ok=True)
129
+
130
+ # Reference asset files in mutable directory
131
+ asset_config_file = mutable_asset_dir / base_asset_file.relative_to(common_dir)
132
+ spec_config_file = mutable_asset_dir / base_spec_file.relative_to(common_dir)
133
+
134
+ # Autoincrement version for mutable asset
135
+ with open(spec_config_file, "r") as f:
136
+ spec_config = yaml.safe_load(f)
137
+ spec_config["version"] = datetime.now().strftime("%Y%m%d%H%M%S")
138
+
139
+ with open(spec_config_file, "w") as f:
140
+ yaml.dump(spec_config, f)
141
+
142
+ mutable_asset = AssetConfig(asset_config_file)
143
+
144
+ return mutable_asset
145
+
146
+
147
+ def create_or_update_asset(readonly_asset: AssetConfig, config: RegistryConfig):
148
+ """Create or update an asset in the AzureML registry.
149
+
150
+ Args:
151
+ readonly_asset (AssetConfig): Asset configuration to create or update
152
+ config (RegistryConfig): Registry configuration settings
153
+ """
154
+ print("[CREATING/UPDATING ASSET]")
155
+ print(f"Using registry configuration from: {config.config_path}")
156
+ # Create ML client
157
+ ml_client = MLClient(
158
+ subscription_id=config.subscription_id,
159
+ resource_group_name=config.resource_group,
160
+ registry_name=config.registry_name,
161
+ credential=DefaultAzureCredential(),
162
+ )
163
+
164
+ with tempfile.TemporaryDirectory() as mutable_asset_dir:
165
+ mutable_asset = build_mutable_asset(base_asset=readonly_asset, mutable_asset_dir=mutable_asset_dir)
166
+ # autoincrement version
167
+ try:
168
+ set_storage_and_sas(mutable_asset, config.storage_config)
169
+ success = create_asset(mutable_asset, config.registry_name, ml_client)
170
+ except Exception as e:
171
+ print(f"Failed to create/update asset: {e}")
172
+ raise
173
+
174
+ if not success:
175
+ print(f"Failed to create/update asset: create_asset 'success' returned {success}")
176
+ raise
177
+
178
+ print("\n[VALIDATE YOUR ASSET IN THE UI HERE]")
179
+ print(f" - Model Catalog link: https://ai.azure.com/explore/models/{mutable_asset.name}/version/{mutable_asset.version}/registry/{config.registry_name}?tid={config.tenant_id}")
180
+ print(f" - Azure Portal link: https://ml.azure.com/registries/{config.registry_name}/models/{mutable_asset.name}/version/{mutable_asset.version}?tid={config.tenant_id}")
181
+
182
+
183
+ def asset_validate(asset_path: Path, dry_run: bool = False) -> bool:
184
+ """Validate an asset at the specified path.
185
+
186
+ Args:
187
+ asset_path (Path): Path to the asset folder to validate
188
+ dry_run (bool): If True, perform a dry run without side effects
189
+
190
+ Returns:
191
+ bool: True if validation passes, False otherwise
192
+ """
193
+ if dry_run:
194
+ print(f"[DRY RUN] Would validate asset at: {asset_path}")
195
+ return True
196
+
197
+ asset_path = asset_path.resolve()
198
+ print(f"[VALIDATION] Begin validating for asset at: {asset_path}...")
199
+
200
+ # Check if asset path exists
201
+ if not asset_path.exists():
202
+ print(f"❌ [ERROR]: Asset path {asset_path} does not exist")
203
+ return False
204
+
205
+ # Check for exactly one asset
206
+ asset_count = len(util.find_assets([asset_path], assets.DEFAULT_ASSET_FILENAME))
207
+ if asset_count != 1:
208
+ print(f"❌ [ERROR]: Expected exactly one asset in {asset_path}, found {asset_count}")
209
+ return False
210
+
211
+ # Load asset configuration
212
+ readonly_asset = assets.AssetConfig(asset_path / assets.DEFAULT_ASSET_FILENAME)
213
+
214
+ # Check asset type
215
+ if readonly_asset.type != AssetType.MODEL:
216
+ print(f"❌ [ERROR]: Asset type {readonly_asset.type} is not supported for validation. "
217
+ f"Only models are currently supported.")
218
+ return False
219
+
220
+ # Perform validation
221
+ return validate_model(readonly_asset.file_path)
222
+
223
+
224
+ def asset_deploy(asset_path: Path, config_path: Path, dry_run: bool = False) -> bool:
225
+ """Deploy an asset using configuration file.
226
+
227
+ Args:
228
+ asset_path (Path): Path to the asset folder to deploy
229
+ config_path (Path): Path to configuration file
230
+ dry_run (bool): If True, perform a dry run without deploying
231
+
232
+ Returns:
233
+ bool: True if deployment succeeds, False otherwise
234
+ """
235
+ try:
236
+ config = RegistryConfig(config_path)
237
+ except Exception as e:
238
+ print(f"❌ [ERROR]: Configuration validation failed: {e}")
239
+ return False
240
+
241
+ if dry_run:
242
+ print(f"[DRY RUN] Would deploy asset at {asset_path} to registry {config.registry_name}")
243
+ return True
244
+
245
+ asset_path = asset_path.resolve()
246
+
247
+ # Validate asset before deployment
248
+ if not asset_validate(asset_path, dry_run=False):
249
+ print("❌ [ERROR]: Asset validation failed. Asset deployment aborted.")
250
+ return False
251
+
252
+ # Load asset configuration
253
+ readonly_asset = assets.AssetConfig(asset_path / assets.DEFAULT_ASSET_FILENAME)
254
+
255
+ try:
256
+ create_or_update_asset(readonly_asset, config)
257
+ return True
258
+ except Exception as e:
259
+ print(f"❌ [ERROR]: Failed to deploy asset: {e}")
260
+ return False
@@ -0,0 +1,87 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ """Asset template creation commands for registry-mgmt CLI."""
5
+
6
+ from pathlib import Path
7
+ from .create_model_spec import generate_model_spec_content
8
+
9
+
10
+ def asset_template(folder_path: Path, dry_run: bool = False) -> bool:
11
+ """Create asset template files in the specified folder.
12
+
13
+ Args:
14
+ folder_path (Path): Path to the folder where template files will be created
15
+ dry_run (bool): If True, perform a dry run without creating files
16
+
17
+ Returns:
18
+ bool: True if template creation succeeds, False otherwise
19
+ """
20
+ if dry_run:
21
+ print(f"[DRY RUN] Would create asset template files in: {folder_path}")
22
+ return True
23
+
24
+ folder_path = folder_path.resolve()
25
+ print(f"Creating asset template files in {folder_path} ...")
26
+
27
+ # Check if folder path exists, create if it doesn't
28
+ if not folder_path.exists():
29
+ try:
30
+ print(f"Folder path does not exist, creating directory {folder_path} ...")
31
+ folder_path.mkdir(parents=True, exist_ok=True)
32
+ print(f"Created directory: {folder_path}")
33
+ except Exception as e:
34
+ print(f"[ERROR] Failed to create directory {folder_path}: {e}")
35
+ return False
36
+
37
+ # Get the data directory path (relative to this module)
38
+ data_dir = Path(__file__).parent.parent / "data"
39
+
40
+ # List of template files to create
41
+ template_files = ["asset.yaml", "spec.yaml", "model.yaml", "notes.md", "evaluation.md", "description.md"]
42
+
43
+ # Create each template file
44
+ try:
45
+ for output_name in template_files:
46
+ output_path = folder_path / output_name
47
+
48
+ # Special handling for spec.yaml (generate from schema)
49
+ if output_name == "spec.yaml":
50
+ schema_path = data_dir / "model.schema.json"
51
+ if not schema_path.exists():
52
+ print(f"[ERROR] Schema file not found: {schema_path}")
53
+ return False
54
+
55
+ try:
56
+ spec_content = generate_model_spec_content(schema_path)
57
+ with open(output_path, "w", encoding="utf-8") as output_file:
58
+ output_file.write(spec_content)
59
+ print(f"Created {output_path}")
60
+ except Exception as e:
61
+ print(f"[ERROR] Failed to generate spec.yaml: {e}")
62
+ return False
63
+ else:
64
+ # Handle other template files from data/ folder
65
+ template_name = f"{output_name}.template"
66
+ template_path = data_dir / template_name
67
+
68
+ # Check if template file exists
69
+ if not template_path.exists():
70
+ print(f"[ERROR] Template file not found: {template_path}")
71
+ return False
72
+
73
+ # Read template content and write to output file
74
+ with open(template_path, "r", encoding="utf-8") as template_file:
75
+ content = template_file.read()
76
+
77
+ with open(output_path, "w", encoding="utf-8") as output_file:
78
+ output_file.write(content)
79
+
80
+ print(f"Created {output_path}")
81
+
82
+ print(f"Created {len(template_files)} template files in {folder_path}")
83
+ return True
84
+
85
+ except Exception as e:
86
+ print(f"[ERROR] Failed to create template files: {e}")
87
+ return False