azureml-registry-tools 0.1.0a2__py3-none-any.whl → 0.1.0a4__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.
- azureml/registry/_rest_client/arm_client.py +3 -24
- azureml/registry/_rest_client/base_rest_client.py +63 -0
- azureml/registry/_rest_client/registry_management_client.py +31 -26
- azureml/registry/tools/create_or_update_assets.py +33 -15
- {azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/METADATA +2 -1
- {azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/RECORD +10 -10
- {azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/WHEEL +0 -0
- {azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/entry_points.txt +0 -0
- {azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/licenses/LICENSE.txt +0 -0
- {azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,14 @@
|
|
1
1
|
# ---------------------------------------------------------
|
2
2
|
# Copyright (c) Microsoft Corporation. All rights reserved.
|
3
3
|
# ---------------------------------------------------------
|
4
|
-
from
|
5
|
-
from .base_rest_client import BaseRestClient
|
6
|
-
import time
|
4
|
+
from .base_rest_client import BaseAzureRestClient
|
7
5
|
from json.decoder import JSONDecodeError
|
8
6
|
|
9
7
|
DEFAULT_API_VERSION = "2025-04-01" # Default API version for Azure Resource Manager
|
10
8
|
|
11
9
|
|
12
|
-
class ArmClient(
|
13
|
-
"""Simple Azure Resource Manager (ARM) client leveraging
|
10
|
+
class ArmClient(BaseAzureRestClient):
|
11
|
+
"""Simple Azure Resource Manager (ARM) client leveraging BaseAzureRestClient for GET, PATCH, PUT, and DELETE operations.
|
14
12
|
|
15
13
|
Handles authentication via Bearer token (pass as api_key) and supports standard ARM resource operations.
|
16
14
|
"""
|
@@ -25,27 +23,8 @@ class ArmClient(BaseRestClient):
|
|
25
23
|
backoff_factor (int): Backoff factor for retries.
|
26
24
|
"""
|
27
25
|
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
26
|
super().__init__(base_url, api_key=api_key, max_retries=max_retries, backoff_factor=backoff_factor)
|
37
27
|
|
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
28
|
def get_resource(self, resource_id, api_version=DEFAULT_API_VERSION, **kwargs) -> object:
|
50
29
|
"""
|
51
30
|
Get an ARM resource by its resource ID.
|
@@ -4,6 +4,11 @@
|
|
4
4
|
import requests
|
5
5
|
import time
|
6
6
|
import logging
|
7
|
+
from azure.identity import DefaultAzureCredential
|
8
|
+
from diskcache import Cache
|
9
|
+
import tempfile
|
10
|
+
|
11
|
+
ENABLE_CACHE = True # Set to False to disable diskcache usage for API key
|
7
12
|
|
8
13
|
|
9
14
|
class BaseRestClient:
|
@@ -136,3 +141,61 @@ class BaseRestClient:
|
|
136
141
|
requests.Response: The HTTP response object.
|
137
142
|
"""
|
138
143
|
return self._request_with_retry('DELETE', url, **kwargs)
|
144
|
+
|
145
|
+
|
146
|
+
class BaseAzureRestClient(BaseRestClient):
|
147
|
+
"""Base Azure REST client that handles Azure authentication and token refresh, with diskcache for API key."""
|
148
|
+
|
149
|
+
if ENABLE_CACHE:
|
150
|
+
_cache_dir = tempfile.gettempdir() + '/.azureml_registry_token_cache'
|
151
|
+
_cache = Cache(_cache_dir)
|
152
|
+
else:
|
153
|
+
_cache = None
|
154
|
+
|
155
|
+
def __init__(self, base_url: str, api_key: str = None, max_retries: int = 5, backoff_factor: int = 1):
|
156
|
+
"""
|
157
|
+
Initialize the BaseAzureRestClient.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
base_url (str): The base URL for the REST API.
|
161
|
+
api_key (str, optional): Bearer token for authentication. If None, uses DefaultAzureCredential.
|
162
|
+
max_retries (int): Maximum number of retries for failed requests.
|
163
|
+
backoff_factor (int): Backoff factor for retry delays.
|
164
|
+
"""
|
165
|
+
self._credential = None
|
166
|
+
self._token_expires_on = None
|
167
|
+
cache_key = f"azureml_api_key_{base_url}"
|
168
|
+
cache_expiry_key = f"azureml_api_key_expiry_{base_url}"
|
169
|
+
cached_token = None
|
170
|
+
cached_expiry = None
|
171
|
+
now = int(time.time())
|
172
|
+
if ENABLE_CACHE and self._cache is not None:
|
173
|
+
cached_token = self._cache.get(cache_key)
|
174
|
+
cached_expiry = self._cache.get(cache_expiry_key)
|
175
|
+
if api_key is None and cached_token and cached_expiry and cached_expiry > now:
|
176
|
+
api_key = cached_token
|
177
|
+
self._token_expires_on = cached_expiry
|
178
|
+
super().__init__(base_url, api_key=api_key, max_retries=max_retries, backoff_factor=backoff_factor)
|
179
|
+
# Only after super().__init__ is self.session available
|
180
|
+
if api_key is None:
|
181
|
+
self._credential = DefaultAzureCredential()
|
182
|
+
self._refresh_api_key_if_needed()
|
183
|
+
# Ensure self.api_key is set for future use
|
184
|
+
api_key = self.api_key
|
185
|
+
|
186
|
+
def _refresh_api_key_if_needed(self) -> None:
|
187
|
+
if self._credential is None:
|
188
|
+
return
|
189
|
+
now = int(time.time())
|
190
|
+
if not getattr(self, '_token_expires_on', None) or self._token_expires_on - now < 600:
|
191
|
+
token = self._credential.get_token("https://management.azure.com/.default")
|
192
|
+
self.api_key = token.token
|
193
|
+
self.session.headers.update({'Authorization': f'Bearer {self.api_key}'})
|
194
|
+
self._token_expires_on = token.expires_on
|
195
|
+
# Store in diskcache if enabled
|
196
|
+
if ENABLE_CACHE and self._cache is not None:
|
197
|
+
base_url = getattr(self, 'base_url', 'default')
|
198
|
+
cache_key = f"azureml_api_key_{base_url}"
|
199
|
+
cache_expiry_key = f"azureml_api_key_expiry_{base_url}"
|
200
|
+
self._cache.set(cache_key, self.api_key, expire=(self._token_expires_on - now))
|
201
|
+
self._cache.set(cache_expiry_key, self._token_expires_on, expire=(self._token_expires_on - now))
|
@@ -1,18 +1,16 @@
|
|
1
1
|
# ---------------------------------------------------------
|
2
2
|
# Copyright (c) Microsoft Corporation. All rights reserved.
|
3
3
|
# ---------------------------------------------------------
|
4
|
-
from
|
5
|
-
from .base_rest_client import BaseRestClient
|
6
|
-
import time
|
4
|
+
from .base_rest_client import BaseAzureRestClient
|
7
5
|
|
8
6
|
|
9
|
-
class RegistryManagementClient(
|
7
|
+
class RegistryManagementClient(BaseAzureRestClient):
|
10
8
|
"""Python client for RegistrySyndicationManifestController (excluding S2S APIs).
|
11
9
|
|
12
10
|
Handles authentication, token refresh, and provides methods for manifest and registry management.
|
13
11
|
"""
|
14
12
|
|
15
|
-
def __init__(self, registry_name: str, primary_region: str =
|
13
|
+
def __init__(self, registry_name: str, primary_region: str = None, api_key: str = None, max_retries: int = 5, backoff_factor: int = 1) -> None:
|
16
14
|
"""
|
17
15
|
Initialize the RegistryManagementClient.
|
18
16
|
|
@@ -23,29 +21,26 @@ class RegistryManagementClient(BaseRestClient):
|
|
23
21
|
max_retries (int): Maximum number of retries for failed requests.
|
24
22
|
backoff_factor (int): Backoff factor for retry delays.
|
25
23
|
"""
|
24
|
+
if primary_region is None:
|
25
|
+
# Resolve the primary region if not provided
|
26
|
+
primary_region = self.resolve_registry_primary_region(registry_name)
|
26
27
|
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
28
|
super().__init__(base_url, api_key=api_key, max_retries=max_retries, backoff_factor=backoff_factor)
|
36
29
|
self.registry_name = registry_name
|
37
30
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
31
|
+
@staticmethod
|
32
|
+
def resolve_registry_primary_region(registry_name: str) -> str:
|
33
|
+
"""
|
34
|
+
Resolve the primary region for the given registry name.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
registry_name (str): The name of the AzureML registry.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
str: The primary region for the registry.
|
41
|
+
"""
|
42
|
+
discovery = RegistryManagementClient(registry_name, primary_region="eastus").discovery()
|
43
|
+
return discovery.get('primaryRegion', 'eastus')
|
49
44
|
|
50
45
|
def create_or_update_manifest(self, manifest_dto: dict) -> dict:
|
51
46
|
"""
|
@@ -110,5 +105,15 @@ class RegistryManagementClient(BaseRestClient):
|
|
110
105
|
"""
|
111
106
|
self._refresh_api_key_if_needed()
|
112
107
|
url = f"{self.base_url}/registrymanagement/v1.0/registries/{self.registry_name}/discovery"
|
113
|
-
|
114
|
-
|
108
|
+
try:
|
109
|
+
response = self.get(url)
|
110
|
+
return response.json()
|
111
|
+
except Exception as ex:
|
112
|
+
# Special handling for HTTP errors with status_code attribute
|
113
|
+
if hasattr(ex, 'response') and ex.response is not None:
|
114
|
+
status_code = ex.response.status_code
|
115
|
+
if status_code == 403:
|
116
|
+
raise RuntimeError(f"Received 403 Forbidden. This may indicate that the registry '{self.registry_name}' does not exist or you do not have access.")
|
117
|
+
else:
|
118
|
+
raise RuntimeError(f"Failed to get discovery information: {status_code} {ex.response.text}")
|
119
|
+
raise RuntimeError(f"Error occurred while trying to get discovery information: {ex}")
|
@@ -5,6 +5,7 @@
|
|
5
5
|
"""Create or update assets."""
|
6
6
|
|
7
7
|
import azure
|
8
|
+
import copy
|
8
9
|
import json
|
9
10
|
import os
|
10
11
|
import re
|
@@ -14,7 +15,8 @@ from git import InvalidGitRepositoryError, Repo
|
|
14
15
|
from pathlib import Path
|
15
16
|
from typing import List, Union
|
16
17
|
|
17
|
-
from azure.ai.ml import MLClient, load_data, load_model
|
18
|
+
from azure.ai.ml import MLClient, load_component, load_data, load_model
|
19
|
+
from azure.ai.ml.entities._component.component import Component
|
18
20
|
from azure.ai.ml.entities._assets._artifacts.data import Data
|
19
21
|
from azure.ai.ml.entities._assets._artifacts.model import Model
|
20
22
|
from azure.identity import DefaultAzureCredential
|
@@ -121,38 +123,52 @@ def find_assets(input_dir: Path,
|
|
121
123
|
|
122
124
|
|
123
125
|
def merge_yamls(existing_asset_file_name: str,
|
124
|
-
|
125
|
-
"""
|
126
|
+
updated_asset: AssetSpec) -> dict:
|
127
|
+
"""Preserve storage value from existing asset to updated asset.
|
126
128
|
|
127
129
|
Args:
|
128
130
|
existing_asset_file_name (str): Name of file containing details of asset currently in registry.
|
129
|
-
|
130
|
-
|
131
|
+
updated_asset (AssetSpec): AssetSpec containing updated asset info.
|
131
132
|
Returns:
|
132
133
|
dict: Merged asset data.
|
133
134
|
"""
|
134
|
-
with open(existing_asset_file_name, "r") as existing_asset_file, open(
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
merged_asset
|
135
|
+
with open(existing_asset_file_name, "r") as existing_asset_file, open(updated_asset.file_name_with_path, "r") as updated_asset_file:
|
136
|
+
existing_asset_dict = yaml.safe_load(existing_asset_file)
|
137
|
+
updated_asset_dict = yaml.safe_load(updated_asset_file)
|
138
|
+
|
139
|
+
merged_asset = copy.deepcopy(updated_asset_dict)
|
140
|
+
|
141
|
+
if updated_asset.type == AssetType.MODEL or updated_asset.type == AssetType.DATA:
|
142
|
+
storage_key = "path"
|
143
|
+
# "path" is always required for models, use the existing value
|
144
|
+
if existing_asset_dict.get(storage_key) != updated_asset_dict.get(storage_key):
|
145
|
+
print(f'"{storage_key}" is immutable, using existing value from asset in registry: {existing_asset_dict[storage_key]}')
|
146
|
+
merged_asset[storage_key] = existing_asset_dict[storage_key]
|
147
|
+
elif updated_asset.type == AssetType.COMPONENT:
|
148
|
+
storage_key = "code"
|
149
|
+
if existing_asset_dict.get(storage_key) is None and updated_asset_dict.get(storage_key) is not None:
|
150
|
+
print(f'"{storage_key}" is immutable and cannot be added. Please create a new version to include "{storage_key}"')
|
151
|
+
elif existing_asset_dict.get(storage_key) != updated_asset_dict.get(storage_key):
|
152
|
+
print(f'"{storage_key}" is immutable, using existing value from asset in registry: {existing_asset_dict[storage_key]}')
|
153
|
+
merged_asset[storage_key] = existing_asset_dict[storage_key]
|
139
154
|
|
140
155
|
return merged_asset
|
141
156
|
|
142
157
|
|
143
|
-
def merge_assets(existing_asset: Union[Data, Model],
|
158
|
+
def merge_assets(existing_asset: Union[Component, Data, Model],
|
144
159
|
asset: AssetSpec):
|
145
|
-
"""
|
160
|
+
"""Prepare asset for update by preserving storage value from existing asset.
|
146
161
|
|
147
162
|
Args:
|
148
|
-
existing_asset (Union[Data, Model]): Asset currently in registry.
|
163
|
+
existing_asset (Union[Component, Data, Model]): Asset currently in registry.
|
149
164
|
asset (AssetSpec): AssetSpec containing updated asset info.
|
150
165
|
"""
|
151
166
|
with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as existing_asset_temp_file:
|
152
167
|
existing_asset_temp_file_name = existing_asset_temp_file.name
|
153
168
|
|
154
169
|
existing_asset.dump(existing_asset_temp_file_name)
|
155
|
-
|
170
|
+
|
171
|
+
merged_result = merge_yamls(existing_asset_temp_file_name, asset)
|
156
172
|
|
157
173
|
with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as merged_asset_temp_file:
|
158
174
|
merged_asset_temp_file_name = merged_asset_temp_file.name
|
@@ -164,6 +180,8 @@ def merge_assets(existing_asset: Union[Data, Model],
|
|
164
180
|
merged_asset = load_model(merged_asset_temp_file_name)
|
165
181
|
elif asset.type == AssetType.DATA:
|
166
182
|
merged_asset = load_data(merged_asset_temp_file_name)
|
183
|
+
elif asset.type == AssetType.COMPONENT:
|
184
|
+
merged_asset = load_component(merged_asset_temp_file_name)
|
167
185
|
|
168
186
|
asset.set_asset_obj_to_create_or_update(merged_asset)
|
169
187
|
|
@@ -289,7 +307,7 @@ def create_or_update_assets(input_dir: Path,
|
|
289
307
|
raise
|
290
308
|
|
291
309
|
if existing_asset:
|
292
|
-
if asset.type
|
310
|
+
if asset.type in [AssetType.COMPONENT, AssetType.DATA, AssetType.MODEL]:
|
293
311
|
try:
|
294
312
|
merge_assets(existing_asset, asset)
|
295
313
|
except Exception as e:
|
{azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: azureml-registry-tools
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.0a4
|
4
4
|
Summary: AzureML Registry tools and CLI
|
5
5
|
Author: Microsoft Corp
|
6
6
|
License: https://aka.ms/azureml-sdk-license
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.9,<3.14
|
|
8
8
|
License-File: LICENSE.txt
|
9
9
|
Requires-Dist: azure-identity<2.0
|
10
10
|
Requires-Dist: ruamel-yaml<0.19,>=0.17.21
|
11
|
+
Requires-Dist: diskcache~=5.6
|
11
12
|
Requires-Dist: azure-ai-ml<2.0
|
12
13
|
Requires-Dist: azureml-assets<2.0
|
13
14
|
Dynamic: author
|
{azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/RECORD
RENAMED
@@ -4,20 +4,20 @@ azureml/registry/_cli/__init__.py,sha256=dJ020ynrregpUaNGu8xVLLmX3cC9DAjaBMP1qnl
|
|
4
4
|
azureml/registry/_cli/registry_syndication_cli.py,sha256=87hxSu4iwFOdwfukSGqc0fjJEpSpQhDRlthiCGCDwQ8,10003
|
5
5
|
azureml/registry/_cli/repo2registry_cli.py,sha256=4CDv4tYWLTjVgdXIcoKA9iu_gOv_Z_xn05wSANkdmdg,5378
|
6
6
|
azureml/registry/_rest_client/__init__.py,sha256=Eh0vB8AVlwkZlHLO4DCGHyyfRYYgeTeVjiUAX_WujG0,219
|
7
|
-
azureml/registry/_rest_client/arm_client.py,sha256=
|
8
|
-
azureml/registry/_rest_client/base_rest_client.py,sha256=
|
9
|
-
azureml/registry/_rest_client/registry_management_client.py,sha256=
|
7
|
+
azureml/registry/_rest_client/arm_client.py,sha256=KVSj0V1bN-vCUV3fBbIdZDpWTLKYT6qdWIZtDjx2la4,4516
|
8
|
+
azureml/registry/_rest_client/base_rest_client.py,sha256=JcmhckfR-ZdUtEh0EeQIKfe63Zjowa1fLzmVeWpeLno,8173
|
9
|
+
azureml/registry/_rest_client/registry_management_client.py,sha256=P2aijKHdHvsSZ507-06Rq_n9pNJ_xci1oQKDGBJjm-I,4986
|
10
10
|
azureml/registry/mgmt/__init__.py,sha256=LMhqcEC8ItmmpKZljElGXH-6olHlT3SLl0dJU01OvuM,226
|
11
11
|
azureml/registry/mgmt/create_manifest.py,sha256=N9wRmjAKO09A3utN_lCUsM_Ufpj7PL0SJz-XHPHWuyM,9528
|
12
12
|
azureml/registry/mgmt/syndication_manifest.py,sha256=ma1_vUEHHtkRcgiFMQanlXZajvKodynylxfblTHuleE,6195
|
13
13
|
azureml/registry/tools/__init__.py,sha256=IAuWWpGfZm__pAkBIxmpJz84QskpkxBr0yDk1TUSnkE,223
|
14
14
|
azureml/registry/tools/config.py,sha256=tjPaoBsWtPXBL8Ww1hcJtsr2SuIjPKt79dR8iovcebg,3639
|
15
|
-
azureml/registry/tools/create_or_update_assets.py,sha256=
|
15
|
+
azureml/registry/tools/create_or_update_assets.py,sha256=Q-_BV7KWn1huQn5JriKT_8xJNoQQ_HK5wCftrq9DepA,15988
|
16
16
|
azureml/registry/tools/registry_utils.py,sha256=zgYlCiOONtQJ4yZ9wg8tKVoE8dh6rrjB8hYBGhpV9-0,1403
|
17
17
|
azureml/registry/tools/repo2registry_config.py,sha256=eXp_tU8Jyi30g8xGf7wbpLgKEPpieohBANKxMSLzq7s,4873
|
18
|
-
azureml_registry_tools-0.1.
|
19
|
-
azureml_registry_tools-0.1.
|
20
|
-
azureml_registry_tools-0.1.
|
21
|
-
azureml_registry_tools-0.1.
|
22
|
-
azureml_registry_tools-0.1.
|
23
|
-
azureml_registry_tools-0.1.
|
18
|
+
azureml_registry_tools-0.1.0a4.dist-info/licenses/LICENSE.txt,sha256=n20rxwp7_NGrrShv9Qvcs90sjI1l3Pkt3m-5OPCWzgs,845
|
19
|
+
azureml_registry_tools-0.1.0a4.dist-info/METADATA,sha256=XCJDpX9oa2LTU8pUOVQOGjizcLtrlRYa1f0fECIa9tM,521
|
20
|
+
azureml_registry_tools-0.1.0a4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
21
|
+
azureml_registry_tools-0.1.0a4.dist-info/entry_points.txt,sha256=iRUkAeQidMnO6RQzpLqMUBTcyYtNzAfSin9WnSdVGLw,147
|
22
|
+
azureml_registry_tools-0.1.0a4.dist-info/top_level.txt,sha256=ZOeEa0TAXo6i5wOjwBoqfIGEuxOcKuscGgNSpizqREY,8
|
23
|
+
azureml_registry_tools-0.1.0a4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
{azureml_registry_tools-0.1.0a2.dist-info → azureml_registry_tools-0.1.0a4.dist-info}/top_level.txt
RENAMED
File without changes
|