Browse Source

Refactored base64 attachment handling (#1191)

pull/622/merge
Chris Caron 3 months ago committed by GitHub
parent
commit
98fb4865fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      apprise/attachment/base.py
  2. 19
      apprise/attachment/memory.py
  3. 40
      apprise/plugins/apprise_api.py
  4. 3
      apprise/plugins/custom_form.py
  5. 37
      apprise/plugins/custom_json.py
  6. 44
      apprise/plugins/custom_xml.py
  7. 8
      apprise/plugins/email.py
  8. 8
      apprise/plugins/mailgun.py
  9. 5
      apprise/plugins/pushbullet.py
  10. 41
      apprise/plugins/pushsafer.py
  11. 12
      apprise/plugins/sendgrid.py
  12. 7
      apprise/plugins/ses.py
  13. 23
      apprise/plugins/signal_api.py
  14. 5
      apprise/plugins/slack.py
  15. 32
      apprise/plugins/smseagle.py
  16. 38
      apprise/plugins/smtp2go.py
  17. 42
      apprise/plugins/sparkpost.py
  18. 10
      apprise/plugins/twitter.py
  19. 32
      test/test_attach_http.py
  20. 10
      test/test_attach_memory.py
  21. 6
      test/test_plugin_sendgrid.py

4
apprise/attachment/base.py

@ -291,7 +291,7 @@ class AttachBase(URLBase):
return False if not retrieve_if_missing else self.download()
def base64(self, encoding='utf-8'):
def base64(self, encoding='ascii'):
"""
Returns the attachment object as a base64 string otherwise
None is returned if an error occurs.
@ -306,7 +306,7 @@ class AttachBase(URLBase):
raise exception.AppriseFileNotFound("Attachment Missing")
try:
with open(self.path, 'rb') as f:
with self.open() as f:
# Prepare our Attachment in Base64
return base64.b64encode(f.read()).decode(encoding) \
if encoding else base64.b64encode(f.read())

19
apprise/attachment/memory.py

@ -29,7 +29,9 @@
import re
import os
import io
import base64
from .base import AttachBase
from .. import exception
from ..common import ContentLocation
from ..locale import gettext_lazy as _
import uuid
@ -145,6 +147,23 @@ class AttachMemory(AttachBase):
return True
def base64(self, encoding='ascii'):
"""
We need to over-ride this since the base64 sub-library seems to close
our file descriptor making it no longer referencable.
"""
if not self:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
self.url(privacy=True)))
raise exception.AppriseFileNotFound("Attachment Missing")
self._data.seek(0, 0)
return base64.b64encode(self._data.read()).decode(encoding) \
if encoding else base64.b64encode(self._data.read())
def invalidate(self):
"""
Removes data

40
apprise/plugins/apprise_api.py

@ -29,8 +29,8 @@
import re
import requests
from json import dumps
import base64
from .. import exception
from .base import NotifyBase
from ..url import PrivacyMode
from ..common import NotifyType
@ -261,39 +261,45 @@ class NotifyAppriseAPI(NotifyBase):
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access Apprise API attachment {}.'.format(
attachment.url(privacy=True)))
return False
try:
# Our Attachment filename
filename = attachment.name \
if attachment.name else f'file{no:03}.dat'
if self.method == AppriseAPIMethod.JSON:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({
'filename': attachment.name,
'base64': base64.b64encode(f.read())
.decode('utf-8'),
'mimetype': attachment.mimetype,
})
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({
"filename": filename,
'base64': attachment.base64(),
'mimetype': attachment.mimetype,
})
else: # AppriseAPIMethod.FORM
files.append((
'file{:02d}'.format(no),
(
attachment.name,
filename,
open(attachment.path, 'rb'),
attachment.mimetype,
)
))
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
except (TypeError, OSError, exception.AppriseException):
# We could not access the attachment
self.logger.error(
'Could not access AppriseAPI attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Appending AppriseAPI attachment {}'.format(
attachment.url(privacy=True)))
# prepare Apprise API Object
payload = {
# Apprise API Payload

3
apprise/plugins/custom_form.py

@ -302,7 +302,8 @@ class NotifyForm(NotifyBase):
files.append((
self.attach_as.format(no)
if self.attach_multi_support else self.attach_as, (
attachment.name,
attachment.name
if attachment.name else f'file{no:03}.dat',
open(attachment.path, 'rb'),
attachment.mimetype)
))

37
apprise/plugins/custom_json.py

@ -27,9 +27,9 @@
# POSSIBILITY OF SUCH DAMAGE.
import requests
import base64
from json import dumps
from .. import exception
from .base import NotifyBase
from ..url import PrivacyMode
from ..common import NotifyImageSize
@ -213,33 +213,34 @@ class NotifyJSON(NotifyBase):
# Track our potential attachments
attachments = []
if attach and self.attachment_support:
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access Custom JSON attachment {}.'.format(
attachment.url(privacy=True)))
return False
try:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({
'filename': attachment.name,
'base64': base64.b64encode(f.read())
.decode('utf-8'),
'mimetype': attachment.mimetype,
})
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
attachments.append({
"filename": attachment.name
if attachment.name else f'file{no:03}.dat',
'base64': attachment.base64(),
'mimetype': attachment.mimetype,
})
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access Custom JSON attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Appending Custom JSON attachment {}'.format(
attachment.url(privacy=True)))
# Prepare JSON Object
payload = {
JSONPayloadField.VERSION: self.json_version,

44
apprise/plugins/custom_xml.py

@ -28,8 +28,8 @@
import re
import requests
import base64
from .. import exception
from .base import NotifyBase
from ..url import PrivacyMode
from ..common import NotifyImageSize
@ -287,35 +287,39 @@ class NotifyXML(NotifyBase):
attachments = []
if attach and self.attachment_support:
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access Custom XML attachment {}.'.format(
attachment.url(privacy=True)))
return False
try:
with open(attachment.path, 'rb') as f:
# Prepare our Attachment in Base64
entry = \
'<Attachment filename="{}" mimetype="{}">'.format(
NotifyXML.escape_html(
attachment.name, whitespace=False),
NotifyXML.escape_html(
attachment.mimetype, whitespace=False))
entry += base64.b64encode(f.read()).decode('utf-8')
entry += '</Attachment>'
attachments.append(entry)
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
# Prepare our Attachment in Base64
entry = \
'<Attachment filename="{}" mimetype="{}">'.format(
NotifyXML.escape_html(
attachment.name if attachment.name
else f'file{no:03}.dat', whitespace=False),
NotifyXML.escape_html(
attachment.mimetype, whitespace=False))
entry += attachment.base64()
entry += '</Attachment>'
attachments.append(entry)
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access Custom XML attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Appending Custom XML attachment {}'.format(
attachment.url(privacy=True)))
# Update our xml_attachments record:
xml_attachments = \
'<Attachments format="base64">' + \

