From fd7191de8705d4d06e141209bd6a2b9ab1d92fbb Mon Sep 17 00:00:00 2001 From: Heli Wang Date: Thu, 1 Oct 2020 10:51:58 -0700 Subject: [PATCH] Azure Communication Services - Phone Number Admin - Implementing long running operations for phone number search (#14157) * Implementing long running operations for phone number search --- .../communication/administration/__init__.py | 10 +- .../_phone_number_administration_client.py | 149 +++++++++++++---- .../communication/administration/_polling.py | 76 +++++++++ .../administration/aio/__init__.py | 4 +- ...hone_number_administration_client_async.py | 151 +++++++++++++++--- .../administration/aio/_polling_async.py | 76 +++++++++ 6 files changed, 408 insertions(+), 58 deletions(-) create mode 100644 sdk/communication/azure-communication-administration/azure/communication/administration/_polling.py create mode 100644 sdk/communication/azure-communication-administration/azure/communication/administration/aio/_polling_async.py diff --git a/sdk/communication/azure-communication-administration/azure/communication/administration/__init__.py b/sdk/communication/azure-communication-administration/azure/communication/administration/__init__.py index 2864e79e5e5e..ecd289cb703f 100644 --- a/sdk/communication/azure-communication-administration/azure/communication/administration/__init__.py +++ b/sdk/communication/azure-communication-administration/azure/communication/administration/__init__.py @@ -6,6 +6,7 @@ from ._communication_identity_client import CommunicationIdentityClient from ._phone_number_administration_client import PhoneNumberAdministrationClient +from ._polling import PhoneNumberPolling from ._identity._generated.models import ( CommunicationTokenRequest, @@ -16,6 +17,7 @@ AcquiredPhoneNumber, AcquiredPhoneNumbers, AreaCodes, + CreateSearchOptions, CreateSearchResponse, LocationOptionsQuery, LocationOptionsResponse, @@ -31,7 +33,7 @@ ReleaseResponse, UpdateNumberCapabilitiesResponse, UpdatePhoneNumberCapabilitiesResponse, - CreateSearchOptions + SearchStatus ) from ._shared.models import ( @@ -43,6 +45,7 @@ __all__ = [ 'CommunicationIdentityClient', 'PhoneNumberAdministrationClient', + 'PhoneNumberPolling', # from _identity 'CommunicationTokenRequest', @@ -52,6 +55,7 @@ 'AcquiredPhoneNumber', 'AcquiredPhoneNumbers', 'AreaCodes', + 'CreateSearchOptions', 'CreateSearchResponse', 'LocationOptionsQuery', 'LocationOptionsResponse', @@ -59,15 +63,15 @@ 'NumberUpdateCapabilities', 'PhoneNumberCountries', 'PhoneNumberEntities', - 'PhoneNumberRelease', 'PhoneNumberSearch', + 'PhoneNumberRelease', 'PhonePlanGroups', 'PhonePlansResponse', 'PstnConfiguration', 'ReleaseResponse', + 'SearchStatus', 'UpdateNumberCapabilitiesResponse', 'UpdatePhoneNumberCapabilitiesResponse', - 'CreateSearchOptions', # from _shared 'CommunicationUser', diff --git a/sdk/communication/azure-communication-administration/azure/communication/administration/_phone_number_administration_client.py b/sdk/communication/azure-communication-administration/azure/communication/administration/_phone_number_administration_client.py index edc0a5657235..ee393fac84ae 100644 --- a/sdk/communication/azure-communication-administration/azure/communication/administration/_phone_number_administration_client.py +++ b/sdk/communication/azure-communication-administration/azure/communication/administration/_phone_number_administration_client.py @@ -6,6 +6,8 @@ # ------------------------------------ from azure.core.tracing.decorator import distributed_trace from azure.core.paging import ItemPaged +from azure.core.polling import LROPoller +from ._polling import PhoneNumberPolling from ._phonenumber._generated._phone_number_administration_service\ import PhoneNumberAdministrationService as PhoneNumberAdministrationClientGen @@ -13,7 +15,6 @@ from ._phonenumber._generated.models import ( AcquiredPhoneNumbers, AreaCodes, - CreateSearchResponse, LocationOptionsResponse, NumberConfigurationResponse, NumberUpdateCapabilities, @@ -25,6 +26,7 @@ PhonePlansResponse, PstnConfiguration, ReleaseResponse, + SearchStatus, UpdateNumberCapabilitiesResponse, UpdatePhoneNumberCapabilitiesResponse ) @@ -405,21 +407,53 @@ def get_search_by_id( ) @distributed_trace - def create_search( + def begin_create_search( self, **kwargs # type: Any ): - # type: (...) -> CreateSearchResponse - """Creates a phone number search. + # type: (...) -> LROPoller + """Begins creating a phone number search. + Caller must provide either body, or continuation_token keywords to use the method. + If both body and continuation_token are specified, only continuation_token will be used to + restart a poller from a saved state, and keyword body will be ignored. :keyword azure.communication.administration.CreateSearchOptions body: - An optional parameter for defining the search options. - The default is None. - :rtype: ~azure.communication.administration.CreateSearchResponse + A parameter for defining the search options. + :keyword str continuation_token: A continuation token to restart a poller from a saved state. + :rtype: ~azure.core.polling.LROPoller[~azure.communication.administration.PhoneNumberSearch] """ - return self._phone_number_administration_client.phone_number_administration.create_search( + cont_token = kwargs.pop('continuation_token', None) # type: Optional[str] + + search_polling = PhoneNumberPolling( + is_terminated=lambda status: status in [ + SearchStatus.Reserved, + SearchStatus.Expired, + SearchStatus.Success, + SearchStatus.Cancelled, + SearchStatus.Error + ] + ) + + if cont_token is not None: + return LROPoller.from_continuation_token( + polling_method=search_polling, + continuation_token=cont_token, + client=self._phone_number_administration_client.phone_number_administration + ) + + if "body" not in kwargs: + raise ValueError("Either kwarg 'body' or 'continuation_token' needs to be specified") + + create_search_response = self._phone_number_administration_client.phone_number_administration.create_search( **kwargs ) + initial_state = self._phone_number_administration_client.phone_number_administration.get_search_by_id( + search_id=create_search_response.search_id + ) + return LROPoller(client=self._phone_number_administration_client.phone_number_administration, + initial_response=initial_state, + deserialization_callback=None, + polling_method=search_polling) @distributed_trace def list_all_searches( @@ -440,37 +474,96 @@ def list_all_searches( ) @distributed_trace - def cancel_search( + def begin_cancel_search( self, - search_id, # type: str **kwargs # type: Any ): - # type: (...) -> None - """Cancels the search. This means existing numbers in the search will be made available. - - :param search_id: The search id to be canceled. - :type search_id: str - :rtype: None + # type: (...) -> LROPoller + """Begins the phone number search cancellation. + Caller must provide either search_id, or continuation_token keywords to use the method. + If both body and continuation_token are specified, only continuation_token will be used to + restart a poller from a saved state, and keyword search_id will be ignored. + + :keyword str search_id: The search id to be canceled. + :keyword str continuation_token: A continuation token to restart a poller from a saved state. + :rtype: ~azure.core.polling.LROPoller[~azure.communication.administration.PhoneNumberSearch] """ - return self._phone_number_administration_client.phone_number_administration.cancel_search( + cont_token = kwargs.pop('continuation_token', None) # type: Optional[str] + + search_polling = PhoneNumberPolling( + is_terminated=lambda status: status in [ + SearchStatus.Expired, + SearchStatus.Cancelled, + SearchStatus.Error + ] + ) + + if cont_token is not None: + return LROPoller.from_continuation_token( + polling_method=search_polling, + continuation_token=cont_token, + client=self._phone_number_administration_client.phone_number_administration + ) + + search_id = kwargs.pop('search_id', None) # type: str + if search_id is None: + raise ValueError("Either kwarg 'search_id' or 'continuation_token' needs to be specified") + + self._phone_number_administration_client.phone_number_administration.cancel_search( search_id, **kwargs ) + initial_state = self._phone_number_administration_client.phone_number_administration.get_search_by_id( + search_id=search_id + ) + return LROPoller(client=self._phone_number_administration_client.phone_number_administration, + initial_response=initial_state, + deserialization_callback=None, + polling_method=search_polling) @distributed_trace - def purchase_search( - self, - search_id, # type: str - **kwargs # type: Any + def begin_purchase_search( + self, + **kwargs # type: Any ): - # type: (...) -> None - """Purchases the phone number search. - - :param search_id: The search id to be purchased. - :type search_id: str - :rtype: None + # type: (...) -> LROPoller + """Begins the phone number search purchase. + Caller must provide either search_id, or continuation_token keywords to use the method. + If both body and continuation_token are specified, only continuation_token will be used to + restart a poller from a saved state, and keyword search_id will be ignored. + + :keyword str search_id: The search id to be purchased. + :keyword str continuation_token: A continuation token to restart a poller from a saved state. + :rtype: ~azure.core.polling.LROPoller[~azure.communication.administration.PhoneNumberSearch] """ - return self._phone_number_administration_client.phone_number_administration.purchase_search( + cont_token = kwargs.pop('continuation_token', None) # type: Optional[str] + + search_polling = PhoneNumberPolling( + is_terminated=lambda status: status in [ + SearchStatus.Success, + SearchStatus.Expired, + SearchStatus.Cancelled, + SearchStatus.Error + ] + ) + + if cont_token is not None: + return LROPoller.from_continuation_token( + polling_method=search_polling, + continuation_token=cont_token, + client=self._phone_number_administration_client.phone_number_administration + ) + + search_id = kwargs.pop('search_id') # type: str + + self._phone_number_administration_client.phone_number_administration.purchase_search( search_id, **kwargs ) + initial_state = self._phone_number_administration_client.phone_number_administration.get_search_by_id( + search_id=search_id + ) + return LROPoller(client=self._phone_number_administration_client.phone_number_administration, + initial_response=initial_state, + deserialization_callback=None, + polling_method=search_polling) diff --git a/sdk/communication/azure-communication-administration/azure/communication/administration/_polling.py b/sdk/communication/azure-communication-administration/azure/communication/administration/_polling.py new file mode 100644 index 000000000000..05555abc171e --- /dev/null +++ b/sdk/communication/azure-communication-administration/azure/communication/administration/_polling.py @@ -0,0 +1,76 @@ +# pylint: disable=W0231 +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import base64 +import time +from typing import Union +from functools import partial +import pickle + +from azure.core.polling import ( + PollingMethod +) +from ._phonenumber._generated.models import ( + PhoneNumberSearch, + PhoneNumberRelease +) + +class PhoneNumberPolling(PollingMethod): + def __init__(self, is_terminated, interval=5): + self._response = None + self._client = None + self._query_status = None + self._is_terminated = is_terminated + self._polling_interval = interval + + def _update_status(self): + # type: () -> None + if self._query_status is None: + raise Exception("this poller has not been initialized") + self._response = self._query_status() + + def initialize(self, client, initial_response, _): + # type: (Any, Any, Callable) -> None + self._client = client + self._response = initial_response + self._query_status = partial(self._client.get_search_by_id, search_id=initial_response.search_id) + + def run(self): + # type: () -> None + while not self.finished(): + self._update_status() + if not self.finished(): + time.sleep(self._polling_interval) + + def finished(self): + # type: () -> bool + if self._response.status is None: + return False + return self._is_terminated(self._response.status) + + def resource(self): + # type: () -> Union[PhoneNumberSearch, PhoneNumberRelease] + if not self.finished(): + return None + return self._response + + def status(self): + # type: () -> str + return self._response.status + + def get_continuation_token(self): + # type() -> str + return base64.b64encode(pickle.dumps(self._response)).decode('ascii') + + @classmethod + def from_continuation_token(cls, continuation_token, **kwargs): + # type(str, Any) -> Tuple + try: + client = kwargs["client"] + except KeyError: + raise ValueError("Kwarg 'client' needs to be specified") + initial_response = pickle.loads(base64.b64decode(continuation_token)) # nosec + return client, initial_response, None diff --git a/sdk/communication/azure-communication-administration/azure/communication/administration/aio/__init__.py b/sdk/communication/azure-communication-administration/azure/communication/administration/aio/__init__.py index f647dfa8f506..dc793b7fa747 100644 --- a/sdk/communication/azure-communication-administration/azure/communication/administration/aio/__init__.py +++ b/sdk/communication/azure-communication-administration/azure/communication/administration/aio/__init__.py @@ -1,7 +1,9 @@ from ._communication_identity_client_async import CommunicationIdentityClient from ._phone_number_administration_client_async import PhoneNumberAdministrationClient +from ._polling_async import PhoneNumberPollingAsync __all__ = [ 'CommunicationIdentityClient', - 'PhoneNumberAdministrationClient' + 'PhoneNumberAdministrationClient', + 'PhoneNumberPollingAsync' ] diff --git a/sdk/communication/azure-communication-administration/azure/communication/administration/aio/_phone_number_administration_client_async.py b/sdk/communication/azure-communication-administration/azure/communication/administration/aio/_phone_number_administration_client_async.py index 50637b8ce729..8cfd19cb7a0f 100644 --- a/sdk/communication/azure-communication-administration/azure/communication/administration/aio/_phone_number_administration_client_async.py +++ b/sdk/communication/azure-communication-administration/azure/communication/administration/aio/_phone_number_administration_client_async.py @@ -5,10 +5,14 @@ # Licensed under the MIT License. # ------------------------------------ from typing import Dict, List + from azure.core.async_paging import AsyncItemPaged from azure.core.tracing.decorator import distributed_trace from azure.core.tracing.decorator_async import distributed_trace_async +from azure.core.polling import AsyncLROPoller + from .._version import SDK_MONIKER +from ._polling_async import PhoneNumberPollingAsync from .._phonenumber._generated.aio._phone_number_administration_service_async\ import PhoneNumberAdministrationService as PhoneNumberAdministrationClientGen @@ -16,7 +20,6 @@ from .._phonenumber._generated.models import ( AcquiredPhoneNumbers, AreaCodes, - CreateSearchResponse, LocationOptionsResponse, NumberConfigurationResponse, NumberUpdateCapabilities, @@ -28,6 +31,7 @@ PhonePlansResponse, PstnConfiguration, ReleaseResponse, + SearchStatus, UpdateNumberCapabilitiesResponse, UpdatePhoneNumberCapabilitiesResponse ) @@ -412,22 +416,55 @@ async def get_search_by_id( ) @distributed_trace_async - async def create_search( + async def begin_create_search( self, **kwargs # type: Any ): - # type: (...) -> CreateSearchResponse - """Creates a phone number search. + # type: (...) -> AsyncLROPoller + """Begins creating a phone number search. + Caller must provide either body, or continuation_token keywords to use the method. + If both body and continuation_token are specified, only continuation_token will be used to + restart a poller from a saved state, and keyword body will be ignored. :keyword azure.communication.administration.CreateSearchOptions body: - An optional parameter for defining the search options. - The default is None. - :rtype: ~azure.communication.administration.CreateSearchResponse + A parameter for defining the search options. + :keyword str continuation_token: A continuation token to restart a poller from a saved state. + :rtype: ~azure.core.polling.AsyncLROPoller[~azure.communication.administration.PhoneNumberSearch] """ - return await self._phone_number_administration_client.phone_number_administration.create_search( - **kwargs + cont_token = kwargs.pop('continuation_token', None) # type: Optional[str] + + search_polling = PhoneNumberPollingAsync( + is_terminated=lambda status: status in [ + SearchStatus.Reserved, + SearchStatus.Expired, + SearchStatus.Success, + SearchStatus.Cancelled, + SearchStatus.Error + ] ) + if cont_token is not None: + return AsyncLROPoller.from_continuation_token( + polling_method=search_polling, + continuation_token=cont_token, + client=self._phone_number_administration_client.phone_number_administration + ) + + if "body" not in kwargs: + raise ValueError("Either kwarg 'body' or 'continuation_token' needs to be specified") + + create_search_response = await self._phone_number_administration_client.\ + phone_number_administration.create_search( + **kwargs + ) + initial_state = await self._phone_number_administration_client.phone_number_administration.get_search_by_id( + search_id=create_search_response.search_id + ) + return AsyncLROPoller(client=self._phone_number_administration_client.phone_number_administration, + initial_response=initial_state, + deserialization_callback=None, + polling_method=search_polling) + @distributed_trace def list_all_searches( self, @@ -447,40 +484,102 @@ def list_all_searches( ) @distributed_trace_async - async def cancel_search( + async def begin_cancel_search( self, - search_id, # type: str **kwargs # type: Any ): - # type: (...) -> None - """Cancels the search. This means existing numbers in the search will be made available. - - :param search_id: The search id to be canceled. - :type search_id: str - :rtype: None + # type: (...) -> AsyncLROPoller + """Begins the phone number search cancellation. + Caller must provide either search_id, or continuation_token keywords to use the method. + If both body and continuation_token are specified, only continuation_token will be used to + restart a poller from a saved state, and keyword search_id will be ignored. + + :keyword str search_id: The search id to be canceled. + :keyword str continuation_token: A continuation token to restart a poller from a saved state. + :rtype: ~azure.core.polling.AsyncLROPoller[~azure.communication.administration.PhoneNumberSearch] """ - return await self._phone_number_administration_client.phone_number_administration.cancel_search( + cont_token = kwargs.pop('continuation_token', None) # type: Optional[str] + + search_polling = PhoneNumberPollingAsync( + is_terminated=lambda status: status in [ + SearchStatus.Expired, + SearchStatus.Cancelled, + SearchStatus.Error + ] + ) + + if cont_token is not None: + return AsyncLROPoller.from_continuation_token( + polling_method=search_polling, + continuation_token=cont_token, + client=self._phone_number_administration_client.phone_number_administration + ) + + search_id = kwargs.pop('search_id', None) # type: str + if search_id is None: + raise ValueError("Either kwarg 'search_id' or 'continuation_token' needs to be specified") + + await self._phone_number_administration_client.phone_number_administration.cancel_search( search_id, **kwargs ) + initial_state = await self._phone_number_administration_client.phone_number_administration.get_search_by_id( + search_id=search_id + ) + return AsyncLROPoller(client=self._phone_number_administration_client.phone_number_administration, + initial_response=initial_state, + deserialization_callback=None, + polling_method=search_polling) @distributed_trace_async - async def purchase_search( + async def begin_purchase_search( self, - search_id, # type: str **kwargs # type: Any ): - # type: (...) -> None - """Purchases the phone number search. - :param search_id: The search id to be purchased. - :type search_id: str - :rtype: None + # type: (...) -> AsyncLROPoller + """Begins the phone number search purchase. + Caller must provide either search_id, or continuation_token keywords to use the method. + If both body and continuation_token are specified, only continuation_token will be used to + restart a poller from a saved state, and keyword search_id will be ignored. + + :keyword str search_id: The search id to be purchased. + :keyword str continuation_token: A continuation token to restart a poller from a saved state. + :rtype: ~azure.core.polling.AsyncLROPoller[~azure.communication.administration.PhoneNumberSearch] """ - return await self._phone_number_administration_client.phone_number_administration.purchase_search( + cont_token = kwargs.pop('continuation_token', None) # type: Optional[str] + + search_polling = PhoneNumberPollingAsync( + is_terminated=lambda status: status in [ + SearchStatus.Success, + SearchStatus.Expired, + SearchStatus.Cancelled, + SearchStatus.Error + ] + ) + + if cont_token is not None: + return AsyncLROPoller.from_continuation_token( + polling_method=search_polling, + continuation_token=cont_token, + client=self._phone_number_administration_client.phone_number_administration + ) + + search_id = kwargs.pop('search_id', None) # type: str + if search_id is None: + raise ValueError("Either kwarg 'search_id' or 'continuation_token' needs to be specified") + + await self._phone_number_administration_client.phone_number_administration.purchase_search( search_id, **kwargs ) + initial_state = await self._phone_number_administration_client.phone_number_administration.get_search_by_id( + search_id=search_id + ) + return AsyncLROPoller(client=self._phone_number_administration_client.phone_number_administration, + initial_response=initial_state, + deserialization_callback=None, + polling_method=search_polling) async def __aenter__(self) -> "PhoneNumberAdministrationClient": await self._phone_number_administration_client.__aenter__() diff --git a/sdk/communication/azure-communication-administration/azure/communication/administration/aio/_polling_async.py b/sdk/communication/azure-communication-administration/azure/communication/administration/aio/_polling_async.py new file mode 100644 index 000000000000..06000f10040b --- /dev/null +++ b/sdk/communication/azure-communication-administration/azure/communication/administration/aio/_polling_async.py @@ -0,0 +1,76 @@ +# pylint: disable=W0231 +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import asyncio +from typing import Union +import base64 +from functools import partial + +from azure.core.polling import AsyncPollingMethod + +from .._phonenumber._generated.models import ( + PhoneNumberSearch, + PhoneNumberRelease +) + +class PhoneNumberPollingAsync(AsyncPollingMethod): + def __init__(self, is_terminated, interval=5): + self._response = None + self._client = None + self._query_status = None + self._is_terminated = is_terminated + self._polling_interval = interval + + async def _update_status(self): + # type: () -> None + if self._query_status is None: + raise Exception("this poller has not been initialized") + self._response = await self._query_status() + + def initialize(self, client, initial_response, _): + # type: (Any, Any, Callable) -> None + self._client = client + self._response = initial_response + self._query_status = partial(self._client.get_search_by_id, search_id=initial_response.search_id) + + async def run(self): + # type: () -> None + while not self.finished(): + await self._update_status() + if not self.finished(): + await asyncio.sleep(self._polling_interval) + + def finished(self): + # type: () -> bool + if self._response.status is None: + return False + return self._is_terminated(self._response.status) + + def resource(self): + # type: () -> Union[PhoneNumberSearch, PhoneNumberRelease] + if not self.finished(): + return None + return self._response + + def status(self): + # type: () -> str + return self._response.status + + def get_continuation_token(self): + # type() -> str + import pickle + return base64.b64encode(pickle.dumps(self._response)).decode('ascii') + + @classmethod + def from_continuation_token(cls, continuation_token, **kwargs): + # type(str, Any) -> Tuple + try: + client = kwargs["client"] + except KeyError: + raise ValueError("Kwarg 'client' needs to be specified") + import pickle + initial_response = pickle.loads(base64.b64decode(continuation_token)) # nosec + return client, initial_response, None