azureml-registry-tools 0.1.0a1__tar.gz → 0.1.0a2__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 (28) hide show
  1. {azureml_registry_tools-0.1.0a1/azureml_registry_tools.egg-info → azureml_registry_tools-0.1.0a2}/PKG-INFO +4 -2
  2. azureml_registry_tools-0.1.0a2/azureml/registry/_cli/registry_syndication_cli.py +230 -0
  3. azureml_registry_tools-0.1.0a2/azureml/registry/_rest_client/__init__.py +4 -0
  4. azureml_registry_tools-0.1.0a2/azureml/registry/_rest_client/arm_client.py +133 -0
  5. azureml_registry_tools-0.1.0a2/azureml/registry/_rest_client/base_rest_client.py +138 -0
  6. azureml_registry_tools-0.1.0a2/azureml/registry/_rest_client/registry_management_client.py +114 -0
  7. azureml_registry_tools-0.1.0a2/azureml/registry/mgmt/__init__.py +4 -0
  8. azureml_registry_tools-0.1.0a2/azureml/registry/mgmt/create_manifest.py +235 -0
  9. azureml_registry_tools-0.1.0a2/azureml/registry/mgmt/syndication_manifest.py +150 -0
  10. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2/azureml_registry_tools.egg-info}/PKG-INFO +4 -2
  11. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml_registry_tools.egg-info/SOURCES.txt +8 -0
  12. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml_registry_tools.egg-info/entry_points.txt +1 -0
  13. azureml_registry_tools-0.1.0a2/azureml_registry_tools.egg-info/requires.txt +4 -0
  14. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/setup.py +6 -3
  15. azureml_registry_tools-0.1.0a1/azureml_registry_tools.egg-info/requires.txt +0 -2
  16. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/LICENSE.txt +0 -0
  17. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/__init__.py +0 -0
  18. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/registry/__init__.py +0 -0
  19. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/registry/_cli/__init__.py +0 -0
  20. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/registry/_cli/repo2registry_cli.py +0 -0
  21. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/registry/tools/__init__.py +0 -0
  22. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/registry/tools/config.py +0 -0
  23. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/registry/tools/create_or_update_assets.py +0 -0
  24. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/registry/tools/registry_utils.py +0 -0
  25. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml/registry/tools/repo2registry_config.py +0 -0
  26. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml_registry_tools.egg-info/dependency_links.txt +0 -0
  27. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/azureml_registry_tools.egg-info/top_level.txt +0 -0
  28. {azureml_registry_tools-0.1.0a1 → azureml_registry_tools-0.1.0a2}/setup.cfg +0 -0
@@ -1,11 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: azureml-registry-tools
3
- Version: 0.1.0a1
3
+ Version: 0.1.0a2
4
4
  Summary: AzureML Registry tools and CLI
5
5
  Author: Microsoft Corp
6
6
  License: https://aka.ms/azureml-sdk-license
7
- Requires-Python: >=3.9,<3.12
7
+ Requires-Python: >=3.9,<3.14
8
8
  License-File: LICENSE.txt
9
+ Requires-Dist: azure-identity<2.0
10
+ Requires-Dist: ruamel-yaml<0.19,>=0.17.21
9
11
  Requires-Dist: azure-ai-ml<2.0
10
12
  Requires-Dist: azureml-assets<2.0
11
13
  Dynamic: author
