Skip to content

Commit d86649a

Browse files
Merge pull request #8 from loadsmart/handleExceptions
Handle Gmail exceptions
2 parents a93ec17 + 36d2223 commit d86649a

File tree

5 files changed

+198
-39
lines changed

5 files changed

+198
-39
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,27 @@ message = client.send(
104104
print(message) # Gmail message: ABC123
105105
```
106106

107+
- Handle exceptions
108+
109+
Exceptions are part of every developer day-to-day. You may want to handle exceptions as follows:
110+
111+
```python
112+
from gmail_wrapper.exceptions import (
113+
MessageNotFoundError,
114+
AttachmentNotFoundError,
115+
GmailError,
116+
)
117+
118+
try:
119+
do_something()
120+
except MessageNotFoundError as e:
121+
print(f"There is no message! {e}")
122+
except AttachmentNotFoundError as e:
123+
print(f"There is no attachment! {e}")
124+
except GmailError as e:
125+
print(f"Google servers are burning! {e}")
126+
```
127+
107128
## Need more help?
108129

109130
Reach `#eng-accounting` Slack channel.

gmail_wrapper/client.py

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
from google.auth.transport.requests import Request
66
from google.oauth2.credentials import Credentials
77
from googleapiclient import discovery
8+
from googleapiclient.errors import HttpError
89

910
from gmail_wrapper.entities import Message, AttachmentBody
11+
from gmail_wrapper.exceptions import (
12+
MessageNotFoundError,
13+
AttachmentNotFoundError,
14+
GmailError,
15+
)
1016

1117

1218
class GmailClient:
@@ -38,11 +44,17 @@ def _make_client(self, secrets_json_string, scopes):
3844
def _messages_resource(self):
3945
return self._client.users().messages()
4046

47+
def _execute(self, executable):
48+
try:
49+
return executable.execute()
50+
except HttpError as e:
51+
if e.resp.status >= 500:
52+
raise GmailError()
53+
raise e
54+
4155
def get_raw_messages(self, query="", limit=None):
42-
return (
43-
self._messages_resource()
44-
.list(userId=self.email, q=query, maxResults=limit)
45-
.execute()
56+
return self._execute(
57+
self._messages_resource().list(userId=self.email, q=query, maxResults=limit)
4658
)
4759

4860
def get_messages(self, query="", limit=None):
@@ -54,49 +66,65 @@ def get_messages(self, query="", limit=None):
5466
return [Message(self, raw_message) for raw_message in raw_messages["messages"]]
5567

5668
def get_raw_message(self, id):
57-
return self._messages_resource().get(userId=self.email, id=id).execute()
69+
try:
70+
return self._execute(
71+
self._messages_resource().get(userId=self.email, id=id)
72+
)
73+
except HttpError as e:
74+
if e.resp.status == 404:
75+
raise MessageNotFoundError(id)
76+
raise e
5877

5978
def get_message(self, id):
6079
raw_message = self.get_raw_message(id)
6180

6281
return Message(self, raw_message)
6382

6483
def modify_raw_message(self, id, add_labels=None, remove_labels=None):
65-
return (
66-
self._messages_resource()
67-
.modify(
68-
userId=self.email,
69-
id=id,
70-
body={
71-
"addLabelIds": add_labels if add_labels else [],
72-
"removeLabelIds": remove_labels if remove_labels else [],
73-
},
84+
try:
85+
return self._execute(
86+
self._messages_resource().modify(
87+
userId=self.email,
88+
id=id,
89+
body={
90+
"addLabelIds": add_labels if add_labels else [],
91+
"removeLabelIds": remove_labels if remove_labels else [],
92+
},
93+
)
7494
)
75-
.execute()
76-
)
95+
except HttpError as e:
96+
if e.resp.status == 404:
97+
raise MessageNotFoundError(id)
98+
raise e
7799

78100
def modify_message(self, id, add_labels=None, remove_labels=None):
79101
raw_modified_message = self.modify_raw_message(id, add_labels, remove_labels)
80102

81103
return Message(self, raw_modified_message)
82104

83105
def modify_multiple_messages(self, ids, add_labels=None, remove_labels=None):
84-
self._messages_resource().batchModify(
85-
userId=self.email,
86-
body={
87-
"ids": ids,
88-
"addLabelIds": add_labels if add_labels else [],
89-
"removeLabelIds": remove_labels if remove_labels else [],
90-
},
91-
).execute()
106+
self._execute(
107+
self._messages_resource().batchModify(
108+
userId=self.email,
109+
body={
110+
"ids": ids,
111+
"addLabelIds": add_labels if add_labels else [],
112+
"removeLabelIds": remove_labels if remove_labels else [],
113+
},
114+
)
115+
)
92116

93117
def get_raw_attachment_body(self, id, message_id):
94-
return (
95-
self._messages_resource()
96-
.attachments()
97-
.get(userId=self.email, id=id, messageId=message_id)
98-
.execute()
99-
)
118+
try:
119+
return self._execute(
120+
self._messages_resource()
121+
.attachments()
122+
.get(userId=self.email, id=id, messageId=message_id)
123+
)
124+
except HttpError as e:
125+
if e.resp.status == 404:
126+
raise AttachmentNotFoundError(message_id, id)
127+
raise e
100128

101129
def get_attachment_body(self, id, message_id):
102130
raw_attachment_body = self.get_raw_attachment_body(id, message_id)
@@ -121,8 +149,8 @@ def send_raw(self, subject, html_content, to, cc=None, bcc=None):
121149
subject, html_content, to, cc if cc else [], bcc if bcc else []
122150
)
123151

124-
return (
125-
self._messages_resource().send(userId=self.email, body=sendable).execute()
152+
return self._execute(
153+
self._messages_resource().send(userId=self.email, body=sendable)
126154
)
127155

128156
def send(self, subject, html_content, to, cc=None, bcc=None):

gmail_wrapper/exceptions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
class GmailError(Exception):
2+
def __str__(self):
3+
return f"GmailError: Gmail returned an internal server error"
4+
5+
6+
class MessageNotFoundError(Exception):
7+
def __init__(self, message_id):
8+
self.message_id = message_id
9+
10+
def __str__(self):
11+
return f"MessageNotFoundError: Gmail returned 404 when attempting to get message {self.message_id}"
12+
13+
14+
class AttachmentNotFoundError(Exception):
15+
def __init__(self, message_id, attachment_id):
16+
self.message_id = message_id
17+
self.attachment_id = attachment_id
18+
19+
def __str__(self):
20+
return f"AttachmentNotFoundError: Gmail returned 404 when attempting to get attachment {self.attachment_id} of message {self.message_id}"

tests/test_client.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import base64
22

3+
import pytest
4+
from googleapiclient.errors import HttpError
5+
36
from gmail_wrapper import GmailClient
47
from gmail_wrapper.entities import Message, AttachmentBody
8+
from gmail_wrapper.exceptions import (
9+
MessageNotFoundError,
10+
AttachmentNotFoundError,
11+
GmailError,
12+
)
513
from tests.utils import make_gmail_client
614

715

@@ -15,6 +23,19 @@ def test_it_returns_raw_messages(self, mocker, raw_incomplete_message):
1523
client = GmailClient(email="[email protected]", secrets_json_string="{}")
1624
assert client.get_raw_messages() == raw_response
1725

26+
def test_it_encapsulates_gmail_exceptions(self, mocker):
27+
server_error_response = mocker.MagicMock(status=500)
28+
mocker.patch(
29+
"gmail_wrapper.client.GmailClient._make_client",
30+
return_value=make_gmail_client(
31+
mocker, list_effect=HttpError(server_error_response, b"Content")
32+
),
33+
)
34+
35+
client = GmailClient(email="[email protected]", secrets_json_string="{}")
36+
with pytest.raises(GmailError):
37+
client.get_raw_messages()
38+
1839

1940
class TestGetMessages:
2041
def test_it_returns_a_message_list(self, mocker, client, raw_incomplete_message):
@@ -50,6 +71,25 @@ def test_it_returns_a_raw_message(self, mocker, raw_complete_message):
5071
message = client.get_raw_message("123AAB")
5172
assert message == raw_complete_message
5273

74+
@pytest.mark.parametrize(
75+
"error_code,exception_expected",
76+
[(500, GmailError), (404, MessageNotFoundError)],
77+
)
78+
def test_it_encapsulates_gmail_exceptions(
79+
self, mocker, error_code, exception_expected
80+
):
81+
error_response = mocker.MagicMock(status=error_code)
82+
mocker.patch(
83+
"gmail_wrapper.client.GmailClient._make_client",
84+
return_value=make_gmail_client(
85+
mocker, get_effect=HttpError(error_response, b"Content")
86+
),
87+
)
88+
89+
client = GmailClient(email="[email protected]", secrets_json_string="{}")
90+
with pytest.raises(exception_expected):
91+
client.get_raw_message("123AAB")
92+
5393

5494
class TestGetMessage:
5595
def test_it_returns_a_message(self, mocker, client, raw_complete_message):
@@ -77,6 +117,25 @@ def test_it_returns_a_raw_attachment_body(self, mocker, raw_attachment_body):
77117
)
78118
assert attachment_body == raw_attachment_body
79119

120+
@pytest.mark.parametrize(
121+
"error_code,exception_expected",
122+
[(500, GmailError), (404, AttachmentNotFoundError)],
123+
)
124+
def test_it_encapsulates_gmail_exceptions(
125+
self, mocker, error_code, exception_expected
126+
):
127+
error_response = mocker.MagicMock(status=error_code)
128+
mocker.patch(
129+
"gmail_wrapper.client.GmailClient._make_client",
130+
return_value=make_gmail_client(
131+
mocker, attachment_effect=HttpError(error_response, b"Content")
132+
),
133+
)
134+
135+
client = GmailClient(email="[email protected]", secrets_json_string="{}")
136+
with pytest.raises(exception_expected):
137+
client.get_raw_attachment_body(id="CCX457", message_id="123AAB")
138+
80139

81140
class TestGetAttachmentBody:
82141
def test_it_returns_an_attachment_body(self, mocker, client, raw_attachment_body):
@@ -107,6 +166,27 @@ def test_it_modifies_and_return_a_raw_message(self, mocker, raw_complete_message
107166
body={"addLabelIds": ["processed"], "removeLabelIds": ["phishing"]},
108167
)
109168

169+
@pytest.mark.parametrize(
170+
"error_code,exception_expected",
171+
[(500, GmailError), (404, MessageNotFoundError)],
172+
)
173+
def test_it_encapsulates_gmail_exceptions(
174+
self, mocker, error_code, exception_expected
175+
):
176+
error_response = mocker.MagicMock(status=error_code)
177+
mocker.patch(
178+
"gmail_wrapper.client.GmailClient._make_client",
179+
return_value=make_gmail_client(
180+
mocker, modify_effect=HttpError(error_response, b"Content")
181+
),
182+
)
183+
184+
client = GmailClient(email="[email protected]", secrets_json_string="{}")
185+
with pytest.raises(exception_expected):
186+
client.modify_raw_message(
187+
id="CCX457", add_labels=["processed"], remove_labels=["phishing"]
188+
)
189+
110190

111191
class TestModifyMessage:
112192
def test_it_returns_a_modified_message(self, client, mocker, raw_complete_message):

tests/utils.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ def make_gmail_client(
55
attachment_return=None,
66
modify_return=None,
77
send_return=None,
8+
list_effect=None,
9+
get_effect=None,
10+
attachment_effect=None,
11+
modify_effect=None,
812
):
913
return mocker.MagicMock(
1014
users=mocker.MagicMock(
@@ -13,23 +17,28 @@ def make_gmail_client(
1317
return_value=mocker.MagicMock(
1418
list=mocker.MagicMock(
1519
return_value=mocker.MagicMock(
16-
execute=mocker.MagicMock(return_value=list_return)
20+
execute=mocker.MagicMock(
21+
return_value=list_return, side_effect=list_effect
22+
)
1723
)
1824
),
1925
get=mocker.MagicMock(
2026
return_value=mocker.MagicMock(
21-
execute=mocker.MagicMock(return_value=get_return)
27+
execute=mocker.MagicMock(
28+
return_value=get_return, side_effect=get_effect
29+
)
2230
)
2331
),
2432
modify=mocker.MagicMock(
2533
return_value=mocker.MagicMock(
26-
execute=mocker.MagicMock(return_value=modify_return)
34+
execute=mocker.MagicMock(
35+
return_value=modify_return,
36+
side_effect=modify_effect,
37+
)
2738
)
2839
),
2940
batchModify=mocker.MagicMock(
30-
return_value=mocker.MagicMock(
31-
execute=mocker.MagicMock()
32-
)
41+
return_value=mocker.MagicMock(execute=mocker.MagicMock())
3342
),
3443
send=mocker.MagicMock(
3544
return_value=mocker.MagicMock(
@@ -41,7 +50,8 @@ def make_gmail_client(
4150
get=mocker.MagicMock(
4251
return_value=mocker.MagicMock(
4352
execute=mocker.MagicMock(
44-
return_value=attachment_return
53+
return_value=attachment_return,
54+
side_effect=attachment_effect,
4555
)
4656
)
4757
)

0 commit comments

Comments
 (0)