Skip to content

Commit 9bb200b

Browse files
CSC-6771 Implement handling of HTTP 429 and 503 errors for Outlook clients
1 parent 3c5fe5d commit 9bb200b

4 files changed

Lines changed: 60 additions & 7 deletions

File tree

office365_api/v2/exceptions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ def __repr__(self):
4141
'Office365ClientError', self.status_code, self.error_code, self.error_message)
4242

4343

44+
class Office365QuotaExceededError(Office365ClientError):
45+
'''
46+
Exception raised when quota limit is exceeded (HTTP status code 429).
47+
Attributes:
48+
data -- error data returned by the server
49+
error_message -- explanation of the error returned by the server
50+
retry_after -- time in seconds to wait before retrying the request
51+
'''
52+
53+
def __init__(self, data=None, error_message=None, retry_after: str | int | None = None):
54+
55+
super(Office365QuotaExceededError, self).__init__(
56+
status_code=429, data=data, error_message=error_message)
57+
self.retry_after = retry_after
58+
4459
class Office365ServerError(Exception):
4560

4661
def __init__(self, status_code, body):

office365_api/v2/services/base.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import urllib.parse
21
import logging
2+
import urllib.parse
3+
34
from requests import HTTPError
4-
from requests.exceptions import ChunkedEncodingError, ConnectionError as RequestsConnectionError, JSONDecodeError as RequestsJSONDecodeError
5+
from requests.exceptions import ChunkedEncodingError
6+
from requests.exceptions import ConnectionError as RequestsConnectionError
7+
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
58

6-
from ..exceptions import Office365ClientError, Office365ServerError
7-
from ..consts import DEFAULT_MAX_ENTRIES, RETRIES_COUNT, RESPONSE_FORMAT_ODATA, RESPONSE_FORMAT_RAW
9+
from ..consts import (DEFAULT_MAX_ENTRIES, RESPONSE_FORMAT_ODATA,
10+
RESPONSE_FORMAT_RAW, RETRIES_COUNT)
11+
from ..exceptions import (Office365ClientError, Office365QuotaExceededError,
12+
Office365ServerError)
813

914
logger = logging.getLogger(__name__)
1015

@@ -63,6 +68,16 @@ def execute_request(self, method, path, query_params=None, headers=None, body=No
6368
error_data = e.response.json()
6469
except (ValueError, RequestsJSONDecodeError):
6570
error_data = {'error': {'message': e.response.content, 'code': 'unknown'}}
71+
if e.response.status_code == 429:
72+
retry_after = None
73+
try:
74+
retry_after = int(
75+
e.response.headers.get('Retry-After'))
76+
except Exception as ex:
77+
logger.error(
78+
'Error parsing Retry-After header: %s', ex)
79+
raise Office365QuotaExceededError(
80+
data=error_data, retry_after=retry_after) from e
6681
raise Office365ClientError(e.response.status_code, error_data) from e
6782
else:
6883
raise Office365ServerError(e.response.status_code, e.response.content) from e

office365_api/v2/services/batch.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
55

66
from office365_api.v2.exceptions import (Office365ClientError,
7+
Office365QuotaExceededError,
78
Office365ServerError)
89

910
from .base import BaseService
1011

1112
logger = logging.getLogger(__name__)
1213

14+
1315
class BatchService(BaseService):
1416
def __init__(self, client, beta=True):
1517
self.client = client
@@ -35,7 +37,8 @@ def add(self, request, callback=None):
3537

3638
def _execute(self, requests):
3739
if self.is_empty:
38-
raise Office365ClientError(error_message='No requests to execute in a batch')
40+
raise Office365ClientError(
41+
error_message='No requests to execute in a batch')
3942
method = 'POST'
4043
default_headers = {'Content-Type': 'application/json'}
4144
logger.info('{}: {} with {}x requests'.format(
@@ -54,6 +57,16 @@ def _execute(self, requests):
5457
except (ValueError, RequestsJSONDecodeError):
5558
error_data = {
5659
'error': {'message': e.response.content, 'code': 'unknown'}}
60+
if e.response.status_code == 429:
61+
retry_after = None
62+
try:
63+
retry_after = int(
64+
e.response.headers.get('Retry-After'))
65+
except Exception as ex:
66+
logger.error(
67+
'Error parsing Retry-After header: %s', ex)
68+
raise Office365QuotaExceededError(
69+
data=error_data, retry_after=retry_after) from e
5770
raise Office365ClientError(e.response.status_code, error_data)
5871
else:
5972
raise Office365ServerError(
@@ -76,6 +89,16 @@ def execute(self):
7689
try:
7790
if response['status'] >= 300:
7891
error_data = response.get('body')
92+
if response['status'] == 429:
93+
retry_after = None
94+
try:
95+
retry_after = int(
96+
response['headers'].get('Retry-After'))
97+
except Exception as ex:
98+
logger.warning(
99+
'Error parsing Retry-After header: %s', ex)
100+
raise Office365QuotaExceededError(
101+
data=error_data, retry_after=retry_after)
79102
raise Office365ClientError(response['status'], error_data)
80103
except Office365ClientError as e:
81104
exception = e

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from setuptools import find_packages, setup
33

44
setup(name='office365-rest-client',
5-
version='3.5.1',
6-
description='Python api wrapper for Office365 API v3.5.1',
5+
version='3.5.2',
6+
description='Python api wrapper for Office365 API v3.5.2',
77
author='SugarCRM',
88
packages=find_packages(),
99
zip_safe=False)

0 commit comments

Comments
 (0)