Skip to content

Commit 9357d9d

Browse files
authored
make registry delete return an lro poller (Azure#26983)
* make registry delete reutrn lro poller * cleanup * change operation name to begin_delete * sleep long enough to dodge ev issues in testing * move CL line to breaking changes * further refine delete swagger, edit docstrings * review comments * add reliability notice
1 parent 52a0357 commit 9357d9d

File tree

9 files changed

+523
-681
lines changed

9 files changed

+523
-681
lines changed

sdk/ml/azure-ai-ml/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- MLClient.from_config can now find the default config.json on Compute Instance when running sample notebooks.
1515
- Registries now assign managed tags to match registry's tags.
1616
- Adjust registry experimental tags and imports to avoid warning printouts for unrelated operations.
17+
- Make registry delete operation return an LROPoller, and change name to begin_delete.
1718
- Prevent registering an already existing environment that references conda file.
1819

1920
### Other Changes

sdk/ml/azure-ai-ml/azure/ai/ml/_restclient/v2022_10_01_preview/aio/operations/_registries_operations.py

+78-22
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from ... import models as _models
2323
from ..._vendor import _convert_request
24-
from ...operations._registries_operations import build_create_or_update_request_initial, build_delete_request, build_get_request, build_list_by_subscription_request, build_list_request, build_update_request
24+
from ...operations._registries_operations import build_create_or_update_request_initial, build_delete_request_initial, build_get_request, build_list_by_subscription_request, build_list_request, build_update_request
2525
T = TypeVar('T')
2626
ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, Dict[str, Any]], Any]]
2727

@@ -214,26 +214,12 @@ async def get_next(next_link=None):
214214
)
215215
list.metadata = {'url': "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/registries"} # type: ignore
216216

