Skip to content

Commit cc7b25e

Browse files
[formrecognizer] raise when call quota exceeded (Azure#20062)
* add custom quota exceeded policy * add mock tests * set content-type in tests * use async mock transport helper * debugging * remove debugging code - mindep problem * updating min dep for azure-core * update changelog
1 parent a3ad60a commit cc7b25e

File tree

8 files changed

+172
-16
lines changed

8 files changed

+172
-16
lines changed

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
## 3.1.2 (Unreleased)
44

5-
### Features Added
6-
7-
### Breaking Changes
8-
9-
### Key Bugs Fixed
10-
11-
### Fixed
5+
### Bugs Fixed
6+
- A `HttpResponseError` will be immediately raised when the call quota volume is exceeded in a `F0` tier Form Recognizer
7+
resource.
128

9+
### Other Changes
10+
- Bumped `azure-core` minimum dependency version from `1.8.2` to `1.13.0`
1311

1412
## 3.1.1 (2021-06-08)
1513

sdk/formrecognizer/azure-ai-formrecognizer/azure/ai/formrecognizer/_form_base_client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from azure.core.pipeline.policies import HttpLoggingPolicy
99
from ._generated._form_recognizer_client import FormRecognizerClient as FormRecognizer
1010
from ._api_versions import FormRecognizerApiVersion, validate_api_version
11-
from ._helpers import _get_deserialize, get_authentication_policy, POLLING_INTERVAL
11+
from ._helpers import _get_deserialize, get_authentication_policy, POLLING_INTERVAL, QuotaExceededPolicy
1212
from ._user_agent import USER_AGENT
1313

1414
if TYPE_CHECKING:
@@ -58,8 +58,9 @@ def __init__(self, endpoint, credential, **kwargs):
5858
credential=credential, # type: ignore
5959
api_version=self._api_version,
6060
sdk_moniker=USER_AGENT,
61-
authentication_policy=authentication_policy,
62-
http_logging_policy=http_logging_policy,
61+
authentication_policy=kwargs.get("authentication_policy", authentication_policy),
62+
http_logging_policy=kwargs.get("http_logging_policy", http_logging_policy),
63+
per_retry_policies=kwargs.get("per_retry_policies", QuotaExceededPolicy()),
6364
polling_interval=polling_interval,
6465
**kwargs
6566
)

sdk/formrecognizer/azure-ai-formrecognizer/azure/ai/formrecognizer/_helpers.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import re
88
import six
99
from azure.core.credentials import AzureKeyCredential
10-
from azure.core.pipeline.policies import AzureKeyCredentialPolicy
10+
from azure.core.pipeline.policies import AzureKeyCredentialPolicy, SansIOHTTPPolicy
1111
from azure.core.pipeline.transport import HttpTransport
12+
from azure.core.exceptions import HttpResponseError
13+
1214

1315
POLLING_INTERVAL = 5
1416
COGNITIVE_KEY_HEADER = "Ocp-Apim-Subscription-Key"
@@ -164,3 +166,23 @@ def __enter__(self):
164166

165167
def __exit__(self, *args): # pylint: disable=arguments-differ
166168
pass
169+
170+
171+
class QuotaExceededPolicy(SansIOHTTPPolicy):
172+
"""Raises an exception immediately when the call quota volume has been exceeded in a F0
173+
tier form recognizer resource. This is to avoid waiting the Retry-After time returned in
174+
the response.
175+
"""
176+
177+
def on_response(self, request, response):
178+
"""Is executed after the request comes back from the policy.
179+
180+
:param request: Request to be modified after returning from the policy.
181+
:type request: ~azure.core.pipeline.PipelineRequest
182+
:param response: Pipeline response object
183+
:type response: ~azure.core.pipeline.PipelineResponse
184+
"""
185+
http_response = response.http_response
186+
if http_response.status_code in [403, 429] and \
187+
"Out of call volume quota for FormRecognizer F0 pricing tier" in http_response.text():
188+
raise HttpResponseError(http_response.text(), response=http_response)

sdk/formrecognizer/azure-ai-formrecognizer/azure/ai/formrecognizer/aio/_form_base_client_async.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
FormRecognizerClient as FormRecognizer,
1515
)
1616
from .._api_versions import FormRecognizerApiVersion, validate_api_version
17-
from .._helpers import _get_deserialize, get_authentication_policy, POLLING_INTERVAL
17+
from .._helpers import _get_deserialize, get_authentication_policy, POLLING_INTERVAL, QuotaExceededPolicy
1818
from .._user_agent import USER_AGENT
1919

