Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Types in poller #29228

Merged
merged 60 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
cfafd30
Types in poller
lmazuel Mar 8, 2023
dbe3d32
Black it right
lmazuel Mar 17, 2023
6fc87de
Saving this week's work
lmazuel Mar 18, 2023
1facfcf
Move typing
lmazuel Mar 20, 2023
7f7f560
Split base polling in two
lmazuel Mar 27, 2023
3f041d8
Merge branch 'main' into polling_types
lmazuel Mar 27, 2023
7d0acfb
Merge branch 'main' into polling_types
lmazuel Apr 14, 2023
1316897
Typing fixes
lmazuel Apr 14, 2023
f3c42e9
Typing update
lmazuel Apr 14, 2023
bfd8432
Black
lmazuel Apr 14, 2023
bae1d5f
Unecessary Generic
lmazuel Apr 14, 2023
e494ee0
Stringify types
lmazuel Apr 14, 2023
c7d8aa8
Fix import
lmazuel Apr 15, 2023
bbe53f2
Spellcheck
lmazuel Apr 15, 2023
e39cccd
Weird typo...
lmazuel Apr 15, 2023
61e1371
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel Apr 21, 2023
03a6d5f
PyLint
lmazuel Apr 21, 2023
bd00b83
More types
lmazuel Apr 22, 2023
e149c70
Update sdk/core/azure-core/azure/core/polling/async_base_polling.py
lmazuel May 2, 2023
368572e
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel May 4, 2023
ef9f186
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel May 22, 2023
bdff940
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel May 23, 2023
11a36f2
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel May 24, 2023
27a6045
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel May 24, 2023
34a6340
Missing type
lmazuel May 24, 2023
80b422c
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel Jun 13, 2023
6465b24
Typing of the day
lmazuel Jun 15, 2023
17787c3
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel Jun 17, 2023
2a5f3ac
Re-enable verifytypes
lmazuel Jun 17, 2023
b3fb232
Simplify the expectations async pipeline has on the response
lmazuel Jun 19, 2023
4f94c88
Async Cxt Manager
lmazuel Jun 19, 2023
ca61aa0
Final Typing?
lmazuel Jun 20, 2023
c82c946
More covariant
lmazuel Jun 20, 2023
4dd11c7
Upside down
lmazuel Jun 20, 2023
a049070
Fix tests
lmazuel Jun 20, 2023
e55ffbd
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel Jun 20, 2023
bf3aa73
Messed up merge
lmazuel Jun 20, 2023
7644c15
Pylint
lmazuel Jun 20, 2023
f4f825e
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel Jun 21, 2023
d229aae
Better Typing
lmazuel Jun 22, 2023
7a8c137
Final typing?
lmazuel Jun 22, 2023
48872c9
Pylint
lmazuel Jun 22, 2023
36e1e78
Simplify translation typing for now
lmazuel Jun 22, 2023
33e6125
Fix backcompat with azure-mgmt-core
lmazuel Jun 23, 2023
8aea6b1
Revert renaming private methods
lmazuel Jun 23, 2023
aa9c89c
Black
lmazuel Jun 23, 2023
188d7ab
Feedback from @kristapratico
lmazuel Jun 26, 2023
dee0db5
Docstrings part 1
lmazuel Jun 26, 2023
9127c89
Polling pylint part 2
lmazuel Jun 26, 2023
1467a21
Black
lmazuel Jun 26, 2023
5b83d9c
All LRO impl should use TypeVar
lmazuel Jun 29, 2023
f4fde67
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel Jun 29, 2023
f559db9
Feedback
lmazuel Jul 5, 2023
5035836
Convert some Anyu after feedback
lmazuel Jul 5, 2023
a7ab020
Merge remote-tracking branch 'origin/main' into polling_types
lmazuel Jul 5, 2023
e7c52a0
Spellcheck
lmazuel Jul 5, 2023
3501c22
Black
lmazuel Jul 6, 2023
c5b06c1
Update sdk/core/azure-core/azure/core/polling/_async_poller.py
kashifkhan Jul 6, 2023
d620445
Update sdk/core/azure-core/azure/core/polling/_async_poller.py
kashifkhan Jul 6, 2023
c7a6d93
Update sdk/core/azure-core/azure/core/polling/_poller.py
kashifkhan Jul 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
"ints",
"iohttp",
"IOHTTP",
"IOLRO",
"inprogress",
"ipconfiguration",
"ipconfigurations",
Expand Down
13 changes: 11 additions & 2 deletions sdk/core/azure-core/azure/core/pipeline/policies/_universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
import uuid
from typing import IO, cast, Union, Optional, AnyStr, Dict, MutableMapping
import urllib.parse
from typing_extensions import Protocol
from typing_extensions import Protocol, runtime_checkable

