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

[formrecognizer] raise when call quota exceeded #20062

Merged
merged 8 commits into from
Aug 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 5 additions & 7 deletions sdk/formrecognizer/azure-ai-formrecognizer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

## 3.1.2 (Unreleased)

### Features Added

### Breaking Changes

### Key Bugs Fixed

### Fixed
### Bugs Fixed
- A `HttpResponseError` will be immediately raised when the call quota volume is exceeded in a `F0` tier Form Recognizer
resource.

### Other Changes
- Bumped `azure-core` minimum dependency version from `1.8.2` to `1.13.0`

## 3.1.1 (2021-06-08)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from azure.core.pipeline.policies import HttpLoggingPolicy
from ._generated._form_recognizer_client import FormRecognizerClient as FormRecognizer
from ._api_versions import FormRecognizerApiVersion, validate_api_version
from ._helpers import _get_deserialize, get_authentication_policy, POLLING_INTERVAL
from ._helpers import _get_deserialize, get_authentication_policy, POLLING_INTERVAL, QuotaExceededPolicy
from ._user_agent import USER_AGENT

if TYPE_CHECKING:
Expand Down Expand Up @@ -58,8 +58,9 @@ def __init__(self, endpoint, credential, **kwargs):
credential=credential, # type: ignore
api_version=self._api_version,
sdk_moniker=USER_AGENT,
authentication_policy=authentication_policy,
http_logging_policy=http_logging_policy,
authentication_policy=kwargs.get("authentication_policy", authentication_policy),
http_logging_policy=kwargs.get("http_logging_policy", http_logging_policy),
per_retry_policies=kwargs.get("per_retry_policies", QuotaExceededPolicy()),
polling_interval=polling_interval,
**kwargs
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import re
import six
from azure.core.credentials import AzureKeyCredential
from azure.core.pipeline.policies import AzureKeyCredentialPolicy
from azure.core.pipeline.policies import AzureKeyCredentialPolicy, SansIOHTTPPolicy
from azure.core.pipeline.transport import HttpTransport
from azure.core.exceptions import HttpResponseError


POLLING_INTERVAL = 5
COGNITIVE_KEY_HEADER = "Ocp-Apim-Subscription-Key"
Expand Down Expand Up @@ -164,3 +166,23 @@ def __enter__(self):

def __exit__(self, *args): # pylint: disable=arguments-differ
pass


class QuotaExceededPolicy(SansIOHTTPPolicy):
"""Raises an exception immediately when the call quota volume has been exceeded in a F0
tier form recognizer resource. This is to avoid waiting the Retry-After time returned in
the response.
"""

def on_response(self, request, response):
"""Is executed after the request comes back from the policy.

:param request: Request to be modified after returning from the policy.
:type request: ~azure.core.pipeline.PipelineRequest
:param response: Pipeline response object
:type response: ~azure.core.pipeline.PipelineResponse
"""
http_response = response.http_response
if http_response.status_code in [403, 429] and \
"Out of call volume quota for FormRecognizer F0 pricing tier" in http_response.text():
raise HttpResponseError(http_response.text(), response=http_response)
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
FormRecognizerClient as FormRecognizer,
)
from .._api_versions import FormRecognizerApiVersion, validate_api_version
from .._helpers import _get_deserialize, get_authentication_policy, POLLING_INTERVAL
from .._helpers import _get_deserialize, get_authentication_policy, POLLING_INTERVAL, QuotaExceededPolicy
from .._user_agent import USER_AGENT

