Skip to content

Commit c2a510e

Browse files
inject custom policy to raise exception on quota limit exceeded (Azure#25631)
1 parent b7d4785 commit c2a510e

File tree

6 files changed

+109
-2
lines changed

6 files changed

+109
-2
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ The version of this client library defaults to the API version `2022-05-01`.
2525
- Renamed `SingleCategoryClassifyAction` to `SingleLabelClassifyAction`
2626
- Renamed `MultiCategoryClassifyAction` to `MultiLabelClassifyAction`.
2727

28+
### Bugs Fixed
29+
30+
- A `HttpResponseError` will be immediately raised when the call quota volume is exceeded in a `F0` tier Language resource.
31+
2832
### Other Changes
2933

3034
- Python 3.6 is no longer supported. Please use Python version 3.7 or later. For more details, see [Azure SDK for Python version support policy](https://github.com/Azure/azure-sdk-for-python/wiki/Azure-SDKs-Python-version-support-policy).

sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_base_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from azure.core.pipeline.policies import AzureKeyCredentialPolicy, HttpLoggingPolicy
1010
from azure.core.credentials import AzureKeyCredential, TokenCredential
1111
from ._generated import TextAnalyticsClient as _TextAnalyticsClient
12-
from ._policies import TextAnalyticsResponseHookPolicy
12+
from ._policies import TextAnalyticsResponseHookPolicy, QuotaExceededPolicy
1313
from ._user_agent import USER_AGENT
1414
from ._version import DEFAULT_API_VERSION
1515

@@ -84,6 +84,7 @@ def __init__(
8484
authentication_policy=kwargs.pop("authentication_policy", _authentication_policy(credential)),
8585
custom_hook_policy=kwargs.pop("custom_hook_policy", TextAnalyticsResponseHookPolicy(**kwargs)),
8686
http_logging_policy=kwargs.pop("http_logging_policy", http_logging_policy),
87+
per_retry_policies=kwargs.get("per_retry_policies", QuotaExceededPolicy()),
8788
**kwargs
8889
)
8990

sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/_policies.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from azure.core.pipeline.policies import ContentDecodePolicy
77
from azure.core.pipeline.policies import SansIOHTTPPolicy
8+
from azure.core.exceptions import HttpResponseError
89
from ._models import TextDocumentBatchStatistics
910
from ._lro import _FINISHED
1011

@@ -43,3 +44,23 @@ def on_response(self, request, response):
4344
response.model_version = model_version
4445
response.raw_response = data
4546
self._response_callback(response)
47+
48+
49+
class QuotaExceededPolicy(SansIOHTTPPolicy):
50+
"""Raises an exception immediately when the call quota volume has been exceeded in a F0
51+
tier language resource. This is to avoid waiting the Retry-After time returned in
52+
the response.
53+
"""
54+
55+
def on_response(self, request, response):
56+
"""Is executed after the request comes back from the policy.
57+
58+
:param request: Request to be modified after returning from the policy.
59+
:type request: ~azure.core.pipeline.PipelineRequest
60+
:param response: Pipeline response object
61+
:type response: ~azure.core.pipeline.PipelineResponse
62+
"""
63+
http_response = response.http_response
64+
if http_response.status_code == 403 and \
65+
"Out of call volume quota for TextAnalytics F0 pricing tier" in http_response.text():
66+
raise HttpResponseError(http_response.text(), response=http_response)

sdk/textanalytics/azure-ai-textanalytics/azure/ai/textanalytics/aio/_base_client_async.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from azure.core.credentials_async import AsyncTokenCredential
88
from azure.core.pipeline.policies import AzureKeyCredentialPolicy, HttpLoggingPolicy
99
from .._generated.aio import TextAnalyticsClient as _TextAnalyticsClient
10-
from .._policies import TextAnalyticsResponseHookPolicy
10+
from .._policies import TextAnalyticsResponseHookPolicy, QuotaExceededPolicy
1111
from .._user_agent import USER_AGENT
1212
from .._version import DEFAULT_API_VERSION
1313

@@ -71,6 +71,7 @@ def __init__(
7171
authentication_policy=kwargs.pop("authentication_policy", _authentication_policy(credential)),
7272
custom_hook_policy=kwargs.pop("custom_hook_policy", TextAnalyticsResponseHookPolicy(**kwargs)),
7373
http_logging_policy=kwargs.pop("http_logging_policy", http_logging_policy),
74+
per_retry_policies=kwargs.get("per_retry_policies", QuotaExceededPolicy()),
7475
**kwargs
7576
)
7677

sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import platform
88
import functools
99
import json
10+
from unittest import mock
1011
from azure.core.exceptions import HttpResponseError, ClientAuthenticationError
1112
from azure.core.credentials import AzureKeyCredential
1213
from testcase import TextAnalyticsTest, TextAnalyticsPreparer
@@ -854,3 +855,25 @@ def test_sentiment_multiapi_validate_args_v3_0(self, **kwargs):
854855
with pytest.raises(ValueError) as e:
855856
res = client.analyze_sentiment(["I'm tired"], show_opinion_mining=True, disable_service_logs=True, string_index_type="UnicodeCodePoint")
856857
assert str(e.value) == "'show_opinion_mining' is only available for API version v3.1 and up.\n'disable_service_logs' is only available for API version v3.1 and up.\n'string_index_type' is only available for API version v3.1 and up.\n"
858+
859+
@TextAnalyticsPreparer()
860+
def test_mock_quota_exceeded(self, **kwargs):
861+
textanalytics_test_endpoint = kwargs.pop("textanalytics_test_endpoint")
862+
textanalytics_test_api_key = kwargs.pop("textanalytics_test_api_key")
863+
response = mock.Mock(
864+
status_code=403,
865+
headers={"Retry-After": 186688, "Content-Type": "application/json"},
866+
reason="Bad Request"
867+
)
868+
response.text = lambda encoding=None: json.dumps(
869+
{"error": {"code": "403", "message": "Out of call volume quota for TextAnalytics F0 pricing tier. Please retry after 15 days. To increase your call volume switch to a paid tier."}}
870+
)
871+
response.content_type = "application/json"
872+
transport = mock.Mock(send=lambda request, **kwargs: response)
873+
874+
client = TextAnalyticsClient(textanalytics_test_endpoint, AzureKeyCredential(textanalytics_test_api_key), transport=transport)
875+
876+
with pytest.raises(HttpResponseError) as e:
877+
result = client.analyze_sentiment(["I'm tired"])
878+
assert e.value.status_code == 403
879+
assert e.value.error.message == 'Out of call volume quota for TextAnalytics F0 pricing tier. Please retry after 15 days. To increase your call volume switch to a paid tier.'

sdk/textanalytics/azure-ai-textanalytics/tests/test_analyze_sentiment_async.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
import platform
88
import functools
99
import json
10+
import sys
11+
import asyncio
12+
import functools
13+
from unittest import mock
1014
from azure.core.exceptions import HttpResponseError, ClientAuthenticationError
1115
from azure.core.credentials import AzureKeyCredential
1216
from azure.ai.textanalytics.aio import TextAnalyticsClient
@@ -25,6 +29,36 @@
2529
# pre-apply the client_cls positional argument so it needn't be explicitly passed below
2630
TextAnalyticsClientPreparer = functools.partial(_TextAnalyticsClientPreparer, TextAnalyticsClient)
2731

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

2963
class TestAnalyzeSentiment(TextAnalyticsTest):
3064

@@ -865,3 +899,26 @@ async def test_sentiment_multiapi_validate_args_v3_0(self, **kwargs):
865899
with pytest.raises(ValueError) as e:
866900
res = await client.analyze_sentiment(["I'm tired"], show_opinion_mining=True, disable_service_logs=True, string_index_type="UnicodeCodePoint")
867901
assert str(e.value) == "'show_opinion_mining' is only available for API version v3.1 and up.\n'disable_service_logs' is only available for API version v3.1 and up.\n'string_index_type' is only available for API version v3.1 and up.\n"
902+
903+
@TextAnalyticsPreparer()
904+
async def test_mock_quota_exceeded(self, **kwargs):
905+
textanalytics_test_endpoint = kwargs.pop("textanalytics_test_endpoint")
906+
textanalytics_test_api_key = kwargs.pop("textanalytics_test_api_key")
907+
908+
response = mock.Mock(
909+
status_code=403,
910+
headers={"Retry-After": 186688, "Content-Type": "application/json"},
911+
reason="Bad Request"
912+
)
913+
response.text = lambda encoding=None: json.dumps(
914+
{"error": {"code": "403", "message": "Out of call volume quota for TextAnalytics F0 pricing tier. Please retry after 15 days. To increase your call volume switch to a paid tier."}}
915+
)
916+
response.content_type = "application/json"
917+
transport = AsyncMockTransport(send=wrap_in_future(lambda request, **kwargs: response))
918+
919+
client = TextAnalyticsClient(textanalytics_test_endpoint, AzureKeyCredential(textanalytics_test_api_key), transport=transport)
920+
921+
with pytest.raises(HttpResponseError) as e:
922+
result = await client.analyze_sentiment(["I'm tired"])
923+
assert e.value.status_code == 403
924+
assert e.value.error.message == 'Out of call volume quota for TextAnalytics F0 pricing tier. Please retry after 15 days. To increase your call volume switch to a paid tier.'

0 commit comments

Comments
 (0)