azureml-registry-tools 0.1.0a1__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.
@@ -0,0 +1,13 @@
1
+ This software is made available to you on the condition that you agree to
2
+ [your agreement][1] governing your use of Azure.
3
+ If you do not have an existing agreement governing your use of Azure, you agree that
4
+ your agreement governing use of Azure is the [Microsoft Online Subscription Agreement][2]
5
+ (which incorporates the [Online Services Terms][3]).
6
+ By using the software you agree to these terms. This software may collect data
7
+ that is transmitted to Microsoft. Please see the [Microsoft Privacy Statement][4]
8
+ to learn more about how Microsoft processes personal data.
9
+
10
+ [1]: https://azure.microsoft.com/en-us/support/legal/
11
+ [2]: https://azure.microsoft.com/en-us/support/legal/subscription-agreement/
12
+ [3]: http://www.microsoftvolumelicensing.com/DocumentSearch.aspx?Mode=3&DocumentTypeId=46
13
+ [4]: http://go.microsoft.com/fwlink/?LinkId=248681
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: azureml-registry-tools
3
+ Version: 0.1.0a1
4
+ Summary: AzureML Registry tools and CLI
5
+ Author: Microsoft Corp
6
+ License: https://aka.ms/azureml-sdk-license
7
+ Requires-Python: >=3.9,<3.12
8
+ License-File: LICENSE.txt
9
+ Requires-Dist: azure-ai-ml<2.0
10
+ Requires-Dist: azureml-assets<2.0
11
+ Dynamic: author
12
+ Dynamic: license
13
+ Dynamic: license-file
14
+ Dynamic: requires-dist
15
+ Dynamic: requires-python
16
+ Dynamic: summary
@@ -0,0 +1,7 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """__init.py__."""
6
+
7
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__)
@@ -0,0 +1,7 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """__init__.py."""
6
+
7
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__)
@@ -0,0 +1,4 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ """Registry CLI tooling."""
@@ -0,0 +1,97 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ import argparse
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from azureml.registry.tools.repo2registry_config import create_repo2registry_config
9
+ from azureml.registry.tools.create_or_update_assets import create_or_update_assets
10
+
11
+
12
+ def validate_json_extension(output_json_file_name) -> Path:
13
+ """Validate if output file name provided is a .json file."""
14
+ output_json_file_path = Path(output_json_file_name)
15
+ if not output_json_file_path.suffix == ".json":
16
+ raise argparse.ArgumentTypeError(f"--output-json arg {output_json_file_name} must have a .json extension")
17
+ return output_json_file_path
18
+
19
+
20
+ def validate_repo2registry_cfg(cfg_file_name) -> Path:
21
+ """Validate if output file name provided is a .cfg file."""
22
+ cfg_file_path = Path(cfg_file_name)
23
+ if not cfg_file_path.suffix == ".cfg":
24
+ raise argparse.ArgumentTypeError(f"--repo2registry-config arg {cfg_file_name} must be a path to a .cfg file")
25
+ return cfg_file_path
26
+
27
+
28
+ def validate_continue_on_asset_failure_bool_str(bool_arg) -> str:
29
+ """Validate if continue_on_asset_failure arg can be converted to a boolean."""
30
+ if bool_arg.lower() not in ("true", "false"):
31
+ raise argparse.ArgumentTypeError(f"--continue-on-asset-failure arg {bool_arg} must be a boolean (True or False)")
32
+
33
+ return bool_arg.capitalize()
34
+
35
+
36
+ def main():
37
+ """Repo2Registry CLI tool."""
38
+ # Handle command-line args
39
+ parser = argparse.ArgumentParser()
40
+ subparsers = parser.add_subparsers(dest="command")
41
+
42
+ update_parser = subparsers.add_parser("update")
43
+ update_parser.add_argument("-i", "--input-dir", type=Path,
44
+ help="Directory containing assets to create/update")
45
+ update_parser.add_argument("-c", "--repo2registry-config", type=validate_repo2registry_cfg,
46
+ help="Config containing info of registry to publish to")
47
+ update_parser.add_argument("-o", "--output-json", type=validate_json_extension, required=True,
48
+ help="Output JSON file to write create/update results")
49
+ update_parser.add_argument("-d", "--dry-run", action="store_true",
50
+ help="Dry run, don't actually create/update assets", default=False)
51
+ update_parser.add_argument("-f", "--filter", type=re.compile,
52
+ help="Regex pattern to select assets to create/update, in the format <type>/<name>/<version>")
53
+
54
+ update_parser.add_argument("--git", action="store_true",
55
+ help="Use git commits to detect file changes", default=False)
56
+ update_parser.add_argument("-b", "--base-commit",
57
+ help="Base git commit for finding file changes")
58
+ update_parser.add_argument("-t", "--target-commit",
59
+ help="Target git commit for finding file changes")
60
+
61
+ config_parser = subparsers.add_parser("config")
62
+ config_parser.add_argument("--registry-name", type=str, required=True,
63
+ help="Name of registry to create/update assets")
64
+ config_parser.add_argument("--subscription", type=str, required=True,
65
+ help="Subscription id of registry")
66
+ config_parser.add_argument("-g", "--resource_group", type=str, required=True,
67
+ help="Name of registry resource group")
68
+ config_parser.add_argument("-c", "--repo2registry-config", type=validate_repo2registry_cfg, default=Path("repo2registry.cfg"),
69
+ help="Config to write info of registry to publish to. Defaults to repo2registry.cfg")
70
+ config_parser.add_argument("--continue-on-asset-failure", default="False", type=validate_continue_on_asset_failure_bool_str,
71
+ help="Continue creating/updating remaining assets when asset creation/update fails. Defaults to False.")
72
+
73
+ args = parser.parse_args()
74
+
75
+ if args.command == "config":
76
+ create_repo2registry_config(registry_name=args.registry_name,
77
+ subscription=args.subscription,
78
+ resource_group=args.resource_group,
79
+ repo2registry_config_file_name=args.repo2registry_config,
80
+ continue_on_asset_failure=args.continue_on_asset_failure)
81
+ elif args.command == "update":
82
+ if (args.git or args.base_commit or args.target_commit) and not (args.git and args.base_commit and args.target_commit):
83
+ parser.error("When using git, --git, --base-commit, and --target-commit are all required.")
84
+ if args.dry_run:
85
+ print("Dry run option was selected. No assets will be created or updated.")
86
+
87
+ create_or_update_assets(input_dir=args.input_dir,
88
+ repo2registry_config_file_name=args.repo2registry_config,
89
+ output_json_file_name=args.output_json,
90
+ base_commit=args.base_commit,
91
+ target_commit=args.target_commit,
92
+ asset_filter=args.filter,
93
+ dry_run=args.dry_run)
94
+
95
+
96
+ if __name__ == '__main__':
97
+ main()
@@ -0,0 +1,4 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ """This module is for registry tooling."""
@@ -0,0 +1,117 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """Asset spec classes."""
6
+
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from ruamel.yaml import YAML
10
+ from typing import Union
11
+
12
+ from azure.ai.ml import load_component, load_data, load_environment, load_model
13
+ from azure.ai.ml.entities import Component, Data, Environment, Model
14
+
15
+ FULL_ASSET_NAME_TEMPLATE = "{type}/{name}/{version}"
16
+
17
+
18
+ class ValidationException(Exception):
19
+ """Validation errors."""
20
+
21
+
22
+ class AssetType(Enum):
23
+ """Asset type."""
24
+
25
+ COMPONENT = 'component'
26
+ DATA = 'data'
27
+ ENVIRONMENT = 'environment'
28
+ MODEL = 'model'
29
+
30
+
31
+ class AssetSpec:
32
+ """Asset spec."""
33
+
34
+ def __init__(self, file_name: str, asset_type: str):
35
+ """Asset spec init."""
36
+ with open(file_name) as f:
37
+ self._yaml = YAML().load(f)
38
+
39
+ self._file_name_with_path = Path(file_name)
40
+ self._file_name = Path(file_name).name
41
+ self._file_path = Path(file_name).parent
42
+
43
+ self._asset_type = asset_type
44
+ self._asset_obj = None
45
+ self._asset_obj_to_create_or_update = None
46
+ self._repo2registry_config = None
47
+
48
+ self._validate()
49
+
50
+ if self.type == AssetType.COMPONENT:
51
+ self._asset_obj = load_component(source=file_name)
52
+ elif self.type == AssetType.DATA:
53
+ self._asset_obj = load_data(source=file_name)
54
+ elif self.type == AssetType.ENVIRONMENT:
55
+ self._asset_obj = load_environment(source=file_name)
56
+ elif self.type == AssetType.MODEL:
57
+ self._asset_obj = load_model(source=file_name)
58
+
59
+ if self._asset_obj is None:
60
+ raise ValidationException(f"Asset type {self.type} is not supported")
61
+
62
+ def _validate(self):
63
+ """Validate asset spec."""
64
+ if self.version is None:
65
+ raise ValidationException("Version not found in spec. Please specify version.")
66
+
67
+ @property
68
+ def file_name(self) -> str:
69
+ """Name of config file."""
70
+ return self._file_name
71
+
72
+ @property
73
+ def file_name_with_path(self) -> Path:
74
+ """Location of config file."""
75
+ return self._file_name_with_path
76
+
77
+ @property
78
+ def file_path(self) -> Path:
79
+ """Directory containing config file."""
80
+ return self._file_path
81
+
82
+ @property
83
+ def type(self) -> AssetType:
84
+ """Asset type."""
85
+ return AssetType(self._asset_type)
86
+
87
+ @property
88
+ def name(self) -> str:
89
+ """Asset name."""
90
+ return self._yaml.get('name')
91
+
92
+ @property
93
+ def version(self) -> str:
94
+ """Asset version."""
95
+ version = self._yaml.get('version')
96
+ return str(version) if version is not None else None
97
+
98
+ @property
99
+ def full_name(self) -> str:
100
+ """Full asset name, including type and version."""
101
+ return FULL_ASSET_NAME_TEMPLATE.format(type=self.type.value, name=self.name, version=self.version)
102
+
103
+ @property
104
+ def asset_obj(self) -> Union[Component, Data, Environment, Model]:
105
+ """Asset loaded using load_*(component, data, environment, model) method."""
106
+ return self._asset_obj
107
+
108
+ @property
109
+ def asset_obj_to_create_or_update(self) -> Union[Component, Data, Environment, Model]:
110
+ """Asset to create_or_update."""
111
+ if self._asset_obj_to_create_or_update:
112
+ return self._asset_obj_to_create_or_update
113
+ return self._asset_obj
114
+
115
+ def set_asset_obj_to_create_or_update(self, asset_obj):
116
+ """Set asset to create_or_update."""
117
+ self._asset_obj_to_create_or_update = asset_obj
@@ -0,0 +1,326 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """Create or update assets."""
6
+
7
+ import azure
8
+ import json
9
+ import os
10
+ import re
11
+ import tempfile
12
+ import yaml
13
+ from git import InvalidGitRepositoryError, Repo
14
+ from pathlib import Path
15
+ from typing import List, Union
16
+
17
+ from azure.ai.ml import MLClient, load_data, load_model
18
+ from azure.ai.ml.entities._assets._artifacts.data import Data
19
+ from azure.ai.ml.entities._assets._artifacts.model import Model
20
+ from azure.identity import DefaultAzureCredential
21
+ import azureml.assets as assets
22
+
23
+ from azureml.registry.tools.config import AssetType, AssetSpec
24
+ from azureml.registry.tools.registry_utils import RegistryUtils
25
+ from azureml.registry.tools.repo2registry_config import Repo2RegistryConfig
26
+
27
+
28
+ def get_diff_files(base_commit: str,
29
+ target_commit: str) -> List[str]:
30
+ """Run git diff to compare changes from base to target commit.
31
+
32
+ Args:
33
+ base_commit (str): Commit to be compared against.
34
+ target_commit (str): Commit to be compared to the base_commit.
35
+
36
+ Returns:
37
+ List[str]: Names of files changed from base to target commit.
38
+ """
39
+ print("Base and/or target commit argument(s) provided, checking for Github repository.")
40
+ try:
41
+ repo = Repo(".", search_parent_directories=True)
42
+ except InvalidGitRepositoryError as e:
43
+ raise Exception(f"Could not find a repository in current or parent directories: {e}")
44
+
45
+ print(f"Found .git folder: {Path(repo.git_dir).as_posix()}")
46
+
47
+ repo.git.execute(["git", "fetch", "origin"])
48
+
49
+ base_commit_type = repo.git.execute(["git", "cat-file", "-t", base_commit])
50
+ target_commit_type = repo.git.execute(["git", "cat-file", "-t", target_commit])
51
+
52
+ # Validate type of both commit arguments
53
+ if base_commit_type != "commit":
54
+ raise Exception(f"Base commit argument {base_commit} is an invalid commit")
55
+ if target_commit_type != "commit":
56
+ raise Exception(f"Target commit argument {target_commit} is an invalid commit")
57
+
58
+ print("Finding changed files using git diff")
59
+ diff_files = repo.git.execute(["git", "diff", base_commit, target_commit, "--name-only"])
60
+
61
+ # Each file in diff_files is relative to parent directory of the .git dir
62
+ parent_dir = os.path.abspath(os.path.join(repo.git_dir, ".."))
63
+ diff_files = diff_files.split("\n")
64
+ diff_files = [Path(parent_dir, f).as_posix() for f in diff_files]
65
+
66
+ return diff_files
67
+
68
+
69
+ def find_assets(input_dir: Path,
70
+ base_commit: str = None,
71
+ target_commit: str = None,
72
+ asset_filter: re.Pattern = None) -> List[assets.AssetConfig]:
73
+ """Search directories for assets.
74
+
75
+ Args:
76
+ input_dir (Path): Directory to search in.
77
+ base_commit (str): Commit to be compared against for detecting file changes.
78
+ target_commit (str): Commit to be compared to the base_commit for detecting file changes.
79
+ asset_filter (re.Pattern): Regex pattern used to filter assets.
80
+ """
81
+ filename_pattern = re.compile(r'(environment|component|model|data)\.yaml$')
82
+ found_assets = []
83
+ found_changed_file = False
84
+
85
+ # Only if there's a base/target commit, check the git diff
86
+ changed_files = get_diff_files(base_commit, target_commit) if base_commit or target_commit else None
87
+ if changed_files == []:
88
+ print("No changed files found using git diff")
89
+ return []
90
+
91
+ print(f"Finding assets inside {input_dir}")
92
+
93
+ for file in input_dir.rglob("*.yaml"):
94
+ resolved_file = Path(file.resolve()).as_posix()
95
+ is_changed_file = resolved_file in changed_files if changed_files else None
96
+ filename_match = filename_pattern.match(file.name)
97
+
98
+ if filename_match:
99
+ if is_changed_file is False:
100
+ continue
101
+ found_changed_file = True
102
+ try:
103
+ asset_spec = AssetSpec(file, filename_match.group(1))
104
+ except Exception as e:
105
+ raise Exception(f"Failed to create AssetSpec object for {resolved_file}: {e}")
106
+
107
+ if asset_filter is not None and not asset_filter.fullmatch(asset_spec.full_name):
108
+ continue
109
+
110
+ found_assets.append(asset_spec)
111
+
112
+ if asset_filter and len(found_assets) == 0 and found_changed_file:
113
+ print(f"No assets found with the asset filter {asset_filter}. Please verify your filter regex can match assets with the format "
114
+ f"<type>/<name>/<version>, where asset type is component, data, environment, model.")
115
+ elif len(found_assets) == 0:
116
+ print("No assets were found.")
117
+ else:
118
+ print(f"Found assets: {[asset.full_name for asset in found_assets]}")
119
+
120
+ return found_assets
121
+
122
+
123
+ 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
+
127
+ Args:
128
+ 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
+ Returns:
132
+ dict: Merged asset data.
133
+ """
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"]
139
+
140
+ return merged_asset
141
+
142
+
143
+ def merge_assets(existing_asset: Union[Data, Model],
144
+ asset: AssetSpec):
145
+ """Overwrite values from updated to existing asset while preserving existing asset path.
146
+
147
+ Args:
148
+ existing_asset (Union[Data, Model]): Asset currently in registry.
149
+ asset (AssetSpec): AssetSpec containing updated asset info.
150
+ """
151
+ with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as existing_asset_temp_file:
152
+ existing_asset_temp_file_name = existing_asset_temp_file.name
153
+
154
+ existing_asset.dump(existing_asset_temp_file_name)
155
+ merged_result = merge_yamls(existing_asset_temp_file_name, asset.file_name_with_path)
156
+
157
+ with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as merged_asset_temp_file:
158
+ merged_asset_temp_file_name = merged_asset_temp_file.name
159
+
160
+ with open(merged_asset_temp_file_name, "w") as merged_asset_temp_file:
161
+ merged_asset_temp_file.write(yaml.dump(merged_result))
162
+
163
+ if asset.type == AssetType.MODEL:
164
+ merged_asset = load_model(merged_asset_temp_file_name)
165
+ elif asset.type == AssetType.DATA:
166
+ merged_asset = load_data(merged_asset_temp_file_name)
167
+
168
+ asset.set_asset_obj_to_create_or_update(merged_asset)
169
+
170
+ os.remove(existing_asset_temp_file_name)
171
+ os.remove(merged_asset_temp_file_name)
172
+
173
+
174
+ def write_results(results_dict: dict,
175
+ output_json_file_name: str,
176
+ found_assets: List[AssetSpec],
177
+ dry_run: bool):
178
+ """Write asset create/update results to JSON output file.
179
+
180
+ Args:
181
+ results_dict (dict): Results from asset create_or_update operations.
182
+ output_json_file_name (str): File name of JSON to write results to.
183
+ found_assets (List[AssetSpec]): List of assets found in input directories.
184
+ dry_run (bool): Dry run flag.
185
+ """
186
+ for asset in found_assets:
187
+ if asset.full_name not in results_dict:
188
+ if dry_run:
189
+ results_dict[asset.full_name] = {"DryRunCreateOrUpdateStatus": "NOT RUN", "Operation": "None", "Logs": ""}
190
+ else:
191
+ results_dict[asset.full_name] = {"CreateOrUpdateStatus": "NOT RUN", "Operation": "None", "Logs": ""}
192
+
193
+ # Produce JSON output on status and diffs
194
+ print("\nAsset create_or_update results:")
195
+ print(json.dumps(results_dict, indent=2))
196
+ with open(output_json_file_name, "w") as f:
197
+ json.dump(results_dict, f, indent=2)
198
+ print(f"Wrote asset create_or_update results to file {output_json_file_name}")
199
+
200
+
201
+ def resolve_from_parent_folder(input_folder: Path) -> Path:
202
+ """Resolve repo2registry.cfg using parent folder.
203
+
204
+ Args:
205
+ input_folder (Path): Folder to check for repo2registry.cfg.
206
+
207
+ Returns:
208
+ Path: Folder containing repo2registry.cfg if found, otherwise None.
209
+ """
210
+ if os.path.exists(os.path.join(input_folder, "repo2registry.cfg")):
211
+ return input_folder
212
+ if os.path.abspath(input_folder) == os.path.abspath(os.sep):
213
+ return None
214
+ return resolve_from_parent_folder(os.path.dirname(input_folder))
215
+
216
+
217
+ def create_or_update_assets(input_dir: Path,
218
+ repo2registry_config_file_name: Path,
219
+ output_json_file_name: Path,
220
+ base_commit: str = None,
221
+ target_commit: str = None,
222
+ asset_filter: re.Pattern = None,
223
+ dry_run: bool = False):
224
+ """Create or update assets.
225
+
226
+ Args:
227
+ input_dir (Path): Directory to search in.
228
+ repo2registry_config (Path): Config file containing registry info.
229
+ base_commit (str): Commit to be compared against for detecting file changes.
230
+ target_commit (str): Commit to be compared to the base_commit for detecting file changes.
231
+ output_json_file_name (Path): File name of JSON to write results to.
232
+ asset_filter (re.Pattern): Regex pattern used to filter assets.
233
+ dry_run (bool): Dry run flag.
234
+ """
235
+ if input_dir is None:
236
+ print(f"Input directory not specified, using current working directory: {os.getcwd()}")
237
+ input_dir = Path(os.getcwd())
238
+
239
+ if repo2registry_config_file_name is not None:
240
+ print(f"Using repo2registry.cfg file specified at path: {Path(repo2registry_config_file_name).resolve()} for all assets")
241
+ repo2registry_config = Repo2RegistryConfig(repo2registry_config_file_name)
242
+ else:
243
+ # If path to repo2registry.cfg file is unspecified, resolve cfg file starting at input_dir
244
+ print("No config file specified as an argument, resolving repo2registry.cfg file")
245
+
246
+ input_dir_abs_path = input_dir.resolve().as_posix()
247
+ repo2registry_config_folder = resolve_from_parent_folder(input_dir_abs_path)
248
+ if repo2registry_config_folder is None:
249
+ raise Exception(f"Could not find repo2registry.cfg file in any parent folders of {input_dir_abs_path}")
250
+
251
+ repo2registry_file_path = os.path.join(repo2registry_config_folder, 'repo2registry.cfg')
252
+ repo2registry_abs_file_path = Path(repo2registry_file_path).resolve().as_posix()
253
+ print(f"Found repo2registry.cfg file in {repo2registry_abs_file_path}")
254
+ repo2registry_config = Repo2RegistryConfig(repo2registry_file_path)
255
+
256
+ ml_client = MLClient(
257
+ subscription_id=repo2registry_config.subscription_id,
258
+ resource_group_name=repo2registry_config.resource_group,
259
+ registry_name=repo2registry_config.registry_name,
260
+ credential=DefaultAzureCredential(),
261
+ )
262
+
263
+ results_dict = {}
264
+ found_assets = find_assets(input_dir, base_commit, target_commit, asset_filter)
265
+
266
+ try:
267
+ for asset in found_assets:
268
+ print(f"\nPreparing {asset.full_name} for create/update")
269
+
270
+ # Check if asset/version has already been created
271
+ print(f"Attempting to find {asset.full_name} in {repo2registry_config.registry_name}")
272
+ existing_asset = None
273
+ try:
274
+ operations = RegistryUtils.get_operations_from_type(asset.type, ml_client=ml_client)
275
+ existing_asset = operations.get(name=asset.name, version=asset.version)
276
+ operation_str = "Update"
277
+ print(f"{asset.full_name} already exists. Preparing to {operation_str.upper()} asset.")
278
+ except azure.core.exceptions.ResourceNotFoundError:
279
+ operation_str = "Create"
280
+ print(f"{asset.full_name} does not exist. Preparing to {operation_str.upper()} asset.")
281
+ except Exception as e:
282
+ if dry_run:
283
+ results_dict[asset.full_name] = {"DryRunCreateOrUpdateStatus": "NOT RUN", "Operation": "None", "Logs": f"{e}"}
284
+ else:
285
+ results_dict[asset.full_name] = {"CreateOrUpdateStatus": "NOT RUN", "Operation": "None", "Logs": f"{e}"}
286
+ if repo2registry_config.continue_on_asset_failure:
287
+ continue
288
+ else:
289
+ raise
290
+
291
+ if existing_asset:
292
+ if asset.type == AssetType.MODEL or asset.type == AssetType.DATA:
293
+ try:
294
+ merge_assets(existing_asset, asset)
295
+ except Exception as e:
296
+ print(f"Failed to prepare {asset.full_name} for create_or_update: {e}")
297
+ if dry_run:
298
+ results_dict[asset.full_name] = {"DryRunCreateOrUpdateStatus": "NOT RUN", "Operation": f"{operation_str}", "Logs": f"{e}"}
299
+ else:
300
+ results_dict[asset.full_name] = {"CreateOrUpdateStatus": "NOT RUN", "Operation": f"{operation_str}", "Logs": f"{e}"}
301
+ if repo2registry_config.continue_on_asset_failure:
302
+ continue
303
+ else:
304
+ raise
305
+
306
+ try:
307
+ print(f"[{operation_str.upper()}] Running create_or_update on {asset.full_name}")
308
+ if not dry_run:
309
+ operations.create_or_update(asset.asset_obj_to_create_or_update)
310
+ print(f"Completed create_or_update for {asset.full_name}")
311
+ results_dict[asset.full_name] = {"CreateOrUpdateStatus": "SUCCESS", "Operation": f"{operation_str}", "Logs": ""}
312
+ else:
313
+ results_dict[asset.full_name] = {"DryRunCreateOrUpdateStatus": "SUCCESS", "Operation": f"{operation_str}", "Logs": ""}
314
+ except Exception as e:
315
+ print(f"Failed create_or_update for {asset.full_name}: {e}")
316
+ results_dict[asset.full_name] = {"CreateOrUpdateStatus": "FAILED", "Operation": f"{operation_str}", "Logs": f"{e}"}
317
+ if repo2registry_config.continue_on_asset_failure:
318
+ continue
319
+ else:
320
+ raise
321
+
322
+ except Exception:
323
+ write_results(results_dict, output_json_file_name, found_assets, dry_run)
324
+ raise Exception("Errors occurred while creating/updating assets")
325
+
326
+ write_results(results_dict, output_json_file_name, found_assets, dry_run)
@@ -0,0 +1,36 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """RegistryUtils class."""
6
+
7
+ from typing import Union
8
+ from azure.ai.ml import MLClient, operations as ops
9
+ from azureml.registry.tools.config import AssetType
10
+
11
+
12
+ class RegistryUtils:
13
+ """Registry utils."""
14
+
15
+ def get_operations_from_type(asset_type: AssetType, ml_client: MLClient) -> Union[
16
+ ops.ComponentOperations, ops.DataOperations, ops.EnvironmentOperations,
17
+ ops.ModelOperations]:
18
+ """Get MLCLient operations related to an asset type.
19
+
20
+ Args:
21
+ asset_type (AssetType): Asset type.
22
+ ml_client (MLClient): ML client.
23
+ Returns:
24
+ Union[ops.ComponentOperations, ops.DataOperations, ops.EnvironmentOperations,
25
+ ops.ModelOperations]: Operations object.
26
+ """
27
+ if asset_type == AssetType.COMPONENT:
28
+ return ml_client.components
29
+ elif asset_type == AssetType.DATA:
30
+ return ml_client.data
31
+ elif asset_type == AssetType.ENVIRONMENT:
32
+ return ml_client.environments
33
+ elif asset_type == AssetType.MODEL:
34
+ return ml_client.models
35
+ else:
36
+ raise Exception(f"Asset type {asset_type} is not supported.")
@@ -0,0 +1,108 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """Repo2RegistryConfig class."""
6
+
7
+ import configparser
8
+ from pathlib import Path
9
+
10
+
11
+ class Repo2RegistryConfig:
12
+ """Repo2RegistryConfig class."""
13
+
14
+ def __init__(self, repo2registry_config_file_name: str):
15
+ """Repo2RegistryConfig init.
16
+
17
+ Args:
18
+ repo2registry_config_file (Path): Path of repo2registry config file.
19
+ """
20
+ if not Path(repo2registry_config_file_name).is_file():
21
+ raise FileNotFoundError(f"File '{repo2registry_config_file_name.resolve().as_posix()}' does not exist.")
22
+
23
+ config = configparser.ConfigParser()
24
+ config.read(repo2registry_config_file_name)
25
+
26
+ self._validate_schema(config, repo2registry_config_file_name)
27
+
28
+ self._subscription_id = config["registry"]["subscription_id"]
29
+ self._resource_group = config["registry"]["resource_group"]
30
+ self._registry_name = config["registry"]["registry_name"]
31
+ self._continue_on_asset_failure = self._get_bool_value(config["settings"]["continue_on_asset_failure"])
32
+
33
+ def _get_bool_value(self, value) -> bool:
34
+ return value.lower() == "true"
35
+
36
+ def _value_is_bool(self, value) -> bool:
37
+ return value.lower() in ("true", "false")
38
+
39
+ def _validate_schema(self, config, repo2registry_config_file_name):
40
+ """Validate repo2registry config schema."""
41
+ if not config.has_section("registry"):
42
+ raise Exception(f'"registry" section not found in config file {repo2registry_config_file_name}')
43
+ if not config.has_section("settings"):
44
+ raise Exception(f'"settings" section not found in config file {repo2registry_config_file_name}')
45
+
46
+ if not config.has_option("registry", "subscription_id"):
47
+ raise Exception(f'Key "subscription_id" not found under "registry" section in config file {repo2registry_config_file_name}')
48
+ if not config.has_option("registry", "resource_group"):
49
+ raise Exception(f'Key "resource_group" not found under "registry" section in config file {repo2registry_config_file_name}')
50
+ if not config.has_option("registry", "registry_name"):
51
+ raise Exception(f'Key "registry_name" not found under "registry" section in config file {repo2registry_config_file_name}')
52
+
53
+ if not config.has_option("settings", "continue_on_asset_failure"):
54
+ raise Exception(f'Key "continue_on_asset_failure" not found under "settings" section in config file {repo2registry_config_file_name}')
55
+ if not self._value_is_bool(config["settings"]["continue_on_asset_failure"]):
56
+ raise Exception(f'Key "continue_on_asset_failure" under "settings" section must be a boolean in config file {repo2registry_config_file_name}')
57
+
58
+ @property
59
+ def subscription_id(self) -> str:
60
+ """Subscription id."""
61
+ return self._subscription_id
62
+
63
+ @property
64
+ def resource_group(self) -> str:
65
+ """Resource group."""
66
+ return self._resource_group
67
+
68
+ @property
69
+ def registry_name(self) -> str:
70
+ """Registry name."""
71
+ return self._registry_name
72
+
73
+ @property
74
+ def continue_on_asset_failure(self) -> bool:
75
+ """Continue on asset failure."""
76
+ return self._continue_on_asset_failure
77
+
78
+
79
+ def create_repo2registry_config(registry_name: str, subscription: str, resource_group: str, repo2registry_config_file_name: Path,
80
+ continue_on_asset_failure: str):
81
+ """Create repo2registry config.
82
+
83
+ Args:
84
+ registry_name (str): Registry name.
85
+ subscription (str): Registry subscription id.
86
+ resource_group (str): Registry resource group.
87
+ repo2registry_config_file_name (Path): Path to config file.
88
+ continue_on_asset_failure (str): Whether to continue creating/updating remaining assets when asset creation/update fails.
89
+ """
90
+ print("Creating repo2registry config...")
91
+
92
+ repo2registry_config = configparser.ConfigParser()
93
+ repo2registry_config.add_section("registry")
94
+ repo2registry_config.set("registry", "registry_name", registry_name)
95
+ repo2registry_config.set("registry", "subscription_id", subscription)
96
+ repo2registry_config.set("registry", "resource_group", resource_group)
97
+
98
+ # Set default settings
99
+ repo2registry_config.add_section("settings")
100
+ repo2registry_config.set("settings", "continue_on_asset_failure", continue_on_asset_failure)
101
+
102
+ # Write to path
103
+ with open(repo2registry_config_file_name, "w") as repo2registry_config_file:
104
+ repo2registry_config.write(repo2registry_config_file)
105
+
106
+ repo2registry_cfg_abs_path = Path(repo2registry_config_file_name).resolve().as_posix()
107
+
108
+ print(f"Wrote repo2registry config file to {repo2registry_cfg_abs_path}")
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: azureml-registry-tools
3
+ Version: 0.1.0a1
4
+ Summary: AzureML Registry tools and CLI
5
+ Author: Microsoft Corp
6
+ License: https://aka.ms/azureml-sdk-license
7
+ Requires-Python: >=3.9,<3.12
8
+ License-File: LICENSE.txt
9
+ Requires-Dist: azure-ai-ml<2.0
10
+ Requires-Dist: azureml-assets<2.0
11
+ Dynamic: author
12
+ Dynamic: license
13
+ Dynamic: license-file
14
+ Dynamic: requires-dist
15
+ Dynamic: requires-python
16
+ Dynamic: summary
@@ -0,0 +1,18 @@
1
+ LICENSE.txt
2
+ setup.cfg
3
+ setup.py
4
+ azureml/__init__.py
5
+ azureml/registry/__init__.py
6
+ azureml/registry/_cli/__init__.py
7
+ azureml/registry/_cli/repo2registry_cli.py
8
+ azureml/registry/tools/__init__.py
9
+ azureml/registry/tools/config.py
10
+ azureml/registry/tools/create_or_update_assets.py
11
+ azureml/registry/tools/registry_utils.py
12
+ azureml/registry/tools/repo2registry_config.py
13
+ azureml_registry_tools.egg-info/PKG-INFO
14
+ azureml_registry_tools.egg-info/SOURCES.txt
15
+ azureml_registry_tools.egg-info/dependency_links.txt
16
+ azureml_registry_tools.egg-info/entry_points.txt
17
+ azureml_registry_tools.egg-info/requires.txt
18
+ azureml_registry_tools.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ repo2registry = azureml.registry._cli.repo2registry_cli:main
@@ -0,0 +1,2 @@
1
+ azure-ai-ml<2.0
2
+ azureml-assets<2.0
@@ -0,0 +1,7 @@
1
+ [metadata]
2
+ license_file = LICENSE.txt
3
+
4
+ [egg_info]
5
+ tag_build =
6
+ tag_date = 0
7
+
@@ -0,0 +1,30 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ """Set up package."""
6
+
7
+ from setuptools import setup, find_packages
8
+
9
+ DEPENDENCIES = [
10
+ "azure-ai-ml<2.0",
11
+ "azureml-assets<2.0"
12
+ ]
13
+
14
+ exclude_list = ["*.tests"]
15
+
16
+ setup(
17
+ name='azureml-registry-tools',
18
+ version="0.1.0a1",
19
+ description='AzureML Registry tools and CLI',
20
+ author='Microsoft Corp',
21
+ license="https://aka.ms/azureml-sdk-license",
22
+ packages=find_packages(exclude=exclude_list),
23
+ install_requires=DEPENDENCIES,
24
+ python_requires=">=3.9,<3.12",
25
+ entry_points={
26
+ 'console_scripts': [
27
+ 'repo2registry = azureml.registry._cli.repo2registry_cli:main'
28
+ ],
29
+ }
30
+ )