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.
@@ -1,16 +1,14 @@
1
1
  # ---------------------------------------------------------
2
2
  # Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  # ---------------------------------------------------------
4
- from azure.identity import DefaultAzureCredential
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(BaseRestClient):
13
- """Simple Azure Resource Manager (ARM) client leveraging BaseRestClient for GET, PATCH, PUT, and DELETE operations.
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 azure.identity import DefaultAzureCredential
5
- from .base_rest_client import BaseRestClient
6
- import time
4
+ from .base_rest_client import BaseAzureRestClient
7
5
 
8
6
 
9
- class RegistryManagementClient(BaseRestClient):
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 = "eastus2euap", api_key: str = None, max_retries: int = 5, backoff_factor: int = 1) -> None:
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
- 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
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
- response = self.get(url)
114
- return response.json()
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
- updated_asset_file_name: str) -> dict:
125
- """Overwrite values from updated asset to existing asset with the exception of storage path.
126
+ updated_asset: AssetSpec) -> dict:
127
+ """Preserve storage value from existing asset to updated asset.
126
128
 
127
129
  Args:
128
130
  existing_asset_file_name (str): Name of file containing details of asset currently in registry.
129
- updated_asset_file_name (str): Name of file containing updated asset.
130
-
131
+ updated_asset (AssetSpec): AssetSpec containing updated asset info.
131
132
  Returns:
132
133
  dict: Merged asset data.
133
134
  """
134
- with open(existing_asset_file_name, "r") as existing_asset_file, open(updated_asset_file_name, "r") as updated_asset_file:
135
- existing_asset_yaml = yaml.safe_load(existing_asset_file)
136
- updated_asset_yaml = yaml.safe_load(updated_asset_file)
137
- merged_asset = {**existing_asset_yaml, **updated_asset_yaml}
138
- merged_asset["path"] = existing_asset_yaml["path"]
135
+ with open(existing_asset_file_name, "r") as existing_asset_file, open(updated_asset.file_name_with_path, "r") as updated_asset_file:
136
+ existing_asset_dict = yaml.safe_load(existing_asset_file)
137
+ updated_asset_dict = yaml.safe_load(updated_asset_file)
138
+
139
+ merged_asset = copy.deepcopy(updated_asset_dict)
140
+
141
+ if updated_asset.type == AssetType.MODEL or updated_asset.type == AssetType.DATA:
142
+ storage_key = "path"
143
+ # "path" is always required for models, use the existing value
144
+ if existing_asset_dict.get(storage_key) != updated_asset_dict.get(storage_key):
145
+ print(f'"{storage_key}" is immutable, using existing value from asset in registry: {existing_asset_dict[storage_key]}')
146
+ merged_asset[storage_key] = existing_asset_dict[storage_key]
147
+ elif updated_asset.type == AssetType.COMPONENT:
148
+ storage_key = "code"
149
+ if existing_asset_dict.get(storage_key) is None and updated_asset_dict.get(storage_key) is not None:
150
+ print(f'"{storage_key}" is immutable and cannot be added. Please create a new version to include "{storage_key}"')
151
+ elif existing_asset_dict.get(storage_key) != updated_asset_dict.get(storage_key):
152
+ print(f'"{storage_key}" is immutable, using existing value from asset in registry: {existing_asset_dict[storage_key]}')
153
+ merged_asset[storage_key] = existing_asset_dict[storage_key]
139
154
 
140
155
  return merged_asset
141
156
 
142
157
 
143
- def merge_assets(existing_asset: Union[Data, Model],
158
+ def merge_assets(existing_asset: Union[Component, Data, Model],
144
159
  asset: AssetSpec):
145
- """Overwrite values from updated to existing asset while preserving existing asset path.
160
+ """Prepare asset for update by preserving storage value from existing asset.
146
161
 
147
162
  Args:
148
- existing_asset (Union[Data, Model]): Asset currently in registry.
163
+ existing_asset (Union[Component, Data, Model]): Asset currently in registry.
149
164
  asset (AssetSpec): AssetSpec containing updated asset info.
150
165
  """
151
166
  with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as existing_asset_temp_file:
152
167
  existing_asset_temp_file_name = existing_asset_temp_file.name
153
168
 
154
169
  existing_asset.dump(existing_asset_temp_file_name)
155
- merged_result = merge_yamls(existing_asset_temp_file_name, asset.file_name_with_path)
170
+
171
+ merged_result = merge_yamls(existing_asset_temp_file_name, asset)
156
172
 
157
173
  with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as merged_asset_temp_file:
158
174
  merged_asset_temp_file_name = merged_asset_temp_file.name
@@ -164,6 +180,8 @@ def merge_assets(existing_asset: Union[Data, Model],
164
180
  merged_asset = load_model(merged_asset_temp_file_name)
165
181
  elif asset.type == AssetType.DATA:
166
182
  merged_asset = load_data(merged_asset_temp_file_name)
183
+ elif asset.type == AssetType.COMPONENT:
184
+ merged_asset = load_component(merged_asset_temp_file_name)
167
185
 
168
186
  asset.set_asset_obj_to_create_or_update(merged_asset)
169
187
 
@@ -289,7 +307,7 @@ def create_or_update_assets(input_dir: Path,
289
307
  raise
290
308
 
291
309
  if existing_asset:
292
- if asset.type == AssetType.MODEL or asset.type == AssetType.DATA:
310
+ if asset.type in [AssetType.COMPONENT, AssetType.DATA, AssetType.MODEL]:
293
311
  try:
294
312
  merge_assets(existing_asset, asset)
295
313
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: azureml-registry-tools
3
- Version: 0.1.0a2
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
@@ -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=dgm9nDnmGI1NIZaopXCIXmViM8sSAipZETfuOWnQRa0,5638
8
- azureml/registry/_rest_client/base_rest_client.py,sha256=cXFYvYgSYSveEeY511VIlDpmXLOY9Jat_E-Zs1ehMyA,5164
9
- azureml/registry/_rest_client/registry_management_client.py,sha256=JGBohCVpL8XR4rNuScOKt7tNQnjw203pqlTSLKVjYgM,4833
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=NexSY22bpN2eHPqZZ21eDIMerwq3SzVk_4fBV5_GTBY,14787
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.0a2.dist-info/licenses/LICENSE.txt,sha256=n20rxwp7_NGrrShv9Qvcs90sjI1l3Pkt3m-5OPCWzgs,845
19
- azureml_registry_tools-0.1.0a2.dist-info/METADATA,sha256=SJpiD2SsYXqJi_DUSLk-mquFZgbnb54Sd_mrP6H-GBI,491
20
- azureml_registry_tools-0.1.0a2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- azureml_registry_tools-0.1.0a2.dist-info/entry_points.txt,sha256=iRUkAeQidMnO6RQzpLqMUBTcyYtNzAfSin9WnSdVGLw,147
22
- azureml_registry_tools-0.1.0a2.dist-info/top_level.txt,sha256=ZOeEa0TAXo6i5wOjwBoqfIGEuxOcKuscGgNSpizqREY,8
23
- azureml_registry_tools-0.1.0a2.dist-info/RECORD,,
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,,