from azure.core import __version__ as azcore_version
from azure.core.exceptions import DecodeError, raise_with_traceback
Expand All @@ -49,15 +49,17 @@
_LOGGER = logging.getLogger(__name__)


@runtime_checkable
class HTTPRequestType(Protocol):
"""Protocol compatible with new rest request and legacy transport request"""

headers: MutableMapping[str, str]
url: str
method: str
body: bytes
body: Optional[Union[bytes, Dict[str, Union[str, int]]]]


@runtime_checkable
class HTTPResponseType(Protocol):
"""Protocol compatible with new rest response and legacy transport response"""

Expand All @@ -73,9 +75,16 @@ def status_code(self) -> int:
def content_type(self) -> Optional[str]:
...

@property
def request(self) -> HTTPRequestType:
...

def text(self, encoding: Optional[str] = None) -> str:
...

def body(self) -> bytes:
...


class HeadersPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]):
"""A simple policy that sends the given headers with the request.
Expand Down
13 changes: 8 additions & 5 deletions sdk/core/azure-core/azure/core/pipeline/policies/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,24 @@
# --------------------------------------------------------------------------
import datetime
import email.utils
from typing import Optional

from ...utils._utils import _FixedOffset, case_insensitive_dict


def _parse_http_date(text):
def _parse_http_date(text: str) -> datetime.datetime:
"""Parse a HTTP date format into datetime."""
parsed_date = email.utils.parsedate_tz(text)
return datetime.datetime(*parsed_date[:6], tzinfo=_FixedOffset(parsed_date[9] / 60))
return datetime.datetime(*parsed_date[:6], tzinfo=_FixedOffset(parsed_date[9] / 60)) # type: ignore
lmazuel marked this conversation as resolved.
Show resolved Hide resolved


def parse_retry_after(retry_after):
def parse_retry_after(retry_after: str) -> float:
"""Helper to parse Retry-After and get value in seconds.

:param str retry_after: Retry-After header
:rtype: int
:rtype: float
lmazuel marked this conversation as resolved.
Show resolved Hide resolved
"""
delay: float # Using the Mypy recommendation to use float for "int or float"
try:
delay = int(retry_after)
except ValueError:
Expand All @@ -49,7 +52,7 @@ def parse_retry_after(retry_after):
return max(0, delay)


def get_retry_after(response):
def get_retry_after(response) -> Optional[float]:
"""Get the value of Retry-After in seconds.

:param response: The PipelineResponse object
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/azure-core/azure/core/pipeline/transport/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def open(self):
def close(self):
"""Close the session if it is not externally owned."""

def sleep(self, duration): # pylint: disable=no-self-use
def sleep(self, duration: float) -> None: # pylint: disable=no-self-use
lmazuel marked this conversation as resolved.
Show resolved Hide resolved
time.sleep(duration)


Expand Down
9 changes: 7 additions & 2 deletions sdk/core/azure-core/azure/core/polling/_async_poller.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ async def run(self): # pylint:disable=invalid-overridden-method
"""


async def async_poller(client, initial_response, deserialization_callback, polling_method):
async def async_poller(
client: Any,
initial_response: Any,
deserialization_callback: Callable[[Any], PollingReturnType],
polling_method: AsyncPollingMethod[PollingReturnType],
):
"""Async Poller for long running operations.