@@ -0,0 +1,230 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ import argparse
5
+ import json
6
+ from azureml.registry._rest_client.registry_management_client import RegistryManagementClient
7
+ from azureml.registry._rest_client.arm_client import ArmClient
8
+ from azureml.registry.mgmt.create_manifest import generate_syndication_manifest
9
+ from azureml.registry.mgmt.syndication_manifest import SyndicationManifest
10
+
11
+
12
+ def syndication_manifest_show(registry_name: str) -> dict:
13
+ """Show the current syndication manifest for the specified registry and region.
14
+
15
+ Args:
16
+ registry_name (str): Name of the AzureML registry.
17
+
18
+ Returns:
19
+ dict: The manifest data.
20
+ """
21
+ return json.dumps(RegistryManagementClient(registry_name=registry_name).get_manifest())
22
+
23
+
24
+ def syndication_manifest_set(manifest_value: str, folder: str, dry_run: bool) -> None:
25
+ """Set the syndication manifest for the specified registry and region.
26
+
27
+ Args:
28
+ manifest_value (str): Manifest value as a string (JSON or similar).
29
+ folder (str): Path to the manifest folder.
30
+ dry_run (bool): If True, do not perform any changes.
31
+ """
32
+ if manifest_value is not None:
33
+ # for inline manifest value allow different casing of keys
34
+ manifest = SyndicationManifest.from_dto(json.loads(manifest_value), normalize_keys=True)
35
+ else:
36
+ # Folder structure should have proper casing
37
+ manifest = generate_syndication_manifest(folder)
38
+ dto = manifest.to_dto()
39
+ if dry_run:
40
+ print(f"Dry run: Would set manifest to {dto}")
41
+ else:
42
+ client = RegistryManagementClient(registry_name=manifest.registry_name)
43
+ client.create_or_update_manifest(dto)
44
+
45
+
46
+ def syndication_manifest_delete(registry_name: str, dry_run: bool) -> None:
47
+ """Delete the syndication manifest for the specified registry.
48
+
49
+ Args:
50
+ registry_name (str): Name of the AzureML registry.
51
+ dry_run (bool): If True, do not perform any changes.
52
+ """
53
+ if dry_run:
54
+ print(f"Dry run: Would delete manifest for registry {registry_name}")
55
+ else:
56
+ RegistryManagementClient(registry_name=registry_name).delete_manifest()
57
+
58
+
59
+ def syndication_target_show(registry_name: str) -> object:
60
+ """Show the current syndication target(s) for the specified registry and region.
61
+
62
+ Args:
63
+ registry_name (str): Name of the AzureML registry.
64
+
65
+ Returns:
66
+ list or str: List of syndicated registries or 'None'.
67
+ """
68
+ discovery = RegistryManagementClient(registry_name=registry_name).discovery()
69
+ arm_resource_id = f"/subscriptions/{discovery.get('subscriptionId')}/resourceGroups/{discovery.get('resourceGroup')}/providers/Microsoft.MachineLearningServices/registries/{discovery.get('registryName')}"
70
+ return ArmClient().get_resource(resource_id=arm_resource_id).get("properties", {}).get("syndicatedRegistries", "None")
71
+
72
+
73
+ def syndication_target_set(registry_name: str, registry_ids: list, dry_run: bool) -> None:
74
+ """Set the syndication target(s) for the specified registry and region.
75
+
76
+ Args:
77
+ registry_name (str): Name of the AzureML registry.
78
+ registry_ids (list): List of registry IDs to set as syndicated targets.
79
+ dry_run (bool): If True, do not perform any changes.
80
+ """
81
+ discovery = RegistryManagementClient(registry_name=registry_name).discovery()
82
+ arm_resource_id = f"/subscriptions/{discovery.get('subscriptionId')}/resourceGroups/{discovery.get('resourceGroup')}/providers/Microsoft.MachineLearningServices/registries/{discovery.get('registryName')}"
83
+ arm_client = ArmClient()
84
+ resource = arm_client.get_resource(resource_id=arm_resource_id)
85
+ resource["properties"]["syndicatedRegistries"] = registry_ids
86
+ if dry_run:
87
+ print(f"Dry run: Would set {registry_ids} as SyndicatedRegistries for {registry_name}")
88
+ else:
89
+ arm_client.put_resource(resource_id=arm_resource_id, put_body=resource)
90
+
91
+
92
+ def show_command(registry_name: str, as_arm_object: bool) -> object:
93
+ """Show registry discovery info or ARM object for the specified registry and region.
94
+
95
+ Args:
96
+ registry_name (str): Name of the AzureML registry.
97
+ as_arm_object (bool): If True, show as ARM object.
98
+
99
+ Returns:
100
+ dict: Discovery info or ARM resource object.
101
+ """
102
+ discovery = RegistryManagementClient(registry_name=registry_name).discovery()
103
+ if as_arm_object:
104
+ arm_resource_id = f"/subscriptions/{discovery.get('subscriptionId')}/resourceGroups/{discovery.get('resourceGroup')}/providers/Microsoft.MachineLearningServices/registries/{discovery.get('registryName')}"
105
+ return ArmClient().get_resource(resource_id=arm_resource_id)
106
+ return discovery
107
+
108
+
109
+ def _add_common_args(p, arg_dicts=None):
110
+ if arg_dicts is None:
111
+ arg_dicts = []
112
+ for arg in arg_dicts:
113
+ p.add_argument(*arg["args"], **arg["kwargs"])
114
+
115
+
116
+ def main() -> None:
117
+ """Azureml Registry Syndication CLI Extension.
118
+
119
+ Examples:
120
+ # Show the current manifest
121
+ registry-mgmt syndication manifest show --registry-name myreg
122
+
123
+ # Set manifest from a folder
124
+ registry-mgmt syndication manifest set --path ./manifest_folder
125
+
126
+ # Set manifest from a value
127
+ registry-mgmt syndication manifest set --value '{"Manifest": "val"}'
128
+
129
+ # Delete the current manifest
130
+ registry-mgmt syndication manifest delete --registry-name myreg
131
+
132
+ # Show the current target
133
+ registry-mgmt syndication target show --registry-name myreg
134
+
135
+ # Set target values
136
+ registry-mgmt syndication target set --registry-name myreg -v reg1Id -v reg2Id
137
+
138
+ # Show registry discovery info
139
+ registry-mgmt show --registry-name myreg
140
+
141
+ # Show registry as ARM object
142
+ registry-mgmt show --registry-name myreg --as-arm-object
143
+
144
+ # Dry run for any command
145
+ registry-mgmt syndication manifest set --registry-name myreg --path ./manifest_folder --dry-run
146
+ """
147
+ parser = argparse.ArgumentParser(prog="registry-mgmt", description="AzureML Registry Syndication CLI Extension")
148
+ subparsers = parser.add_subparsers(dest="command", required=True)
149
+
150
+ registry_name_arg = {
151
+ "args": ("-r", "--registry-name"),
152
+ "kwargs": {
153
+ "type": str,
154
+ "required": True,
155
+ "help": "Name of the AzureML registry."
156
+ }
157
+ }
158
+
159
+ dry_run_arg = {
160
+ "args": ("--dry-run",),
161
+ "kwargs": {
162
+ "action": "store_true",
163
+ "help": "Perform a dry run without making changes."
164
+ }
165
+ }
166
+
167
+ # syndication root command
168
+ synd_parser = subparsers.add_parser("syndication", help="Syndication operations")
169
+ synd_subparsers = synd_parser.add_subparsers(dest="synd_subcommand", required=True)
170
+
171
+ # syndication manifest
172
+ manifest_parser = synd_subparsers.add_parser("manifest", help="Manage syndication manifest.")
173
+ manifest_subparsers = manifest_parser.add_subparsers(dest="manifest_subcommand", required=True)
174
+
175
+ manifest_show_parser = manifest_subparsers.add_parser("show", help="Show the current manifest.")
176
+ _add_common_args(manifest_show_parser, [registry_name_arg, dry_run_arg])
177
+
178
+ manifest_set_parser = manifest_subparsers.add_parser("set", help="Set manifest values.")
179
+ _add_common_args(manifest_set_parser, [dry_run_arg])
180
+ group = manifest_set_parser.add_mutually_exclusive_group(required=True)
181
+ group.add_argument("-v", "--value", type=str, help="Manifest value.")
182
+ group.add_argument("-p", "--path", type=str, help="Path to manifest root folder.")
183
+
184
+ manifest_delete_parser = manifest_subparsers.add_parser("delete", help="Delete the current manifest.")
185
+ _add_common_args(manifest_delete_parser, [registry_name_arg, dry_run_arg])
186
+
187
+ # syndication target
188
+ target_parser = synd_subparsers.add_parser("target", help="Manage syndication target.")
189
+ target_subparsers = target_parser.add_subparsers(dest="target_subcommand", required=True)
190
+
191
+ target_show_parser = target_subparsers.add_parser("show", help="Show the current target.")
192
+ _add_common_args(target_show_parser, [registry_name_arg, dry_run_arg])
193
+
194
+ target_set_parser = target_subparsers.add_parser("set", help="Set target values.")
195
+ _add_common_args(target_set_parser, [registry_name_arg, dry_run_arg])
196
+ target_set_parser.add_argument("-v", "--value", type=str, action="append", required=True, help="Target value (can be specified multiple times).")
197
+
198
+ # show root command
199
+ show_parser = subparsers.add_parser("show", help="Show syndication info.")
200
+ _add_common_args(show_parser, [registry_name_arg, dry_run_arg])
201
+ show_parser.add_argument("--as-arm-object", action="store_true", help="Show as ARM object.")
202
+
203
+ args = parser.parse_args()
204
+
205
+ # Command dispatch
206
+ if args.command == "syndication":
207
+ if args.synd_subcommand == "manifest":
208
+ if args.manifest_subcommand == "show":
209
+ print(syndication_manifest_show(args.registry_name))
210
+ elif args.manifest_subcommand == "set":
211
+ print(syndication_manifest_set(args.value, args.path, args.dry_run))
212
+ elif args.manifest_subcommand == "delete":
213
+ confirm = input(f"Proceed with manifest deletion for {args.registry_name}? [y/N]: ")
214
+ if confirm.lower() == "y":
215
+ syndication_manifest_delete(args.registry_name, args.dry_run)
216
+ else:
217
+ print("Manifest deletion cancelled.")
218
+ elif args.synd_subcommand == "target":
219
+ if args.target_subcommand == "show":
220
+ print(syndication_target_show(args.registry_name))
221
+ elif args.target_subcommand == "set":
222
+ syndication_target_set(args.registry_name, args.value, args.dry_run)
223
+ elif args.command == "show":
224
+ print(show_command(args.registry_name, args.as_arm_object))
225
+ else:
226
+ parser.print_help()
227
+
228
+
229
+ if __name__ == "__main__":
230
+ main()
@@ -0,0 +1,4 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ """This module is for rest clients."""
@@ -0,0 +1,133 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ from azure.identity import DefaultAzureCredential
5
+ from .base_rest_client import BaseRestClient
6
+ import time
7
+ from json.decoder import JSONDecodeError
8
+
9
+ DEFAULT_API_VERSION = "2025-04-01" # Default API version for Azure Resource Manager
10
+
11
+
12
+ class ArmClient(BaseRestClient):
13
+ """Simple Azure Resource Manager (ARM) client leveraging BaseRestClient for GET, PATCH, PUT, and DELETE operations.
14
+
15
+ Handles authentication via Bearer token (pass as api_key) and supports standard ARM resource operations.
16
+ """
17
+
18
+ def __init__(self, api_key=None, max_retries=5, backoff_factor=1):
19
+ """
20
+ Initialize the ArmClient.
21
+
22
+ Args:
23
+ api_key (str, optional): API key or bearer token. If None, uses DefaultAzureCredential.
24
+ max_retries (int): Maximum number of retries for requests.
25
+ backoff_factor (int): Backoff factor for retries.
26
+ """
27
+ base_url = "https://management.azure.com"
28
+ self._credential = None
29
+ self._token_expires_on = None
30
+ if api_key is None:
31
+ # Use DefaultAzureCredential for authentication if no API key is provided
32
+ self._credential = DefaultAzureCredential()
33
+ token = self._credential.get_token("https://management.azure.com/.default")
34
+ api_key = token.token
35
+ self._token_expires_on = token.expires_on
36
+ super().__init__(base_url, api_key=api_key, max_retries=max_retries, backoff_factor=backoff_factor)
37
+
38
+ def _refresh_api_key_if_needed(self) -> None:
39
+ """Refresh the API key if using DefaultAzureCredential and the token is close to expiration."""
40
+ if self._credential is not None:
41
+ now = int(time.time())
42
+ # Refresh if less than 10 minutes (600 seconds) left
43
+ if not self._token_expires_on or self._token_expires_on - now < 600:
44
+ token = self._credential.get_token("https://management.azure.com/.default")
45
+ self.api_key = token.token
46
+ self.session.headers.update({'Authorization': f'Bearer {self.api_key}'})
47
+ self._token_expires_on = token.expires_on
48
+
49
+ def get_resource(self, resource_id, api_version=DEFAULT_API_VERSION, **kwargs) -> object:
50
+ """
51
+ Get an ARM resource by its resource ID.
52
+
53
+ Args:
54
+ resource_id (str): The ARM resource ID.
55
+ api_version (str): The API version to use.
56
+ **kwargs: Additional arguments for the request.
57
+
58
+ Returns:
59
+ object: The resource as a dict if JSON, or text/None otherwise.
60
+ """
61
+ self._refresh_api_key_if_needed()
62
+ url = f"{self.base_url}{resource_id}?api-version={api_version}"
63
+ response = self.get(url, **kwargs)
64
+ try:
65
+ return response.json()
66
+ except JSONDecodeError:
67
+ raise ValueError(f"Failed to decode JSON response from {url}: {response.text.strip()}")
68
+
69
+ def patch_resource(self, resource_id, patch_body, api_version=DEFAULT_API_VERSION, **kwargs) -> object:
70
+ """
71
+ Patch an ARM resource.
72
+
73
+ Args:
74
+ resource_id (str): The ARM resource ID.
75
+ patch_body (dict): The patch body to send.
76
+ api_version (str): The API version to use.
77
+ **kwargs: Additional arguments for the request.
78
+
79
+ Returns:
80
+ object: The resource as a dict if JSON, or text/None otherwise.
81
+ """
82
+ self._refresh_api_key_if_needed()
83
+ url = f"{self.base_url}{resource_id}?api-version={api_version}"
84
+ headers = kwargs.pop('headers', {})
85
+ headers['Content-Type'] = 'application/json'
86
+ response = self.patch(url, json=patch_body, headers=headers, **kwargs)
87
+ try:
88
+ return response.json()
89
+ except JSONDecodeError:
90
+ return response.text or None
91
+
92
+ def put_resource(self, resource_id, put_body, api_version=DEFAULT_API_VERSION, **kwargs) -> object:
93
+ """
94
+ Put (create or update) an ARM resource.
95
+
96
+ Args:
97
+ resource_id (str): The ARM resource ID.
98
+ put_body (dict): The body to send for the resource.
99
+ api_version (str): The API version to use.
100
+ **kwargs: Additional arguments for the request.
101
+
102
+ Returns:
103
+ object: The resource as a dict if JSON, or text/None otherwise.
104
+ """
105
+ self._refresh_api_key_if_needed()
106
+ url = f"{self.base_url}{resource_id}?api-version={api_version}"
107
+ headers = kwargs.pop('headers', {})
108
+ headers['Content-Type'] = 'application/json'
109
+ response = self.put(url, json=put_body, headers=headers, **kwargs)
110
+ try:
111
+ return response.json()
112
+ except JSONDecodeError:
113
+ return response.text or None
114
+
115
+ def delete_resource(self, resource_id, api_version=DEFAULT_API_VERSION, **kwargs) -> object:
116
+ """
117
+ Delete an ARM resource by its resource ID.
118
+
119
+ Args:
120
+ resource_id (str): The ARM resource ID.
121
+ api_version (str): The API version to use.
122
+ **kwargs: Additional arguments for the request.
123
+
124
+ Returns:
125
+ object: The resource as a dict if JSON, or text/None otherwise.
126
+ """
127
+ self._refresh_api_key_if_needed()
128
+ url = f"{self.base_url}{resource_id}?api-version={api_version}"
129
+ response = self.delete(url, **kwargs)
130
+ try:
131
+ return response.json()
132
+ except JSONDecodeError:
133
+ return response.text or None
@@ -0,0 +1,138 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ import requests
5
+ import time
6
+ import logging
7
+
8
+
9
+ class BaseRestClient:
10
+ """Base REST client for making HTTP requests with retry logic and optional API key authentication."""
11
+
12
+ def __init__(self, base_url: str, api_key: str = None, max_retries: int = 5, backoff_factor: int = 1) -> None:
13
+ """
14
+ Initialize the BaseRestClient.
15
+
16
+ Args:
17
+ base_url (str): The base URL for the REST API.
18
+ api_key (str): Bearer token for authentication.
19
+ max_retries (int): Maximum number of retries for failed requests.
20
+ backoff_factor (int): Backoff factor for retry delays.
21
+ """
22
+ self.base_url = base_url.rstrip('/')
23
+ self.session = requests.Session()
24
+ if api_key:
25
+ self.session.headers.update({'Authorization': f'Bearer {api_key}'})
26
+ self.max_retries = max_retries
27
+ self.backoff_factor = backoff_factor
28
+
29
+ def _request_with_retry(self, method: str, url: str, **kwargs) -> requests.Response:
30
+ """
31
+ Make an HTTP request with retry logic for 5xx and 429 errors.
32
+
33
+ Args:
34
+ method (str): HTTP method (GET, POST, etc.).
35
+ url (str): Full URL for the request.
36
+ **kwargs: Additional arguments for requests.Session.request.
37
+
38
+ Returns:
39
+ requests.Response: The HTTP response object.
40
+ """
41
+ retries = 0
42
+ while True:
43
+ logging.info(f"Attempt {retries + 1}: {method} {url}")
44
+ response = self.session.request(method, url, **kwargs)
45
+ if response.status_code < 500 and response.status_code != 429:
46
+ logging.info(f"Success: {method} {url} (status {response.status_code})")
47
+ response.raise_for_status()
48
+ return response
49
+ # Handle 429 (Too Many Requests)
50
+ if response.status_code == 429:
51
+ retry_after = response.headers.get("Retry-After")
52
+ if retry_after:
53
+ try:
54
+ sleep_time = int(retry_after)
55
+ except ValueError:
56
+ sleep_time = self.backoff_factor * (2 ** retries)
57
+ else:
58
+ sleep_time = self.backoff_factor * (2 ** retries)
59
+ logging.info(f"Received 429. Retrying after {sleep_time} seconds...")
60
+ else:
61
+ # For all other 4xx errors, raise with details
62
+ error_msg = (
63
+ f"HTTP {response.status_code} error for {method} {url}: "
64
+ f"{response.text.strip()}"
65
+ )
66
+ logging.error(f"Error: {error_msg}")
67
+ raise Exception(error_msg)
68
+ retries += 1
69
+ if retries > self.max_retries:
70
+ logging.error(f"Max retries exceeded for {method} {url}")
71
+ response.raise_for_status()
72
+ logging.info(f"Sleeping for {sleep_time} seconds before retry...")
73
+ time.sleep(sleep_time)
74
+
75
+ def get(self, url: str, **kwargs) -> requests.Response:
76
+ """
77
+ Make a GET request with retry logic.
78
+
79
+ Args:
80
+ url (str): The URL to request.
81
+ **kwargs: Additional arguments for requests.Session.request.
82
+
83
+ Returns:
84
+ requests.Response: The HTTP response object.
85
+ """
86
+ return self._request_with_retry('GET', url, **kwargs)
87
+
88
+ def post(self, url: str, **kwargs) -> requests.Response:
89
+ """
90
+ Make a POST request with retry logic.
91
+
92
+ Args:
93
+ url (str): The URL to request.
94
+ **kwargs: Additional arguments for requests.Session.request.
95
+
96
+ Returns:
97
+ requests.Response: The HTTP response object.
98
+ """
99
+ return self._request_with_retry('POST', url, **kwargs)
100
+
101
+ def put(self, url: str, **kwargs) -> requests.Response:
102
+ """
103
+ Make a PUT request with retry logic.
104
+
105
+ Args:
106
+ url (str): The URL to request.
107
+ **kwargs: Additional arguments for requests.Session.request.
108
+
109
+ Returns:
110
+ requests.Response: The HTTP response object.
111
+ """
112
+ return self._request_with_retry('PUT', url, **kwargs)
113
+
114
+ def patch(self, url: str, **kwargs) -> requests.Response:
115
+ """
116
+ Make a PATCH request with retry logic.
117
+
118
+ Args:
119
+ url (str): The URL to request.
120
+ **kwargs: Additional arguments for requests.Session.request.
121
+
122
+ Returns:
123
+ requests.Response: The HTTP response object.
124
+ """
125
+ return self._request_with_retry('PATCH', url, **kwargs)
126
+
127
+ def delete(self, url: str, **kwargs) -> requests.Response:
128
+ """
129
+ Make a DELETE request with retry logic.
130
+
131
+ Args:
132
+ url (str): The URL to request.
133
+ **kwargs: Additional arguments for requests.Session.request.
134
+
135
+ Returns:
136
+ requests.Response: The HTTP response object.
137
+ """
138
+ return self._request_with_retry('DELETE', url, **kwargs)
@@ -0,0 +1,114 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ from azure.identity import DefaultAzureCredential
5
+ from .base_rest_client import BaseRestClient
6
+ import time
7
+
8
+
9
+ class RegistryManagementClient(BaseRestClient):
10
+ """Python client for RegistrySyndicationManifestController (excluding S2S APIs).
11
+
12
+ Handles authentication, token refresh, and provides methods for manifest and registry management.
13
+ """
14
+
15
+ def __init__(self, registry_name: str, primary_region: str = "eastus2euap", api_key: str = None, max_retries: int = 5, backoff_factor: int = 1) -> None:
16
+ """
17
+ Initialize the RegistryManagementClient.
18
+
19
+ Args:
20
+ primary_region (str): The Azure region for the registry.
21
+ registry_name (str): The name of the AzureML registry.
22
+ api_key (str, optional): Bearer token for authentication. If None, uses DefaultAzureCredential.
23
+ max_retries (int): Maximum number of retries for failed requests.
24
+ backoff_factor (int): Backoff factor for retry delays.
25
+ """
26
+ base_url = f"https://{primary_region}.api.azureml.ms"
27
+ self._credential = None
28
+ self._token_expires_on = None
29
+ if api_key is None:
30
+ # Use DefaultAzureCredential for authentication if no API key is provided
31
+ self._credential = DefaultAzureCredential()
32
+ token = self._credential.get_token("https://management.azure.com/.default")
33
+ api_key = token.token
34
+ self._token_expires_on = token.expires_on
35
+ super().__init__(base_url, api_key=api_key, max_retries=max_retries, backoff_factor=backoff_factor)
36
+ self.registry_name = registry_name
37
+
38
+ def _refresh_api_key_if_needed(self) -> None:
39
+ """Refresh the API key if using DefaultAzureCredential and the token is close to expiration."""
40
+ # Only refresh if using DefaultAzureCredential
41
+ if self._credential is not None:
42
+ now = int(time.time())
43
+ # Refresh if less than 10 minutes (600 seconds) left
44
+ if not self._token_expires_on or self._token_expires_on - now < 600:
45
+ token = self._credential.get_token("https://management.azure.com/.default")
46
+ self.api_key = token.token
47
+ self.session.headers.update({'Authorization': f'Bearer {self.api_key}'})
48
+ self._token_expires_on = token.expires_on
49
+
50
+ def create_or_update_manifest(self, manifest_dto: dict) -> dict:
51
+ """
52
+ Create or update the syndication manifest for the registry.
53
+
54
+ Args:
55
+ manifest_dto (dict): The manifest data transfer object.
56
+
57
+ Returns:
58
+ dict: The response from the service.
59
+ """
60
+ self._refresh_api_key_if_needed()
61
+ url = f"{self.base_url}/registrymanagement/v1.0/registrySyndication/{self.registry_name}/createOrUpdateManifest"
62
+ response = self.post(url, json=manifest_dto)
63
+ return response.json()
64
+
65
+ def resync_assets_in_manifest(self, resync_assets_dto: dict) -> dict:
66
+ """
67
+ Resynchronize assets in the syndication manifest.
68
+
69
+ Args:
70
+ resync_assets_dto (dict): The DTO specifying which assets to resync.
71
+
72
+ Returns:
73
+ dict: The response from the service.
74
+ """
75
+ self._refresh_api_key_if_needed()
76
+ url = f"{self.base_url}/registrymanagement/v1.0/registrySyndication/{self.registry_name}/resyncAssetsInManifest"
77
+ response = self.post(url, json=resync_assets_dto)
78
+ return response.json()
79
+
80
+ def delete_manifest(self) -> dict:
81
+ """
82
+ Delete the syndication manifest for the registry.
83
+
84
+ Returns:
85
+ dict: The response from the service.
86
+ """
87
+ self._refresh_api_key_if_needed()
88
+ url = f"{self.base_url}/registrymanagement/v1.0/registrySyndication/{self.registry_name}/deleteManifest"
89
+ response = self.post(url)
90
+ return response.json()
91
+
92
+ def get_manifest(self) -> dict:
93
+ """
94
+ Get the syndication manifest for the registry.
95
+
96
+ Returns:
97
+ dict: The manifest data.
98
+ """
99
+ self._refresh_api_key_if_needed()
100
+ url = f"{self.base_url}/registrymanagement/v1.0/registrySyndication/{self.registry_name}/getManifest"
101
+ response = self.get(url)
102
+ return response.json()
103
+
104
+ def discovery(self) -> dict:
105
+ """
106
+ Get discovery information for the registry.
107
+
108
+ Returns:
109
+ dict: The discovery information for the registry.
110
+ """
111
+ self._refresh_api_key_if_needed()
112
+ url = f"{self.base_url}/registrymanagement/v1.0/registries/{self.registry_name}/discovery"
113
+ response = self.get(url)
114
+ return response.json()
@@ -0,0 +1,4 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ """This module is for manifest operations."""
@@ -0,0 +1,235 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ """Syndication manifest generator for AzureML registries.
5
+
6
+ This script parses YAML files describing source registries and assets, validates them, and generates a syndication
7
+ manifest for AzureML registries.
8
+ """
9
+
10
+ import os
11
+ import glob
12
+ import argparse
13
+ import json
14
+ from ruamel.yaml import YAML
15
+ from .syndication_manifest import Asset, SourceRegistry, SyndicationManifest
16
+
17
+ REQUIRED_SOURCE_FIELDS = ['name', 'tenant_id', 'is_ipp', 'assets']
18
+ REQUIRED_REGISTRY_FIELDS = ['tenant_id', 'name']
19
+ ALLOWED_ASSET_TYPES = {
20
+ 'environments', 'models', 'deployment-templates', 'components', 'datasets'
21
+ }
22
+
23
+ ASSET_TYPE_MAP = {
24
+ 'models': 'Models',
25
+ 'environments': 'Environments',
26
+ 'deployment-templates': 'DeploymentTemplates',
27
+ 'components': 'Components',
28
+ 'datasets': 'Datasets',
29
+ }
30
+
31
+
32
+ def validate_manifest_data(data: dict, file_path: str) -> None:
33
+ """Validate required root-level fields and allowed values for nested fields under 'assets'.
34
+
35
+ Args:
36
+ data (dict): Parsed YAML content.
37
+ file_path (str): Path to the YAML file (for error reporting).
38
+
39
+ Raises:
40
+ ValueError: If required fields are missing or invalid values are found.
41
+ """
42
+ missing = [field for field in REQUIRED_SOURCE_FIELDS if field not in data]
43
+ if missing:
44
+ raise ValueError(
45
+ f"Missing required field(s) in {file_path}: {', '.join(missing)}"
46
+ )
47
+ assets = data.get('assets', {})
48
+ if not isinstance(assets, dict):
49
+ raise ValueError(f"'assets' field in {file_path} must be a dictionary.")
50
+ for asset_type in assets:
51
+ if asset_type not in ALLOWED_ASSET_TYPES:
52
+ raise ValueError(
53
+ f"Invalid asset type '{asset_type}' in 'assets' of {file_path}. Allowed: "
54
+ f"{', '.join(ALLOWED_ASSET_TYPES)}"
55
+ )
56
+
57
+
58
+ def parse_asset_objs(asset_type: str, asset_dict: dict) -> list:
59
+ """Parse asset objects from the asset dictionary for a given asset type.
60
+
61
+ Args:
62
+ asset_type (str): The asset type (e.g., 'models').
63
+ asset_dict (dict or str): Dictionary of asset names to version lists or dicts, or a wildcard string.
64
+
65
+ Returns:
66
+ list: List of Asset objects.
67
+ """
68
+ assets = []
69
+ # Handle wildcard string case: environments: "*"
70
+ if asset_dict == "*":
71
+ assets.append(Asset(name=".*", version=".*"))
72
+ return assets
73
+ if not asset_dict or not isinstance(asset_dict, dict):
74
+ return assets
75
+ for asset_name, asset_info in asset_dict.items():
76
+ if isinstance(asset_info, dict) and 'versions' in asset_info:
77
+ versions = asset_info['versions']
78
+ elif isinstance(asset_info, list):
79
+ versions = asset_info
80
+ else:
81
+ versions = [asset_info]
82
+ for version in versions:
83
+ assets.append(Asset(name=asset_name, version=version))
84
+ return assets
85
+
86
+
87
+ def read_manifest_files(folder: str) -> dict:
88
+ """Recursively read all YAML files in the 'sources' subdirectory and validate required root-level fields.
89
+
90
+ Args:
91
+ folder (str): Root folder containing the 'sources' directory.
92
+
93
+ Returns:
94
+ dict: Mapping of file paths to loaded SourceRegistry instances.
95
+
96
+ Raises:
97
+ ValueError: If required fields are missing in any YAML file.
98
+ """
99
+ sources_path = os.path.join(folder, 'sources')
100
+ yaml_files = glob.glob(os.path.join(sources_path, '**', '*.yaml'), recursive=True)
101
+ contents = {}
102
+ yaml = YAML(typ='safe')
103
+ for file_path in yaml_files:
104
+ with open(file_path, 'r', encoding='utf-8') as f:
105
+ data = yaml.load(f)
106
+ validate_manifest_data(data, file_path)
107
+ assets = {}
108
+ for yaml_key in ALLOWED_ASSET_TYPES:
109
+ asset_objs = parse_asset_objs(yaml_key, data.get('assets', {}).get(yaml_key))
110
+ if asset_objs:
111
+ assets[yaml_key] = asset_objs
112
+ source_registry = SourceRegistry(
113
+ registry_name=data['name'],
114
+ tenant_id=data['tenant_id'],
115
+ assets=assets
116
+ )
117
+ contents[file_path] = source_registry
118
+ return contents
119
+
120
+
121
+ def _load_registry_yaml(folder: str) -> SyndicationManifest:
122
+ """Load 'registry.yaml' from the specified folder into a SyndicationManifest instance (without SourceRegistries).
123
+
124
+ Args:
125
+ folder (str): Folder containing the 'registry.yaml' file.
126
+
127
+ Returns:
128
+ SyndicationManifest: Instance populated from the YAML file (SourceRegistries is an empty list), with
129
+ _allow_wildcards set.
130
+
131
+ Raises:
132
+ FileNotFoundError: If 'registry.yaml' is not found in the folder.
133
+ ValueError: If required fields are missing.
134
+ """
135
+ registry_path = os.path.join(folder, 'registry.yaml')
136
+ if not os.path.isfile(registry_path):
137
+ raise FileNotFoundError(f"{registry_path} not found.")
138
+ yaml = YAML(typ='safe')
139
+ with open(registry_path, 'r', encoding='utf-8') as f:
140
+ data = yaml.load(f)
141
+ missing = [field for field in REQUIRED_REGISTRY_FIELDS if field not in data]
142
+ if missing:
143
+ raise ValueError(
144
+ f"Missing required field(s) in registry.yaml: {', '.join(missing)}"
145
+ )
146
+ # Read allow_wildcards flag, default to False if not present
147
+ allow_wildcards = data.get('allow_wildcards', False)
148
+ return SyndicationManifest(
149
+ registry_name=data['name'],
150
+ tenant_id=data['tenant_id'],
151
+ source_registries=[],
152
+ _allow_wildcards=allow_wildcards
153
+ )
154
+
155
+
156
+ def generate_syndication_manifest(folder: str) -> SyndicationManifest:
157
+ """Load the destination registry and all source registries manifests from the specified folder.
158
+
159
+ This function loads the destination registry's configuration, all source registries, validates asset uniqueness
160
+ and wildcard usage, and returns a complete SyndicationManifest object.
161
+
162
+ Args:
163
+ folder (str): Root folder containing the manifest for the sources.
164
+
165
+ Returns:
166
+ SyndicationManifest: The complete syndication manifest with destination and source registries.
167
+
168
+ Raises:
169
+ ValueError: If asset names are not unique across all SourceRegistry instances, or if wildcards are not allowed
170
+ but used.
171
+ """
172
+ # Load the destination registry manifest and allow_wildcards flag
173
+ manifest = _load_registry_yaml(folder)
174
+ allow_wildcards = manifest._allow_wildcards
175
+ # Read all source registry manifests from the sources directory
176
+ source_registries_dict = read_manifest_files(folder)
177
+ source_registries = list(source_registries_dict.values())
178
+
179
+ # Validate uniqueness of asset names within the same asset type across all SourceRegistry instances
180
+ asset_types = [
181
+ ('Models', 'models'),
182
+ ('Environments', 'environments'),
183
+ ('DeploymentTemplates', 'deployment-templates'),
184
+ ('Components', 'components'),
185
+ ('Datasets', 'datasets'),
186
+ ]
187
+ for class_attr, yaml_key in asset_types:
188
+ registry_asset_map = dict() # asset_name -> set of (registry_name, uri)
189
+ duplicates = dict() # asset_name -> set of uris
190
+ for sr in source_registries:
191
+ asset_list = sr.assets.get(yaml_key, [])
192
+ for asset in asset_list:
193
+ uri = f"azureml://registries/{sr.registry_name}/{yaml_key}/name/{asset.name}/versions/{asset.version}"
194
+ # If allow_wildcards is False, disallow wildcard versions in asset URIs
195
+ if not allow_wildcards:
196
+ if asset.version == ".*" or asset.version == "*":
197
+ raise ValueError(
198
+ f"Wildcard asset version not allowed (allow_wildcards is False): {uri}"
199
+ )
200
+ # Extract asset name from URI for uniqueness validation
201
+ asset_name = asset.name
202
+ registry_name = sr.registry_name
203
+ if asset_name not in registry_asset_map:
204
+ registry_asset_map[asset_name] = set()
205
+ registry_asset_map[asset_name].add((registry_name, uri))
206
+ # Check for duplicate asset names across registries for this asset type
207
+ for asset_name, reg_uris in registry_asset_map.items():
208
+ registries = {reg for reg, _ in reg_uris}
209
+ if len(registries) > 1:
210
+ uris = {uri for _, uri in reg_uris}
211
+ duplicates[asset_name] = uris
212
+ if duplicates:
213
+ error_lines = []
214
+ for asset_name, uris in sorted(duplicates.items()):
215
+ error_lines.append(
216
+ f"Asset name '{asset_name}' for asset type '{yaml_key}' is duplicated across registries in URIs: "
217
+ + ", ".join(sorted(uris))
218
+ )
219
+ raise ValueError("\n".join(error_lines))
220
+
221
+ # Populate SourceRegistries in the SyndicationManifest instance
222
+ manifest.source_registries = source_registries
223
+ return manifest
224
+
225
+
226
+ if __name__ == "__main__":
227
+ # Parse command-line arguments for the manifest folder
228
+ parser = argparse.ArgumentParser(description="Syndication manifest structure.")
229
+ parser.add_argument(
230
+ '-f', '--folder', type=str, help='Registry syndication manifest folder', required=True
231
+ )
232
+ args = parser.parse_args()
233
+ # Generate the syndication manifest and print as formatted JSON
234
+ manifest = generate_syndication_manifest(args.folder)
235
+ print(json.dumps(manifest.to_dto(), indent=2))
@@ -0,0 +1,150 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ """Syndication manifest dataclasses and validation for AzureML registry asset syndication."""
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import List
8
+ from uuid import UUID
9
+
10
+
11
+ def _norm_key(k):
12
+ return k.replace('_', '').lower()
13
+
14
+
15
+ def _get_key(dct, *keys, normalize_keys=False):
16
+ """Normalize and retrieve a value from a dict by trying multiple key casings.
17
+
18
+ If normalize_keys is True, normalize the dict keys before searching.
19
+ """
20
+ if normalize_keys:
21
+ dct_norm = {_norm_key(k): v for k, v in dct.items()}
22
+ for k in keys:
23
+ nk = _norm_key(k)
24
+ if nk in dct_norm:
25
+ return dct_norm[nk]
26
+ return None
27
+ else:
28
+ for k in keys:
29
+ if k in dct:
30
+ return dct[k]
31
+ return None
32
+
33
+
34
+ @dataclass
35
+ class Asset:
36
+ """Represents a single asset with a name and version."""
37
+
38
+ name: str
39
+ version: str
40
+
41
+ def to_dict(self) -> dict:
42
+ """Convert the Asset instance to a dictionary with serialized field names."""
43
+ return {"Name": self.name, "Version": self.version}
44
+
45
+ @staticmethod
46
+ def from_dict(d: dict, normalize_keys=False) -> "Asset":
47
+ """Create an Asset instance from a dictionary with serialized field names."""
48
+ return Asset(
49
+ name=_get_key(d, "Name", normalize_keys=normalize_keys),
50
+ version=_get_key(d, "Version", normalize_keys=normalize_keys) or ".*"
51
+ )
52
+
53
+
54
+ @dataclass
55
+ class SourceRegistry:
56
+ """Represents a source registry in the syndication manifest.
57
+
58
+ Attributes:
59
+ registry_name (str): The name of the source registry.
60
+ tenant_id (UUID): The Azure tenant ID for the source registry.
61
+ assets (dict): Dictionary mapping asset type to list of Asset objects.
62
+ """
63
+
64
+ registry_name: str
65
+ tenant_id: UUID
66
+ assets: dict # asset_type (str) -> List[Asset]
67
+
68
+ def to_dict(self) -> dict:
69
+ """Convert the SourceRegistry instance to a dictionary with serialized field names."""
70
+ asset_dict = {}
71
+ for asset_type, asset_list in self.assets.items():
72
+ if asset_list:
73
+ asset_dict[asset_type] = [
74
+ {"Name": a.name} if a.name == ".*" else {"Name": a.name, "Version": a.version}
75
+ for a in asset_list
76
+ ]
77
+ return {
78
+ "RegistryName": self.registry_name,
79
+ "TenantId": str(self.tenant_id),
80
+ "Assets": asset_dict
81
+ }
82
+
83
+ @staticmethod
84
+ def from_dict(d: dict, normalize_keys=False) -> "SourceRegistry":
85
+ """Create a SourceRegistry instance from a dictionary with any key casing (snake, camel, Pascal)."""
86
+ assets = {}
87
+ assets_dict = _get_key(d, "Assets", "assets", normalize_keys=normalize_keys)
88
+ for asset_type, asset_list in assets_dict.items():
89
+ assets[asset_type] = [Asset.from_dict(a, normalize_keys=normalize_keys) for a in asset_list]
90
+ return SourceRegistry(
91
+ registry_name=_get_key(d, "RegistryName", "registry_name", normalize_keys=normalize_keys),
92
+ tenant_id=UUID(str(_get_key(d, "TenantId", "tenant_id", normalize_keys=normalize_keys))),
93
+ assets=assets
94
+ )
95
+
96
+
97
+ @dataclass
98
+ class SyndicationManifest:
99
+ """Represents the root syndication manifest for a destination registry.
100
+
101
+ Attributes:
102
+ registry_name (str): The name of the destination registry.
103
+ tenant_id (UUID): The Azure tenant ID for the destination registry.
104
+ source_registries (List[SourceRegistry]): All source registries.
105
+ _allow_wildcards (bool): Internal flag for wildcard version validation (not serialized).
106
+ """
107
+
108
+ registry_name: str
109
+ tenant_id: UUID
110
+ source_registries: List[SourceRegistry]
111
+ _allow_wildcards: bool = field(default=False, repr=False, compare=False)
112
+
113
+ def to_dict(self) -> dict:
114
+ """Convert the SyndicationManifest instance to a dictionary with serialized field names."""
115
+ return {
116
+ "RegistryName": self.registry_name,
117
+ "TenantId": str(self.tenant_id),
118
+ "SourceRegistries": [sr.to_dict() for sr in self.source_registries]
119
+ }
120
+
121
+ def to_dto(self) -> dict:
122
+ """Serialize the manifest as a value of {"Manifest": SyndicationManifest} for external consumers."""
123
+ return {"Manifest": self.to_dict()}
124
+
125
+ @staticmethod
126
+ def from_dto(dto: dict, normalize_keys=False) -> "SyndicationManifest":
127
+ """Deserialize a SyndicationManifest from a dictionary produced by to_dto, with validation. Handles any key casing."""
128
+ if not isinstance(dto, dict) or _get_key(dto, "Manifest", normalize_keys=normalize_keys) is None:
129
+ raise ValueError("Input must be a dict with a 'Manifest' key.")
130
+ manifest = _get_key(dto, "Manifest", normalize_keys=normalize_keys)
131
+ if not isinstance(manifest, dict):
132
+ raise ValueError("'Manifest' value must be a dict.")
133
+ for field_name in ("RegistryName", "TenantId", "SourceRegistries"):
134
+ if _get_key(manifest, field_name, normalize_keys=normalize_keys) is None:
135
+ raise ValueError(f"Missing required field '{field_name}' in Manifest.")
136
+ registry_name = _get_key(manifest, "RegistryName", "registry_name", normalize_keys=normalize_keys)
137
+ tenant_id = _get_key(manifest, "TenantId", "tenant_id", normalize_keys=normalize_keys)
138
+ try:
139
+ tenant_id = UUID(str(tenant_id))
140
+ except Exception:
141
+ raise ValueError("TenantId must be a valid UUID string.")
142
+ sr_list = _get_key(manifest, "SourceRegistries", "source_registries", normalize_keys=normalize_keys)
143
+ if not isinstance(sr_list, list):
144
+ raise ValueError("SourceRegistries must be a list.")
145
+ source_registries = [SourceRegistry.from_dict(sr, normalize_keys=normalize_keys) for sr in sr_list]
146
+ return SyndicationManifest(
147
+ registry_name=registry_name,
148
+ tenant_id=tenant_id,
149
+ source_registries=source_registries
150
+ )
@@ -1,11 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: azureml-registry-tools
3
- Version: 0.1.0a1
3
+ Version: 0.1.0a2
4
4
  Summary: AzureML Registry tools and CLI