2020
if TYPE_CHECKING:
@@ -68,8 +68,9 @@ def __init__(
6868
credential=credential, # type: ignore
6969
api_version=self._api_version,
7070
sdk_moniker=USER_AGENT,
71-
authentication_policy=authentication_policy,
72-
http_logging_policy=http_logging_policy,
71+
authentication_policy=kwargs.get("authentication_policy", authentication_policy),
72+
http_logging_policy=kwargs.get("http_logging_policy", http_logging_policy),
73+
per_retry_policies=kwargs.get("per_retry_policies", QuotaExceededPolicy()),
7374
polling_interval=polling_interval,
7475
**kwargs
7576
)

sdk/formrecognizer/azure-ai-formrecognizer/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
'azure.ai',
8181
]),
8282
install_requires=[
83-
"azure-core<2.0.0,>=1.8.2",
83+
"azure-core<2.0.0,>=1.13.0",
8484
"msrest>=0.6.21",
8585
'six>=1.11.0',
8686
'azure-common~=1.1',

sdk/formrecognizer/azure-ai-formrecognizer/tests/test_logging.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@
77

88
import logging
99
import pytest
10+
import json
11+
try:
12+
from unittest import mock
13+
except ImportError: # python < 3.3
14+
import mock # type: ignore
15+
1016
from azure.ai.formrecognizer import FormRecognizerClient, FormTrainingClient
1117
from azure.core.credentials import AzureKeyCredential
18+
from azure.core.exceptions import HttpResponseError
1219
from testcase import FormRecognizerTest
1320
from preparers import FormRecognizerPreparer
1421

@@ -63,3 +70,47 @@ def test_logging_info_ft_client(self, formrecognizer_test_endpoint, formrecogniz
6370
assert message.message.find("REDACTED") != -1
6471
else:
6572
assert message.message.find("REDACTED") == -1
73+
74+
@FormRecognizerPreparer()
75+
def test_mock_quota_exceeded_403(self, formrecognizer_test_endpoint, formrecognizer_test_api_key):
76+
77+
response = mock.Mock(
78+
status_code=403,
79+
headers={"Retry-After": 186688, "Content-Type": "application/json"},
80+
reason="Bad Request"
81+
)
82+
response.text = lambda encoding=None: json.dumps(
83+
{"error": {"code": "403", "message": "Out of call volume quota for FormRecognizer F0 pricing tier. "
84+
"Please retry after 1 day. To increase your call volume switch to a paid tier."}}
85+
)
86+
response.content_type = "application/json"
87+
transport = mock.Mock(send=lambda request, **kwargs: response)
88+
89+
client = FormRecognizerClient(formrecognizer_test_endpoint, AzureKeyCredential(formrecognizer_test_api_key), transport=transport)
90+
91+
with pytest.raises(HttpResponseError) as e:
92+
poller = client.begin_recognize_receipts_from_url(self.receipt_url_jpg)
93+
assert e.value.status_code == 403
94+
assert e.value.error.message == 'Out of call volume quota for FormRecognizer F0 pricing tier. Please retry after 1 day. To increase your call volume switch to a paid tier.'
95+
96+
@FormRecognizerPreparer()
97+
def test_mock_quota_exceeded_429(self, formrecognizer_test_endpoint, formrecognizer_test_api_key):
98+
99+
response = mock.Mock(
100+
status_code=429,
101+
headers={"Retry-After": 186688, "Content-Type": "application/json"},
102+
reason="Bad Request"
103+
)
104+
response.text = lambda encoding=None: json.dumps(
105+
{"error": {"code": "429", "message": "Out of call volume quota for FormRecognizer F0 pricing tier. "
106+
"Please retry after 1 day. To increase your call volume switch to a paid tier."}}
107+
)
108+
response.content_type = "application/json"
109+
transport = mock.Mock(send=lambda request, **kwargs: response)
110+
111+
client = FormRecognizerClient(formrecognizer_test_endpoint, AzureKeyCredential(formrecognizer_test_api_key), transport=transport)
112+
113+
with pytest.raises(HttpResponseError) as e:
114+
poller = client.begin_recognize_receipts_from_url(self.receipt_url_jpg)
115+
assert e.value.status_code == 429
116+
assert e.value.error.message == 'Out of call volume quota for FormRecognizer F0 pricing tier. Please retry after 1 day. To increase your call volume switch to a paid tier.'

sdk/formrecognizer/azure-ai-formrecognizer/tests/test_logging_async.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@
77

88
import logging
99
import pytest
10+
import json
11+
import sys
12+
import asyncio
13+
import functools
14+
try:
15+
from unittest import mock
16+
except ImportError: # python < 3.3
17+
import mock # type: ignore
1018
from azure.ai.formrecognizer.aio import FormRecognizerClient, FormTrainingClient
1119
from azure.core.credentials import AzureKeyCredential
20+
from azure.core.exceptions import HttpResponseError
1221
from preparers import FormRecognizerPreparer
1322
from asynctestcase import AsyncFormRecognizerTest
1423

@@ -22,6 +31,38 @@ def emit(self, record):
2231
self.messages.append(record)
2332

2433

34+
def get_completed_future(result=None):
35+
future = asyncio.Future()
36+
future.set_result(result)
37+
return future
38+
39+
40+
def wrap_in_future(fn):
41+
"""Return a completed Future whose result is the return of fn.
42+
Added to simplify using unittest.Mock in async code. Python 3.8's AsyncMock would be preferable.
43+
"""
44+
45+
@functools.wraps(fn)
46+
def wrapper(*args, **kwargs):
47+
result = fn(*args, **kwargs)
48+
return get_completed_future(result)
49+
return wrapper
50+
51+
52+
class AsyncMockTransport(mock.MagicMock):
53+
"""Mock with do-nothing aenter/exit for mocking async transport.
54+
55+
This is unnecessary on 3.8+, where MagicMocks implement aenter/exit.
56+
"""
57+
58+
def __init__(self, *args, **kwargs):
59+
super().__init__(*args, **kwargs)
60+
61+
if sys.version_info < (3, 8):
62+
self.__aenter__ = mock.Mock(return_value=get_completed_future())
63+
self.__aexit__ = mock.Mock(return_value=get_completed_future())
64+
65+
2566
class TestLogging(AsyncFormRecognizerTest):
2667

2768
@FormRecognizerPreparer()
@@ -64,3 +105,45 @@ async def test_logging_info_ft_client(self, formrecognizer_test_endpoint, formre
64105
assert message.message.find("REDACTED") != -1
65106
else:
66107
assert message.message.find("REDACTED") == -1
108+
109+
@FormRecognizerPreparer()
110+
async def test_mock_quota_exceeded_403(self, formrecognizer_test_endpoint, formrecognizer_test_api_key):
111+
112+
response = mock.Mock(
113+
status_code=403,
114+
headers={"Retry-After": 186688, "Content-Type": "application/json"},
115+
reason="Bad Request"
116+
)
117+
response.text = lambda encoding=None: json.dumps(
118+
{"error": {"code": "403", "message": "Out of call volume quota for FormRecognizer F0 pricing tier. "
119+
"Please retry after 1 day. To increase your call volume switch to a paid tier."}}
120+
)
121+
response.content_type = "application/json"
122+
transport = AsyncMockTransport(send=wrap_in_future(lambda request, **kwargs: response))
123+
124+
client = FormRecognizerClient(formrecognizer_test_endpoint, AzureKeyCredential(formrecognizer_test_api_key), transport=transport)
125+
126+
with pytest.raises(HttpResponseError) as e:
127+
poller = await client.begin_recognize_receipts_from_url(self.receipt_url_jpg)
128+
assert e.value.status_code == 403
129+
assert e.value.error.message == 'Out of call volume quota for FormRecognizer F0 pricing tier. Please retry after 1 day. To increase your call volume switch to a paid tier.'
130+
131+
@FormRecognizerPreparer()
132+
async def test_mock_quota_exceeded_429(self, formrecognizer_test_endpoint, formrecognizer_test_api_key):
133+
response = mock.Mock(
134+
status_code=429,
135+
headers={"Retry-After": 186688, "Content-Type": "application/json"},
136+
reason="Bad Request"
137+
)
138+
response.text = lambda encoding=None: json.dumps(
139+
{"error": {"code": "429", "message": "Out of call volume quota for FormRecognizer F0 pricing tier. "
140+
"Please retry after 1 day. To increase your call volume switch to a paid tier."}}
141+
)
142+
response.content_type = "application/json"
143+
transport = AsyncMockTransport(send=wrap_in_future(lambda request, **kwargs: response))
144+
145+
client = FormRecognizerClient(formrecognizer_test_endpoint, AzureKeyCredential(formrecognizer_test_api_key), transport=transport)
146+
with pytest.raises(HttpResponseError) as e:
147+
poller = await client.begin_recognize_receipts_from_url(self.receipt_url_jpg)
148+
assert e.value.status_code == 429
149+
assert e.value.error.message == 'Out of call volume quota for FormRecognizer F0 pricing tier. Please retry after 1 day. To increase your call volume switch to a paid tier.'

shared_requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ chardet<5,>=3.0.2
153153
#override azure-ai-language-questionanswering msrest>=0.6.21
154154
#override azure-search-documents azure-core<2.0.0,>=1.14.0
155155
#override azure-ai-formrecognizer msrest>=0.6.21
156-
#override azure-ai-formrecognizer azure-core<2.0.0,>=1.8.2
156+
#override azure-ai-formrecognizer azure-core<2.0.0,>=1.13.0
157157
#override azure-storage-blob azure-core<2.0.0,>=1.10.0
158158
#override azure-storage-blob msrest>=0.6.21
159159
#override azure-storage-blob-changefeed azure-storage-blob>=12.5.0,<13.0.0

0 commit comments

Comments
 (0)