8
apprise/plugins/email.py

@ -799,7 +799,7 @@ class NotifyEmail(NotifyBase):
mixed = MIMEMultipart("mixed")
mixed.attach(base)
# Now store our attachments
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
if not attachment:
# We could not load the attachment; take an early
# exit since this isn't what the end user wanted
@ -819,10 +819,14 @@ class NotifyEmail(NotifyBase):
app = MIMEApplication(abody.read())
app.set_type(attachment.mimetype)
# Prepare our attachment name
filename = attachment.name \
if attachment.name else f'file{no:03}.dat'
app.add_header(
'Content-Disposition',
'attachment; filename="{}"'.format(
Header(attachment.name, 'utf-8')),
Header(filename, 'utf-8')),
)
mixed.attach(app)
base = mixed

8
apprise/plugins/mailgun.py

@ -383,9 +383,15 @@ class NotifyMailgun(NotifyBase):
self.logger.debug(
'Preparing Mailgun attachment {}'.format(
attachment.url(privacy=True)))
# Prepare our filename
filename = attachment.name \
if attachment.name \
else 'file{no:03}.dat'.format(no=idx + 1)
try:
files['attachment[{}]'.format(idx)] = \
(attachment.name, open(attachment.path, 'rb'))
(filename, open(attachment.path, 'rb'))
except (OSError, IOError) as e:
self.logger.warning(

5
apprise/plugins/pushbullet.py

@ -152,7 +152,7 @@ class NotifyPushBullet(NotifyBase):
if attach and self.attachment_support:
# We need to upload our payload first so that we can source it
# in remaining messages
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
@ -168,7 +168,8 @@ class NotifyPushBullet(NotifyBase):
# prepare payload
payload = {
'file_name': attachment.name,
'file_name': attachment.name
if attachment.name else f'file{no:03}.dat',
'file_type': attachment.mimetype,
}
# First thing we need to do is make a request so that we can

41
apprise/plugins/pushsafer.py

@ -26,11 +26,11 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import base64
import requests
from json import loads
from .base import NotifyBase
from .. import exception
from ..common import NotifyType
from ..utils import parse_list
from ..utils import validate_regex
@ -548,12 +548,12 @@ class NotifyPushSafer(NotifyBase):
if attach and self.attachment_support:
# We need to upload our payload first so that we can source it
# in remaining messages
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# prepare payload
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access PushSafer attachment {}.'.format(
attachment.url(privacy=True)))
return False
@ -569,24 +569,27 @@ class NotifyPushSafer(NotifyBase):
attachment.url(privacy=True)))
try:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachment = (
attachment.name,
'data:{};base64,{}'.format(
attachment.mimetype,
base64.b64encode(f.read())))
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append((
attachment.name
if attachment.name else f'file{no:03}.dat',
'data:{};base64,{}'.format(
attachment.mimetype,
attachment.base64(),
)
))
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access PushSafer attachment {}.'.format(
attachment.url(privacy=True)))
return False
# Save our pre-prepared payload for attachment posting
attachments.append(attachment)
self.logger.debug(
'Appending PushSafer attachment {}'.format(
attachment.url(privacy=True)))
# Create a copy of the targets list
targets = list(self.targets)