5
5
  Author: Microsoft Corp
6
6
  License: https://aka.ms/azureml-sdk-license
7
- Requires-Python: >=3.9,<3.12
7
+ Requires-Python: >=3.9,<3.14
8
8
  License-File: LICENSE.txt
9
+ Requires-Dist: azure-identity<2.0
10
+ Requires-Dist: ruamel-yaml<0.19,>=0.17.21
9
11
  Requires-Dist: azure-ai-ml<2.0
10
12
  Requires-Dist: azureml-assets<2.0
11
13
  Dynamic: author
@@ -4,7 +4,15 @@ setup.py
4
4
  azureml/__init__.py
5
5
  azureml/registry/__init__.py
6
6
  azureml/registry/_cli/__init__.py
7
+ azureml/registry/_cli/registry_syndication_cli.py
7
8
  azureml/registry/_cli/repo2registry_cli.py
9
+ azureml/registry/_rest_client/__init__.py
10
+ azureml/registry/_rest_client/arm_client.py
11
+ azureml/registry/_rest_client/base_rest_client.py
12
+ azureml/registry/_rest_client/registry_management_client.py
13
+ azureml/registry/mgmt/__init__.py
14
+ azureml/registry/mgmt/create_manifest.py
15
+ azureml/registry/mgmt/syndication_manifest.py
8
16
  azureml/registry/tools/__init__.py