if TYPE_CHECKING:
Expand Down Expand Up @@ -68,8 +68,9 @@ def __init__(
credential=credential, # type: ignore
api_version=self._api_version,
sdk_moniker=USER_AGENT,
authentication_policy=authentication_policy,
http_logging_policy=http_logging_policy,
authentication_policy=kwargs.get("authentication_policy", authentication_policy),
http_logging_policy=kwargs.get("http_logging_policy", http_logging_policy),
per_retry_policies=kwargs.get("per_retry_policies", QuotaExceededPolicy()),
polling_interval=polling_interval,
**kwargs
)
Expand Down
2 changes: 1 addition & 1 deletion sdk/formrecognizer/azure-ai-formrecognizer/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
'azure.ai',
]),
install_requires=[
"azure-core<2.0.0,>=1.8.2",
"azure-core<2.0.0,>=1.13.0",
"msrest>=0.6.21",
'six>=1.11.0',
'azure-common~=1.1',
Expand Down
51 changes: 51 additions & 0 deletions sdk/formrecognizer/azure-ai-formrecognizer/tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@

import logging
import pytest
import json
try:
from unittest import mock
except ImportError: # python < 3.3
import mock # type: ignore

from azure.ai.formrecognizer import FormRecognizerClient, FormTrainingClient
from azure.core.credentials import AzureKeyCredential
from azure.core.exceptions import HttpResponseError
from testcase import FormRecognizerTest
from preparers import FormRecognizerPreparer

Expand Down Expand Up @@ -63,3 +70,47 @@ def test_logging_info_ft_client(self, formrecognizer_test_endpoint, formrecogniz
assert message.message.find("REDACTED") != -1
else:
assert message.message.find("REDACTED") == -1

@FormRecognizerPreparer()
def test_mock_quota_exceeded_403(self, formrecognizer_test_endpoint, formrecognizer_test_api_key):

response = mock.Mock(
status_code=403,
headers={"Retry-After": 186688, "Content-Type": "application/json"},
reason="Bad Request"
)
response.text = lambda encoding=None: json.dumps(
{"error": {"code": "403", "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."}}
)
response.content_type = "application/json"
transport = mock.Mock(send=lambda request, **kwargs: response)

client = FormRecognizerClient(formrecognizer_test_endpoint, AzureKeyCredential(formrecognizer_test_api_key), transport=transport)

with pytest.raises(HttpResponseError) as e:
poller = client.begin_recognize_receipts_from_url(self.receipt_url_jpg)
assert e.value.status_code == 403
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.'

@FormRecognizerPreparer()
def test_mock_quota_exceeded_429(self, formrecognizer_test_endpoint, formrecognizer_test_api_key):

response = mock.Mock(
status_code=429,
headers={"Retry-After": 186688, "Content-Type": "application/json"},
reason="Bad Request"
)
response.text = lambda encoding=None: json.dumps(
{"error": {"code": "429", "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."}}
)
response.content_type = "application/json"
transport = mock.Mock(send=lambda request, **kwargs: response)

client = FormRecognizerClient(formrecognizer_test_endpoint, AzureKeyCredential(formrecognizer_test_api_key), transport=transport)

with pytest.raises(HttpResponseError) as e:
poller = client.begin_recognize_receipts_from_url(self.receipt_url_jpg)
assert e.value.status_code == 429
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.'
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@

import logging
import pytest
import json
import sys
import asyncio
import functools
try:
from unittest import mock
except ImportError: # python < 3.3
import mock # type: ignore
from azure.ai.formrecognizer.aio import FormRecognizerClient, FormTrainingClient
from azure.core.credentials import AzureKeyCredential
from azure.core.exceptions import HttpResponseError
from preparers import FormRecognizerPreparer
from asynctestcase import AsyncFormRecognizerTest

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


def get_completed_future(result=None):
future = asyncio.Future()
future.set_result(result)
return future


def wrap_in_future(fn):
"""Return a completed Future whose result is the return of fn.
Added to simplify using unittest.Mock in async code. Python 3.8's AsyncMock would be preferable.
"""

@functools.wraps(fn)
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
return get_completed_future(result)
return wrapper


class AsyncMockTransport(mock.MagicMock):
"""Mock with do-nothing aenter/exit for mocking async transport.

This is unnecessary on 3.8+, where MagicMocks implement aenter/exit.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

if sys.version_info < (3, 8):
self.__aenter__ = mock.Mock(return_value=get_completed_future())
self.__aexit__ = mock.Mock(return_value=get_completed_future())


class TestLogging(AsyncFormRecognizerTest):

@FormRecognizerPreparer()
Expand Down Expand Up @@ -64,3 +105,45 @@ async def test_logging_info_ft_client(self, formrecognizer_test_endpoint, formre
assert message.message.find("REDACTED") != -1
else:
assert message.message.find("REDACTED") == -1

@FormRecognizerPreparer()
async def test_mock_quota_exceeded_403(self, formrecognizer_test_endpoint, formrecognizer_test_api_key):

response = mock.Mock(
status_code=403,
headers={"Retry-After": 186688, "Content-Type": "application/json"},
reason="Bad Request"
)
response.text = lambda encoding=None: json.dumps(
{"error": {"code": "403", "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."}}
)
response.content_type = "application/json"
transport = AsyncMockTransport(send=wrap_in_future(lambda request, **kwargs: response))

client = FormRecognizerClient(formrecognizer_test_endpoint, AzureKeyCredential(formrecognizer_test_api_key), transport=transport)

with pytest.raises(HttpResponseError) as e:
poller = await client.begin_recognize_receipts_from_url(self.receipt_url_jpg)
assert e.value.status_code == 403
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.'

@FormRecognizerPreparer()
async def test_mock_quota_exceeded_429(self, formrecognizer_test_endpoint, formrecognizer_test_api_key):
response = mock.Mock(
status_code=429,
headers={"Retry-After": 186688, "Content-Type": "application/json"},
reason="Bad Request"
)
response.text = lambda encoding=None: json.dumps(
{"error": {"code": "429", "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."}}
)
response.content_type = "application/json"
transport = AsyncMockTransport(send=wrap_in_future(lambda request, **kwargs: response))

client = FormRecognizerClient(formrecognizer_test_endpoint, AzureKeyCredential(formrecognizer_test_api_key), transport=transport)
with pytest.raises(HttpResponseError) as e:
poller = await client.begin_recognize_receipts_from_url(self.receipt_url_jpg)
assert e.value.status_code == 429
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.'
2 changes: 1 addition & 1 deletion shared_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ chardet<5,>=3.0.2
#override azure-ai-language-questionanswering msrest>=0.6.21
#override azure-search-documents azure-core<2.0.0,>=1.14.0
#override azure-ai-formrecognizer msrest>=0.6.21
#override azure-ai-formrecognizer azure-core<2.0.0,>=1.8.2
#override azure-ai-formrecognizer azure-core<2.0.0,>=1.13.0
#override azure-storage-blob azure-core<2.0.0,>=1.10.0
#override azure-storage-blob msrest>=0.6.21
#override azure-storage-blob-changefeed azure-storage-blob>=12.5.0,<13.0.0
Expand Down