.. deprecated:: 1.5.0
Expand Down Expand Up @@ -109,7 +114,7 @@ def __init__(
self,
client: Any,
initial_response: Any,
deserialization_callback: Callable,
deserialization_callback: Callable[[Any], PollingReturnType],
polling_method: AsyncPollingMethod[PollingReturnType],
):
self._polling_method = polling_method
Expand Down
23 changes: 14 additions & 9 deletions sdk/core/azure-core/azure/core/polling/_poller.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,18 @@ def from_continuation_token(cls, continuation_token: str, **kwargs) -> Tuple[Any
raise TypeError("Polling method '{}' doesn't support from_continuation_token".format(cls.__name__))


class NoPolling(PollingMethod):
class NoPolling(PollingMethod[PollingReturnType]):
"""An empty poller that returns the deserialized initial response."""

_deserialization_callback: Callable[[Any], PollingReturnType]
"""Deserialization callback passed during initialization"""

def __init__(self):
self._initial_response = None
self._deserialization_callback = None

def initialize(self, _: Any, initial_response: Any, deserialization_callback: Callable) -> None:
def initialize(
self, _: Any, initial_response: Any, deserialization_callback: Callable[[Any], PollingReturnType]
) -> None:
self._initial_response = initial_response
self._deserialization_callback = deserialization_callback

Expand All @@ -92,7 +96,7 @@ def finished(self) -> bool:
"""
return True

def resource(self) -> Any:
def resource(self) -> PollingReturnType:
return self._deserialization_callback(self._initial_response)

def get_continuation_token(self) -> str:
Expand Down Expand Up @@ -130,7 +134,7 @@ def __init__(
self,
client: Any,
initial_response: Any,
deserialization_callback: Callable,
deserialization_callback: Callable[[Any], PollingReturnType],
polling_method: PollingMethod[PollingReturnType],
) -> None:
self._callbacks: List[Callable] = []
Expand All @@ -147,10 +151,11 @@ def __init__(

# Prepare thread execution
self._thread = None
self._done = None
self._done = threading.Event()
self._exception = None
if not self._polling_method.finished():
self._done = threading.Event()
if self._polling_method.finished():
self._done.set()
else:
self._thread = threading.Thread(
target=with_current_context(self._start),
name="LROPoller({})".format(uuid.uuid4()),
Expand Down Expand Up @@ -266,7 +271,7 @@ def add_done_callback(self, func: Callable) -> None:
argument, a completed LongRunningOperation.
"""
# Still use "_done" and not "done", since CBs are executed inside the thread.
if self._done is None or self._done.is_set():
if self._done.is_set():
func(self._polling_method)
# Let's add them still, for consistency (if you wish to access to it for some reasons)
self._callbacks.append(func)
Expand Down
65 changes: 53 additions & 12 deletions sdk/core/azure-core/azure/core/polling/async_base_polling.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,60 @@
# IN THE SOFTWARE.
#
# --------------------------------------------------------------------------
from typing import TYPE_CHECKING, TypeVar, cast
from ..exceptions import HttpResponseError
from .base_polling import (
_failed,
BadStatus,
BadResponse,
OperationFailed,
LROBasePolling,
_SansIOLROBasePolling,
_raise_if_bad_http_status_and_method,
)
from ._async_poller import AsyncPollingMethod
from ..pipeline._tools import is_rest


if TYPE_CHECKING:
from azure.core import AsyncPipelineClient
from azure.core.pipeline import PipelineResponse
from azure.core.pipeline.transport import AsyncHttpTransport
from azure.core._pipeline_client_async import _AsyncContextManagerCloseable
from azure.core.pipeline.policies._universal import HTTPRequestType, HTTPResponseType

AsyncHTTPResponseType = TypeVar("AsyncHTTPResponseType", bound="_AsyncContextManagerCloseable")
AsyncPipelineResponseType = PipelineResponse[HTTPRequestType, AsyncHTTPResponseType]

PollingReturnType = TypeVar("PollingReturnType")

__all__ = ["AsyncLROBasePolling"]


class AsyncLROBasePolling(LROBasePolling):
"""A subclass or LROBasePolling that redefine "run" as async."""
class AsyncLROBasePolling(
_SansIOLROBasePolling[PollingReturnType, "AsyncPipelineClient[HTTPRequestType, AsyncHTTPResponseType]"],
AsyncPollingMethod[PollingReturnType],
):
"""A base LRO async poller.

This assumes a basic flow:
- I analyze the response to decide the polling approach
- I poll
- I ask the final resource depending of the polling approach

async def run(self): # pylint:disable=invalid-overridden-method
If your polling need are more specific, you could implement a PollingMethod directly
"""

_initial_response: "AsyncPipelineResponseType"
"""Store the initial response."""

_pipeline_response: "AsyncPipelineResponseType"
"""Store the latest received HTTP response, initialized by the first answer."""

@property
def _transport(self) -> "AsyncHttpTransport":
return self._client._pipeline._transport # pylint: disable=protected-access

async def run(self) -> None:
try:
await self._poll()

Expand All @@ -59,7 +95,7 @@ async def run(self): # pylint:disable=invalid-overridden-method
except OperationFailed as err:
raise HttpResponseError(response=self._pipeline_response.http_response, error=err)

async def _poll(self): # pylint:disable=invalid-overridden-method
async def _poll(self) -> None:
"""Poll status of operation so long as operation is incomplete and
we have an endpoint to query.

Expand All @@ -83,23 +119,23 @@ async def _poll(self): # pylint:disable=invalid-overridden-method
self._pipeline_response = await self.request_status(final_get_url)
_raise_if_bad_http_status_and_method(self._pipeline_response.http_response)

async def _sleep(self, delay): # pylint:disable=invalid-overridden-method
async def _sleep(self, delay: float):
lmazuel marked this conversation as resolved.
Show resolved Hide resolved
await self._transport.sleep(delay)

async def _delay(self): # pylint:disable=invalid-overridden-method
async def _delay(self):
"""Check for a 'retry-after' header to set timeout,
otherwise use configured timeout.
"""
delay = self._extract_delay()
await self._sleep(delay)

async def update_status(self): # pylint:disable=invalid-overridden-method
async def update_status(self):
"""Update the current status of the LRO."""
self._pipeline_response = await self.request_status(self._operation.get_polling_url())
_raise_if_bad_http_status_and_method(self._pipeline_response.http_response)
self._status = self._operation.get_status(self._pipeline_response)

async def request_status(self, status_link): # pylint:disable=invalid-overridden-method
async def request_status(self, status_link: str) -> "AsyncPipelineResponseType":
"""Do a simple GET to this status link.

This method re-inject 'x-ms-client-request-id'.
Expand All @@ -117,12 +153,17 @@ async def request_status(self, status_link): # pylint:disable=invalid-overridde
from azure.core.rest import HttpRequest as RestHttpRequest

request = RestHttpRequest("GET", status_link)
return await self._client.send_request(request, _return_pipeline_response=True, **self._operation_config)
# Need a cast, as "_return_pipeline_response" mutate the return type, and that return type is not
# declared in the typing of "send_request"
return cast(
"AsyncPipelineResponseType",
await self._client.send_request(request, _return_pipeline_response=True, **self._operation_config),
)
# if I am a azure.core.pipeline.transport.HttpResponse
request = self._client.get(status_link)
legacy_request = self._client.get(status_link)

return await self._client._pipeline.run( # pylint: disable=protected-access
request, stream=False, **self._operation_config
legacy_request, stream=False, **self._operation_config
)


Expand Down
Loading