9
17
  azureml/registry/tools/config.py
10
18
  azureml/registry/tools/create_or_update_assets.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
+ registry-mgmt = azureml.registry._cli.registry_syndication_cli:main
2
3
  repo2registry = azureml.registry._cli.repo2registry_cli:main
@@ -0,0 +1,4 @@
1
+ azure-identity<2.0
2
+ ruamel-yaml<0.19,>=0.17.21
3
+ azure-ai-ml<2.0
4
+ azureml-assets<2.0
@@ -7,6 +7,8 @@
7
7
  from setuptools import setup, find_packages
8
8
 
9
9
  DEPENDENCIES = [
10
+ "azure-identity<2.0",
11
+ "ruamel-yaml>=0.17.21,<0.19",
10
12
  "azure-ai-ml<2.0",
11
13
  "azureml-assets<2.0"
12
14
  ]
@@ -15,16 +17,17 @@ exclude_list = ["*.tests"]
15
17
 
16
18
  setup(
17
19
  name='azureml-registry-tools',
18
- version="0.1.0a1",
20
+ version="0.1.0a2",
19
21
  description='AzureML Registry tools and CLI',
20
22
  author='Microsoft Corp',
21
23
  license="https://aka.ms/azureml-sdk-license",
22
24
  packages=find_packages(exclude=exclude_list),
23
25
  install_requires=DEPENDENCIES,
24
- python_requires=">=3.9,<3.12",
26
+ python_requires=">=3.9,<3.14",
25
27
  entry_points={
26
28
  'console_scripts': [
27
- 'repo2registry = azureml.registry._cli.repo2registry_cli:main'
29
+ 'repo2registry = azureml.registry._cli.repo2registry_cli:main',
30
+ 'registry-mgmt = azureml.registry._cli.registry_syndication_cli:main',
28
31
  ],
29
32
  }
30
33
  )
@@ -1,2 +0,0 @@
1
- azure-ai-ml<2.0
2
- azureml-assets<2.0