Skip to content
This repository was archived by the owner on Apr 10, 2024. It is now read-only.

Commit 35dc0e1

Browse files
authored
Async (#71)
* Make AzureOperationPoller almost a asyncio.coroutine * Split polling and initial sent into two parts * AzureOperationPoller is now a Future * Switch to async/await + test on real SDK * Implement async with AsyncPoller * Remove async hack * Update to latest general msrestazure update * Pipeline changes for msrestazure * Update dep * Fix incorrect last master merge * Adapt tests to msrest 0.6.2 * Add async extra in setup * Ignore async tests on Python < 3.5 * Ignore cov errors from async * Remove async extra * msrestazure 0.6.0
1 parent bfd0b7b commit 35dc0e1

File tree

12 files changed

+611
-20
lines changed

12 files changed

+611
-20
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ mock = {markers="python_version<='2.7'"}
1111
httpretty = '*'
1212
pytest = '*'
1313
pytest-cov = '*'
14+
pytest-asyncio = {version = "*", markers="python_version >= '3.5'"}
1415
pylint = '*'

README.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ To install:
2020
Release History
2121
---------------
2222

23+
2018-12-17 Version 0.6.0
24+
++++++++++++++++++++++++
25+
26+
**Features**
27+
28+
- Implementation of LRO async, based on msrest 0.6.x series (*experimental*)
29+
30+
**Disclaimer**
31+
32+
- This version contains no direct breaking changes, but is bumped to 0.6.x since it requires a breaking change version of msrest.
33+
34+
Thanks to @gison93 for his documentation contribution
35+
2336
2018-11-01 Version 0.5.1
2437
++++++++++++++++++++++++
2538

dev_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ mock;python_version<="2.7"
33
httpretty
44
pytest
55
pytest-cov
6+
pytest-asyncio;python_full_version>="3.5.2"

msrestazure/azure_exceptions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,15 +203,17 @@ def _get_state(self, content):
203203
return "Resource state {}".format(state) if state else "none"
204204

205205
def _build_error_message(self, response):
206+
# Assume ClientResponse has "body", and otherwise it's a requests.Response
207+
content = response.text() if hasattr(response, "body") else response.text
206208
try:
207-
data = response.json()
209+
data = json.loads(content)
208210
except ValueError:
209211
message = "none"
210212
else:
211213
try:
212214
message = data.get("message", self._get_state(data))
213215
except AttributeError: # data is not a dict, but is a requests.Response parsable as JSON
214-
message = str(response.text)
216+
message = str(content)
215217
try:
216218
response.raise_for_status()
217219
except RequestException as err:

msrestazure/azure_operation.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,6 @@ class AzureOperationPoller(object):
335335
"""Initiates long running operation and polls status in separate
336336
thread.
337337
338-
This class is used in old SDK and has been replaced. See "polling"
339-
submodule now.
340-
341338
:param callable send_cmd: The API request to initiate the operation.
342339
:param callable update_cmd: The API reuqest to check the status of
343340
the operation.
@@ -502,7 +499,7 @@ def wait(self, timeout=None):
502499
503500
:param int timeout: Perion of time to wait for the long running
504501
operation to complete.
505-
:raises ~msrestazure.azure_exceptions.CloudError: Server problem with the query.
502+
:raises CloudError: Server problem with the query.
506503
"""
507504
if self._thread is None:
508505
return

msrestazure/polling/arm_polling.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@
2323
# IN THE SOFTWARE.
2424
#
2525
# --------------------------------------------------------------------------
26-
import re
26+
import json
2727
import time
2828
try:
2929
from urlparse import urlparse
3030
except ImportError:
3131
from urllib.parse import urlparse
3232

33-
from msrest.exceptions import DeserializationError, ClientException
33+
from msrest.exceptions import DeserializationError
3434
from msrest.polling import PollingMethod
3535

3636
from ..azure_exceptions import CloudError
@@ -143,14 +143,29 @@ def _is_empty(self, response):
143143
"""Check if response body contains meaningful content.
144144
145145
:rtype: bool
146-
:raises: DeserializationError if response body contains invalid
147-
json data.
146+
:raises: DeserializationError if response body contains invalid json data.
148147
"""
149-
if not response.content:
148+
# Assume ClientResponse has "body", and otherwise it's a requests.Response
149+
content = response.text() if hasattr(response, "body") else response.text
150+
if not content:
150151
return True
151152
try:
152-
body = response.json()
153-
return not body
153+
return not json.loads(content)
154+
except ValueError:
155+
raise DeserializationError(
156+
"Error occurred in deserializing the response body.")
157+
158+
def _as_json(self, response):
159+
"""Assuming this is not empty, return the content as JSON.
160+
161+
Result/exceptions is not determined if you call this method without testing _is_empty.
162+
163+
:raises: DeserializationError if response body contains invalid json data.
164+
"""
165+
# Assume ClientResponse has "body", and otherwise it's a requests.Response
166+
content = response.text() if hasattr(response, "body") else response.text
167+
try:
168+
return json.loads(content)
154169
except ValueError:
155170
raise DeserializationError(
156171
"Error occurred in deserializing the response body.")
@@ -171,7 +186,7 @@ def _get_async_status(self, response):
171186
"""
172187
if self._is_empty(response):
173188
return None
174-
body = response.json()
189+
body = self._as_json(response)
175190
return body.get('status')
176191

177192
def _get_provisioning_state(self, response):
@@ -182,7 +197,7 @@ def _get_provisioning_state(self, response):
182197
"""
183198
if self._is_empty(response):
184199
return None
185-
body = response.json()
200+
body = self._as_json(response)
186201
return body.get("properties", {}).get("provisioningState")
187202

188203
def should_do_final_get(self):
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# --------------------------------------------------------------------------
2+
#
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
#
5+
# The MIT License (MIT)
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the ""Software""), to
9+
# deal in the Software without restriction, including without limitation the
10+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11+
# sell copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in
15+
# all copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23+
# IN THE SOFTWARE.
24+
#
25+
# --------------------------------------------------------------------------
26+
import asyncio
27+
28+
from ..azure_exceptions import CloudError
29+
from .arm_polling import (
30+
failed,
31+
BadStatus,
32+
BadResponse,
33+
OperationFailed,
34+
ARMPolling
35+
)
36+
37+
__all__ = ["AsyncARMPolling"]
38+
39+
class AsyncARMPolling(ARMPolling):
40+
"""A subclass or ARMPolling that redefine "run" as async.
41+
"""
42+
43+
async def run(self):
44+
try:
45+
await self._poll()
46+
except BadStatus:
47+
self._operation.status = 'Failed'
48+
raise CloudError(self._response)
49+
50+
except BadResponse as err:
51+
self._operation.status = 'Failed'
52+
raise CloudError(self._response, str(err))
53+
54+
except OperationFailed:
55+
raise CloudError(self._response)
56+
57+
async def _poll(self):
58+
"""Poll status of operation so long as operation is incomplete and
59+
we have an endpoint to query.
60+
61+
:param callable update_cmd: The function to call to retrieve the
62+
latest status of the long running operation.
63+
:raises: OperationFailed if operation status 'Failed' or 'Cancelled'.
64+
:raises: BadStatus if response status invalid.
65+
:raises: BadResponse if response invalid.
66+
"""
67+
68+
while not self.finished():
69+
await self._delay()
70+
await self.update_status()
71+
72+
if failed(self._operation.status):
73+
raise OperationFailed("Operation failed or cancelled")
74+
75+
elif self._operation.should_do_final_get():
76+
if self._operation.method == 'POST' and self._operation.location_url:
77+
final_get_url = self._operation.location_url
78+
else:
79+
final_get_url = self._operation.initial_response.request.url
80+
self._response = await self.request_status(final_get_url)
81+
self._operation.get_status_from_resource(self._response)
82+
83+
async def _delay(self):
84+
"""Check for a 'retry-after' header to set timeout,
85+
otherwise use configured timeout.
86+
"""
87+
if self._response is None:
88+
await asyncio.sleep(0)
89+
if self._response.headers.get('retry-after'):
90+
await asyncio.sleep(int(self._response.headers['retry-after']))
91+
else:
92+
await asyncio.sleep(self._timeout)
93+
94+
async def update_status(self):
95+
"""Update the current status of the LRO.
96+
"""
97+
if self._operation.async_url:
98+
self._response = await self.request_status(self._operation.async_url)
99+
self._operation.set_async_url_if_present(self._response)
100+
self._operation.get_status_from_async(self._response)
101+
elif self._operation.location_url:
102+
self._response = await self.request_status(self._operation.location_url)
103+
self._operation.set_async_url_if_present(self._response)
104+
self._operation.get_status_from_location(self._response)
105+
elif self._operation.method == "PUT":
106+
initial_url = self._operation.initial_response.request.url
107+
self._response = await self.request_status(initial_url)
108+
self._operation.set_async_url_if_present(self._response)
109+
self._operation.get_status_from_resource(self._response)
110+
else:
111+
raise BadResponse("Unable to find status link for polling.")
112+
113+
async def request_status(self, status_link):
114+
"""Do a simple GET to this status link.
115+
116+
This method re-inject 'x-ms-client-request-id'.
117+
118+
:rtype: requests.Response
119+
"""
120+
# ARM requires to re-inject 'x-ms-client-request-id' while polling
121+
header_parameters = {
122+
'x-ms-client-request-id': self._operation.initial_response.request.headers['x-ms-client-request-id']
123+
}
124+
request = self._client.get(status_link, headers=header_parameters)
125+
return await self._client.async_send(request, stream=False, **self._operation_config)

msrestazure/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@
2525
# --------------------------------------------------------------------------
2626

2727
#: version of the package. Use msrestazure.__version__ instead.
28-
msrestazure_version = "0.5.1"
28+
msrestazure_version = "0.6.0"

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
setup(
3030
name='msrestazure',
31-
version='0.5.1',
31+
version='0.6.0',
3232
author='Microsoft Corporation',
3333
author_email='azpysdkhelp@microsoft.com',
3434
packages=find_packages(exclude=["tests", "tests.*"]),
@@ -50,7 +50,7 @@
5050
'License :: OSI Approved :: MIT License',
5151
'Topic :: Software Development'],
5252
install_requires=[
53-
"msrest>=0.4.28,<2.0.0",
53+
"msrest>=0.6.0,<2.0.0",
5454
"adal>=0.6.0,<2.0.0",
5555
],
5656
)

0 commit comments

Comments
 (0)