217-
@distributed_trace_async
218-
async def delete( # pylint: disable=inconsistent-return-statements
217+
async def _delete_initial( # pylint: disable=inconsistent-return-statements
219218
self,
220219
resource_group_name: str,
221220
registry_name: str,
222221
**kwargs: Any
223222
) -> None:
224-
"""Delete registry.
225-
226-
Delete registry.
227-
228-
:param resource_group_name: The name of the resource group. The name is case insensitive.
229-
:type resource_group_name: str
230-
:param registry_name: Name of registry. This is case-insensitive.
231-
:type registry_name: str
232-
:keyword callable cls: A custom type or function that will be passed the direct response
233-
:return: None, or the result of cls(response)
234-
:rtype: None
235-
:raises: ~azure.core.exceptions.HttpResponseError
236-
"""
237223
cls = kwargs.pop('cls', None) # type: ClsType[None]
238224
error_map = {
239225
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
@@ -243,12 +229,12 @@ async def delete( # pylint: disable=inconsistent-return-statements
243229
api_version = kwargs.pop('api_version', "2022-10-01-preview") # type: str
244230

245231

246-
request = build_delete_request(
232+
request = build_delete_request_initial(
247233
subscription_id=self._config.subscription_id,
248234
resource_group_name=resource_group_name,
249235
registry_name=registry_name,
250236
api_version=api_version,
251-
template_url=self.delete.metadata['url'],
237+
template_url=self._delete_initial.metadata['url'],
252238
)
253239
request = _convert_request(request)
254240
request.url = self._client.format_url(request.url)
@@ -262,14 +248,84 @@ async def delete( # pylint: disable=inconsistent-return-statements
262248

263249
if response.status_code not in [200, 202, 204]:
264250
map_error(status_code=response.status_code, response=response, error_map=error_map)
265-
error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, pipeline_response)
266-
raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat)
251+
raise HttpResponseError(response=response, error_format=ARMErrorFormat)
252+
253+
response_headers = {}
254+
if response.status_code == 202:
255+
response_headers['x-ms-async-operation-timeout']=self._deserialize('duration', response.headers.get('x-ms-async-operation-timeout'))
256+
response_headers['Location']=self._deserialize('str', response.headers.get('Location'))
257+
response_headers['Retry-After']=self._deserialize('int', response.headers.get('Retry-After'))
258+
267259

268260
if cls:
269-
return cls(pipeline_response, None, {})
261+
return cls(pipeline_response, None, response_headers)
262+
263+
_delete_initial.metadata = {'url': "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/registries/{registryName}"} # type: ignore
264+
270265

271-
delete.metadata = {'url': "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/registries/{registryName}"} # type: ignore
266+
@distributed_trace_async
267+
async def begin_delete( # pylint: disable=inconsistent-return-statements
268+
self,
269+
resource_group_name: str,
270+
registry_name: str,
271+
**kwargs: Any
272+
) -> AsyncLROPoller[None]:
273+
"""Delete registry.
274+
275+
Delete registry.
276+
277+
:param resource_group_name: The name of the resource group. The name is case insensitive.
278+
:type resource_group_name: str
279+
:param registry_name: Name of registry. This is case-insensitive.
280+
:type registry_name: str
281+
:keyword callable cls: A custom type or function that will be passed the direct response
282+
:keyword str continuation_token: A continuation token to restart a poller from a saved state.
283+
:keyword polling: By default, your polling method will be AsyncARMPolling. Pass in False for
284+
this operation to not poll, or pass in your own initialized polling object for a personal
285+
polling strategy.
286+
:paramtype polling: bool or ~azure.core.polling.AsyncPollingMethod
287+
:keyword int polling_interval: Default waiting time between two polls for LRO operations if no
288+
Retry-After header is present.
289+
:return: An instance of AsyncLROPoller that returns either None or the result of cls(response)
290+
:rtype: ~azure.core.polling.AsyncLROPoller[None]
291+
:raises: ~azure.core.exceptions.HttpResponseError
292+
"""
293+
api_version = kwargs.pop('api_version', "2022-10-01-preview") # type: str
294+
polling = kwargs.pop('polling', True) # type: Union[bool, AsyncPollingMethod]
295+
cls = kwargs.pop('cls', None) # type: ClsType[None]
296+
lro_delay = kwargs.pop(
297+
'polling_interval',
298+
self._config.polling_interval
299+
)
300+
cont_token = kwargs.pop('continuation_token', None) # type: Optional[str]
301+
if cont_token is None:
302+
raw_result = await self._delete_initial(
303+
resource_group_name=resource_group_name,
304+
registry_name=registry_name,
305+
api_version=api_version,
306+
cls=lambda x,y,z: x,
307+
**kwargs
308+
)
309+
kwargs.pop('error_map', None)
310+
311+
def get_long_running_output(pipeline_response):
312+
if cls:
313+
return cls(pipeline_response, None, {})
314+
315+
316+
if polling is True: polling_method = AsyncARMPolling(lro_delay, **kwargs)
317+
elif polling is False: polling_method = AsyncNoPolling()
318+
else: polling_method = polling
319+
if cont_token:
320+
return AsyncLROPoller.from_continuation_token(
321+
polling_method=polling_method,
322+
continuation_token=cont_token,
323+
client=self._client,
324+
deserialization_callback=get_long_running_output
325+
)
326+
return AsyncLROPoller(self._client, raw_result, get_long_running_output, polling_method)
272327

328+
begin_delete.metadata = {'url': "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/registries/{registryName}"} # type: ignore
273329

274330
@distributed_trace_async
275331
async def get(

sdk/ml/azure-ai-ml/azure/ai/ml/_restclient/v2022_10_01_preview/operations/_registries_operations.py

+79-22
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def build_list_request(
107107
)
108108

109109

110-
def build_delete_request(
110+
def build_delete_request_initial(
111111
subscription_id, # type: str
112112
resource_group_name, # type: str
113113
registry_name, # type: str
@@ -452,27 +452,13 @@ def get_next(next_link=None):
452452
)
453453
list.metadata = {'url': "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/registries"} # type: ignore
454454

455-
@distributed_trace
456-
def delete( # pylint: disable=inconsistent-return-statements
455+
def _delete_initial( # pylint: disable=inconsistent-return-statements
457456
self,
458457
resource_group_name, # type: str
459458
registry_name, # type: str
460459
**kwargs # type: Any
461460
):
462461
# type: (...) -> None
463-
"""Delete registry.
464-
465-
Delete registry.
466-
467-
:param resource_group_name: The name of the resource group. The name is case insensitive.
468-
:type resource_group_name: str
469-
:param registry_name: Name of registry. This is case-insensitive.
470-
:type registry_name: str
471-
:keyword callable cls: A custom type or function that will be passed the direct response
472-
:return: None, or the result of cls(response)
473-
:rtype: None
474-
:raises: ~azure.core.exceptions.HttpResponseError
475-
"""
476462
cls = kwargs.pop('cls', None) # type: ClsType[None]
477463
error_map = {
478464
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
@@ -482,12 +468,12 @@ def delete( # pylint: disable=inconsistent-return-statements
482468
api_version = kwargs.pop('api_version', "2022-10-01-preview") # type: str
483469

484470

485-
request = build_delete_request(
471+
request = build_delete_request_initial(
486472
subscription_id=self._config.subscription_id,
487473
resource_group_name=resource_group_name,
488474
registry_name=registry_name,
489475
api_version=api_version,
490-
template_url=self.delete.metadata['url'],
476+
template_url=self._delete_initial.metadata['url'],
491477
)
492478
request = _convert_request(request)
493479
request.url = self._client.format_url(request.url)
@@ -501,14 +487,85 @@ def delete( # pylint: disable=inconsistent-return-statements
501487

502488
if response.status_code not in [200, 202, 204]:
503489
map_error(status_code=response.status_code, response=response, error_map=error_map)
504-
error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, pipeline_response)
505-
raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat)
490+
raise HttpResponseError(response=response, error_format=ARMErrorFormat)
491+
492+
response_headers = {}
493+
if response.status_code == 202:
494+
response_headers['x-ms-async-operation-timeout']=self._deserialize('duration', response.headers.get('x-ms-async-operation-timeout'))
495+
response_headers['Location']=self._deserialize('str', response.headers.get('Location'))
496+
response_headers['Retry-After']=self._deserialize('int', response.headers.get('Retry-After'))
497+
506498

507499
if cls:
508-
return cls(pipeline_response, None, {})
500+
return cls(pipeline_response, None, response_headers)
501+
502+
_delete_initial.metadata = {'url': "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/registries/{registryName}"} # type: ignore
503+
504+
505+
@distributed_trace
506+
def begin_delete( # pylint: disable=inconsistent-return-statements
507+
self,
508+
resource_group_name, # type: str
509+
registry_name, # type: str
510+
**kwargs # type: Any
511+
):
512+
# type: (...) -> LROPoller[None]
513+
"""Delete registry.
514+
515+
Delete registry.
516+
517+
:param resource_group_name: The name of the resource group. The name is case insensitive.
518+
:type resource_group_name: str
519+
:param registry_name: Name of registry. This is case-insensitive.
520+
:type registry_name: str
521+
:keyword callable cls: A custom type or function that will be passed the direct response
522+
:keyword str continuation_token: A continuation token to restart a poller from a saved state.
523+
:keyword polling: By default, your polling method will be ARMPolling. Pass in False for this
524+
operation to not poll, or pass in your own initialized polling object for a personal polling
525+
strategy.
526+
:paramtype polling: bool or ~azure.core.polling.PollingMethod
527+
:keyword int polling_interval: Default waiting time between two polls for LRO operations if no
528+
Retry-After header is present.
529+
:return: An instance of LROPoller that returns either None or the result of cls(response)
530+
:rtype: ~azure.core.polling.LROPoller[None]
531+
:raises: ~azure.core.exceptions.HttpResponseError
532+
"""
533+
api_version = kwargs.pop('api_version', "2022-10-01-preview") # type: str
534+
polling = kwargs.pop('polling', True) # type: Union[bool, PollingMethod]
535+
cls = kwargs.pop('cls', None) # type: ClsType[None]
536+
lro_delay = kwargs.pop(
537+
'polling_interval',
538+
self._config.polling_interval
539+
)
540+
cont_token = kwargs.pop('continuation_token', None) # type: Optional[str]
541+
if cont_token is None:
542+
raw_result = self._delete_initial(
543+
resource_group_name=resource_group_name,
544+
registry_name=registry_name,
545+
api_version=api_version,
546+
cls=lambda x,y,z: x,
547+
**kwargs
548+
)
549+
kwargs.pop('error_map', None)
509550

510-
delete.metadata = {'url': "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/registries/{registryName}"} # type: ignore
551+
def get_long_running_output(pipeline_response):
552+
if cls:
553+
return cls(pipeline_response, None, {})
554+
555+
556+
if polling is True: polling_method = ARMPolling(lro_delay, **kwargs)
557+
elif polling is False: polling_method = NoPolling()
558+
else: polling_method = polling
559+
if cont_token:
560+
return LROPoller.from_continuation_token(
561+
polling_method=polling_method,
562+
continuation_token=cont_token,
563+
client=self._client,
564+
deserialization_callback=get_long_running_output
565+
)
566+
return LROPoller(self._client, raw_result, get_long_running_output, polling_method)
511567

568+
begin_delete.metadata = {'url': "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/registries/{registryName}"} # type: ignore
512569

513570
@distributed_trace
514571
def get(

sdk/ml/azure-ai-ml/azure/ai/ml/operations/_registry_operations.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,16 @@ def begin_create(
118118
registry: Registry,
119119
**kwargs: Dict,
120120
) -> LROPoller[Registry]:
121-
"""Create a new Azure Machine Learning Registry.
121+
"""Create a new Azure Machine Learning Registry,
122+
or try to update if it already exists.
122123
123-
Returns the registry if already exists.
124+
Note: Due to service limitations we have to sleep for
125+
an additional 30~45 seconds AFTER the LRO Poller concludes
126+
before the registry will be consistently deleted from the
127+
perspective of subsequent operations.
128+
If a deletion is required for subsequent operations to
129+
work properly, callers should implement that logic until the
130+
service has been fixed to return a reliable LRO.
124131
125132
:param registry: Registry definition.
126133
:type registry: Registry
@@ -140,16 +147,18 @@ def begin_create(
140147
return poller
141148

142149

143-
@monitor_with_activity(logger, "Registry.Delete", ActivityType.PUBLICAPI)
150+
@monitor_with_activity(logger, "Registry.BeginDelete", ActivityType.PUBLICAPI)
144151
@experimental
145-
def delete(self, *, name: str, **kwargs: Dict) -> None:
146-
"""Delete a registry. Returns nothing on a successful operation.
152+
def begin_delete(self, *, name: str, **kwargs: Dict) -> LROPoller[Registry]:
153+
"""Delete a registry if it exists. Returns nothing on a successful operation.
147154
148155
:param name: Name of the registry
149156
:type name: str
157+
:return: A poller to track the operation status.
158+
:rtype: LROPoller
150159
"""
151160
resource_group = kwargs.get("resource_group") or self._resource_group_name
152-
return self._operation.delete(
161+
return self._operation.begin_delete(
153162
resource_group_name=resource_group,
154163
registry_name=name,
155164
**self._init_kwargs,

sdk/ml/azure-ai-ml/swagger/machinelearningservices/resource-manager/Microsoft.MachineLearningServices/preview/2022-10-01-preview/registries.json

+21-2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"RegistryManagement"
112112
],
113113
"summary": "Delete registry.",
114+
"x-ms-long-running-operation": true,
114115
"operationId": "Registries_Delete",
115116
"produces": [
116117
"application/json"
@@ -142,9 +143,27 @@
142143
},
143144
"200": {
144145
"description": "Success"
145-
},
146+
},
146147
"202": {
147-
"description": "Success"
148+
"description": "Accepted",
149+
"headers": {
150+
"x-ms-async-operation-timeout": {
151+
"description": "Timeout for the client to use when polling the asynchronous operation.",
152+
"type": "string",
153+
"format": "duration"
154+
},
155+
"Location": {
156+
"description": "URI to poll for asynchronous operation result.",
157+
"type": "string"
158+
},
159+
"Retry-After": {
160+
"description": "Duration the client should wait between requests, in seconds.",
161+
"type": "integer",
162+
"format": "int32",
163+
"maximum": 600,
164+
"minimum": 10
165+
}
166+
}
148167
},
149168
"204": {
150169
"description": "No Content"

0 commit comments

Comments
 (0)