12
apprise/plugins/sendgrid.py

@ -342,11 +342,19 @@ class NotifySendGrid(NotifyBase):
# Send our attachments
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access SendGrid attachment {}.'.format(
attachment.url(privacy=True)))
return False
try:
attachments.append({
"content": attachment.base64(),
"filename": attachment.name
if attachment.name else f'attach{no:03}.dat',
if attachment.name else f'file{no:03}.dat',
"type": "application/octet-stream",
"disposition": "attachment"
})
@ -354,7 +362,7 @@ class NotifySendGrid(NotifyBase):
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access SendGrid attachment {}.'.format(
attachment.url(privacy=True)))
return False

7
apprise/plugins/ses.py

@ -448,7 +448,7 @@ class NotifySES(NotifyBase):
base.attach(content)
# Now store our attachments
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
if not attachment:
# We could not load the attachment; take an early
# exit since this isn't what the end user wanted
@ -468,10 +468,13 @@ class NotifySES(NotifyBase):
app = MIMEApplication(abody.read())
app.set_type(attachment.mimetype)
filename = attachment.name \
if attachment.name else f'file{no:03}.dat'
app.add_header(
'Content-Disposition',
'attachment; filename="{}"'.format(
Header(attachment.name, 'utf-8')),
Header(filename, 'utf-8')),
)
base.attach(app)

23
apprise/plugins/signal_api.py

@ -29,10 +29,10 @@
import re
import requests
from json import dumps
import base64
from .base import NotifyBase
from ..common import NotifyType
from .. import exception
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import parse_bool
@ -239,23 +239,24 @@ class NotifySignalAPI(NotifyBase):
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access Signal API attachment {}.'.format(
attachment.url(privacy=True)))
return False
try:
with open(attachment.path, 'rb') as f:
# Prepare our Attachment in Base64
attachments.append(
base64.b64encode(f.read()).decode('utf-8'))
attachments.append(attachment.base64())
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access Signal API attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Appending Signal API attachment {}'.format(
attachment.url(privacy=True)))
# Prepare our headers
headers = {
'User-Agent': self.app_id,

5
apprise/plugins/slack.py

@ -646,7 +646,7 @@ class NotifySlack(NotifyBase):
if attach and self.attachment_support and \
self.mode is SlackMode.BOT and attach_channel_list:
# Send our attachments (can only be done in bot mode)
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
@ -663,7 +663,8 @@ class NotifySlack(NotifyBase):
# Get the URL to which to upload the file.
# https://api.slack.com/methods/files.getUploadURLExternal
_params = {
'filename': attachment.name,
'filename': attachment.name
if attachment.name else f'file{no:03}.dat',
'length': len(attachment),
}
_url = self.api_url.format('files.getUploadURLExternal')

32
apprise/plugins/smseagle.py

@ -29,11 +29,11 @@
import re
import requests
from json import dumps, loads
import base64
from itertools import chain
from .base import NotifyBase
from ..common import NotifyType
from .. import exception
from ..utils import validate_regex
from ..utils import is_phone_no
from ..utils import parse_phone_no
@ -345,7 +345,7 @@ class NotifySMSEagle(NotifyBase):
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access SMSEagle attachment {}.'.format(
attachment.url(privacy=True)))
return False
@ -357,21 +357,23 @@ class NotifySMSEagle(NotifyBase):
continue
try:
with open(attachment.path, 'rb') as f:
# Prepare our Attachment in Base64
attachments.append({
'content_type': attachment.mimetype,
'content': base64.b64encode(
f.read()).decode('utf-8'),
})
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
# Prepare our Attachment in Base64
attachments.append({
'content_type': attachment.mimetype,
'content': attachment.base64(),
})
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access SMSEagle attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Appending SMSEagle attachment {}'.format(
attachment.url(privacy=True)))
# Prepare our headers
headers = {
'User-Agent': self.app_id,

38
apprise/plugins/smtp2go.py

@ -45,11 +45,11 @@
# the email will be transmitted from. If no email address is specified
# then it will also become the 'to' address as well.
#
import base64
import requests
from json import dumps
from email.utils import formataddr
from .base import NotifyBase
from .. import exception
from ..common import NotifyType
from ..common import NotifyFormat
from ..utils import parse_emails
@ -294,33 +294,35 @@ class NotifySMTP2Go(NotifyBase):
attachments = []
if attach and self.attachment_support:
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access SMTP2Go attachment {}.'.format(
attachment.url(privacy=True)))
return False
try:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({
'filename': attachment.name,
'fileblob': base64.b64encode(f.read())
.decode('utf-8'),
'mimetype': attachment.mimetype,
})
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
# Format our attachment
attachments.append({
'filename': attachment.name
if attachment.name else f'file{no:03}.dat',
'fileblob': attachment.base64(),
'mimetype': attachment.mimetype,
})
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access SMTP2Go attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Appending SMTP2Go attachment {}'.format(
attachment.url(privacy=True)))
sender = formataddr(
(self.from_name if self.from_name else False,
self.from_addr), charset='utf-8')

42
apprise/plugins/sparkpost.py

@ -55,10 +55,10 @@
# API Documentation: https://developers.sparkpost.com/api/
# Specifically: https://developers.sparkpost.com/api/transmissions/
import requests
import base64
from json import loads
from json import dumps
from .base import NotifyBase
from .. import exception
from ..common import NotifyType
from ..common import NotifyFormat
from ..utils import is_email
@ -500,7 +500,7 @@ class NotifySparkPost(NotifyBase):
if not self.targets:
# There is no one to email; we're done
self.logger.warning(
'There are no Email recipients to notify')
'There are no SparkPost Email recipients to notify')
return False
# Initialize our has_error flag
@ -546,35 +546,35 @@ class NotifySparkPost(NotifyBase):
# Prepare ourselves an attachment object
payload['content']['attachments'] = []
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
'Could not access SparkPost attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Preparing SparkPost attachment {}'.format(
attachment.url(privacy=True)))
try:
with open(attachment.path, 'rb') as fp:
# Prepare API Upload Payload
payload['content']['attachments'].append({
'name': attachment.name,
'type': attachment.mimetype,
'data': base64.b64encode(fp.read()).decode("ascii")
})
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
# Prepare API Upload Payload
payload['content']['attachments'].append({
'name': attachment.name
if attachment.name else f'file{no:03}.dat',
'type': attachment.mimetype,
'data': attachment.base64(),
})
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access SparkPost attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Appending SparkPost attachment {}'.format(
attachment.url(privacy=True)))
# Take a copy of our token dictionary
tokens = self.tokens.copy()

10
apprise/plugins/twitter.py

@ -287,7 +287,7 @@ class NotifyTwitter(NotifyBase):
if attach and self.attachment_support:
# We need to upload our payload first so that we can source it
# in remaining messages
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
@ -320,11 +320,15 @@ class NotifyTwitter(NotifyBase):
# We can't post our attachment
return False
# Prepare our filename
filename = attachment.name \
if attachment.name else f'file{no:03}.dat'
if not (isinstance(response, dict)
and response.get('media_id')):
self.logger.debug(
'Could not attach the file to Twitter: %s (mime=%s)',
attachment.name, attachment.mimetype)
filename, attachment.mimetype)
continue
# If we get here, our output will look something like this:
@ -344,7 +348,7 @@ class NotifyTwitter(NotifyBase):
response.update({
# Update our response to additionally include the
# attachment details
'file_name': attachment.name,
'file_name': filename,
'file_mime': attachment.mimetype,
'file_path': attachment.path,
})

32
test/test_attach_http.py

@ -29,6 +29,8 @@
import re
from unittest import mock
import pytest
from apprise import exception
import requests
import mimetypes
from os.path import join
@ -481,3 +483,33 @@ def test_attach_http(mock_get, mock_post):
assert mock_post.call_count == 30
# We only fetched once and re-used the same fetch for all posts
assert mock_get.call_count == 1
#
# We will test our base64 handling now
#
mock_get.reset_mock()
mock_post.reset_mock()
AttachHTTP.max_file_size = getsize(path)
# Set ourselves a Content-Disposition (providing a filename)
dummy_response.headers['Content-Disposition'] = \
'attachment; filename="myimage.gif"'
results = AttachHTTP.parse_url('http://user@localhost/filename.gif')
assert isinstance(results, dict)
obj = AttachHTTP(**results)
# now test our base64 output
assert isinstance(obj.base64(), str)
# No encoding if we choose
assert isinstance(obj.base64(encoding=None), bytes)
# Error cases:
with mock.patch("builtins.open", new_callable=mock.mock_open,
read_data="mocked file content") as mock_file:
mock_file.side_effect = FileNotFoundError
with pytest.raises(exception.AppriseFileNotFound):
obj.base64()
mock_file.side_effect = OSError
with pytest.raises(exception.AppriseDiskIOError):
obj.base64()

10
test/test_attach_memory.py

@ -30,6 +30,7 @@ import re
import urllib
import pytest
from apprise import exception
from apprise.attachment.base import AttachBase
from apprise.attachment.memory import AttachMemory
from apprise import AppriseAttachment
@ -203,3 +204,12 @@ def test_attach_memory():
# Test hosted configuration and that we can't add a valid memory file
aa = AppriseAttachment(location=ContentLocation.HOSTED)
assert aa.add(response) is False
# now test our base64 output
assert isinstance(response.base64(), str)
# No encoding if we choose
assert isinstance(response.base64(encoding=None), bytes)
response.invalidate()
with pytest.raises(exception.AppriseFileNotFound):
response.base64()

6
test/test_plugin_sendgrid.py

@ -196,6 +196,12 @@ def test_plugin_sendgrid_attachments(mock_post, mock_get):
mock_post.reset_mock()
mock_get.reset_mock()
# Try again in a use case where we can't access the file
with mock.patch("os.path.isfile", return_value=False):
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is False
# Try again in a use case where we can't access the file
with mock.patch("builtins.open", side_effect=OSError):
assert obj.notify(

Loading…
Cancel
Save