From 408810d6b41dc9f70fbb77c2f2f11bf167e1d80d Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sun, 15 Dec 2019 13:17:58 -0500 Subject: [PATCH] URL parameters preserved for attachment queries (#187) --- apprise/attachment/AttachBase.py | 63 ++++++++++++++++++++++++++------ apprise/attachment/AttachHTTP.py | 28 ++++++++++---- test/test_apprise_attachments.py | 8 +++- test/test_attach_http.py | 53 ++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 22 deletions(-) diff --git a/apprise/attachment/AttachBase.py b/apprise/attachment/AttachBase.py index 3fdbbf58..1fde66f4 100644 --- a/apprise/attachment/AttachBase.py +++ b/apprise/attachment/AttachBase.py @@ -28,6 +28,7 @@ import time import mimetypes from ..URLBase import URLBase from ..utils import parse_bool +from ..AppriseLocale import gettext_lazy as _ class AttachBase(URLBase): @@ -61,7 +62,35 @@ class AttachBase(URLBase): # 5 MB = 5242880 bytes max_file_size = 5242880 - def __init__(self, name=None, mimetype=None, cache=True, **kwargs): + # Here is where we define all of the arguments we accept on the url + # such as: schema://whatever/?overflow=upstream&format=text + # These act the same way as tokens except they are optional and/or + # have default values set if mandatory. This rule must be followed + template_args = { + 'cache': { + 'name': _('Cache Age'), + 'type': 'int', + # We default to (600) which means we cache for 10 minutes + 'default': 600, + }, + 'mime': { + 'name': _('Forced Mime Type'), + 'type': 'string', + }, + 'name': { + 'name': _('Forced File Name'), + 'type': 'string', + }, + 'verify': { + 'name': _('Verify SSL'), + # SSL Certificate Authority Verification + 'type': 'bool', + # Provide a default + 'default': True, + }, + } + + def __init__(self, name=None, mimetype=None, cache=None, **kwargs): """ Initialize some general logging and common server arguments that will keep things consistent when working with the configurations that @@ -109,19 +138,27 @@ class AttachBase(URLBase): # Absolute path to attachment self.download_path = None - # Set our cache flag; it can be True or a (positive) integer - try: - self.cache = cache if isinstance(cache, bool) else int(cache) + # Set our cache flag; it can be True, False, None, or a (positive) + # integer... nothing else + if cache is not None: + try: + self.cache = cache if isinstance(cache, bool) else int(cache) + + except (TypeError, ValueError): + err = 'An invalid cache value ({}) was specified.'.format( + cache) + self.logger.warning(err) + raise TypeError(err) + + # Some simple error checking if self.cache < 0: err = 'A negative cache value ({}) was specified.'.format( cache) self.logger.warning(err) raise TypeError(err) - except (ValueError, TypeError): - err = 'An invalid cache value ({}) was specified.'.format(cache) - self.logger.warning(err) - raise TypeError(err) + else: + self.cache = None # Validate mimetype if specified if self._mimetype: @@ -211,12 +248,16 @@ class AttachBase(URLBase): Simply returns true if the object has downloaded and stored the attachment AND the attachment has not expired. """ + + cache = self.template_args['cache']['default'] \ + if self.cache is None else self.cache + if self.download_path and os.path.isfile(self.download_path) \ - and self.cache: + and cache: # We have enough reason to look further into our cached content # and verify it has not expired. - if self.cache is True: + if cache is True: # return our fixed content as is; we will always cache it return True @@ -224,7 +265,7 @@ class AttachBase(URLBase): # content again. try: age_in_sec = time.time() - os.stat(self.download_path).st_mtime - if age_in_sec <= self.cache: + if age_in_sec <= cache: return True except (OSError, IOError): diff --git a/apprise/attachment/AttachHTTP.py b/apprise/attachment/AttachHTTP.py index f5986fbb..046babdd 100644 --- a/apprise/attachment/AttachHTTP.py +++ b/apprise/attachment/AttachHTTP.py @@ -78,6 +78,11 @@ class AttachHTTP(AttachBase): # Where our content is written to upon a call to download. self._temp_file = None + # Our Query String Dictionary; we use this to track arguments + # specified that aren't otherwise part of this class + self.qsd = {k: v for k, v in kwargs.get('qsd', {}).items() + if k not in self.template_args} + return def download(self, **kwargs): @@ -122,6 +127,7 @@ class AttachHTTP(AttachBase): url, headers=headers, auth=auth, + params=self.qsd, verify=self.verify_certificate, timeout=self.connection_timeout_sec, stream=True) as r: @@ -252,18 +258,21 @@ class AttachHTTP(AttachBase): Returns the URL built dynamically based on specified arguments. """ - # Prepare our cache value - if isinstance(self.cache, bool) or not self.cache: - cache = 'yes' if self.cache else 'no' - else: - cache = int(self.cache) - # Define any arguments set args = { 'verify': 'yes' if self.verify_certificate else 'no', - 'cache': cache, } + # Prepare our cache value + if self.cache is not None: + if isinstance(self.cache, bool) or not self.cache: + cache = 'yes' if self.cache else 'no' + else: + cache = int(self.cache) + + # Set our cache value + args['cache'] = cache + if self._mimetype: # A format was enforced args['mime'] = self._mimetype @@ -275,6 +284,9 @@ class AttachHTTP(AttachBase): # Append our headers into our args args.update({'+{}'.format(k): v for k, v in self.headers.items()}) + # Apply any remaining entries to our URL + args.update(self.qsd) + # Determine Authentication auth = '' if self.user and self.password: @@ -290,7 +302,7 @@ class AttachHTTP(AttachBase): default_port = 443 if self.secure else 80 - return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format( + return '{schema}://{auth}{hostname}{port}{fullpath}?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, hostname=self.quote(self.host, safe=''), diff --git a/test/test_apprise_attachments.py b/test/test_apprise_attachments.py index 7e78994a..85ec6986 100644 --- a/test/test_apprise_attachments.py +++ b/test/test_apprise_attachments.py @@ -78,14 +78,14 @@ def test_apprise_attachment(): assert aa # Add another entry already in it's AttachBase format - response = AppriseAttachment.instantiate(path) + response = AppriseAttachment.instantiate(path, cache=True) assert isinstance(response, AttachBase) assert aa.add(response, asset=AppriseAsset()) # There is now 2 attachments assert len(aa) == 2 - # No cache set, so our cache defaults to True + # Cache is initialized to True assert aa[1].cache is True # Reset our object @@ -173,6 +173,10 @@ def test_apprise_attachment(): assert not aa.add(AppriseAttachment.instantiate( 'file://{}?name=andanother.png&cache=-600'.format(path))) + # Invalid cache value + assert not aa.add(AppriseAttachment.instantiate( + 'file://{}?name=andanother.png'.format(path), cache='invalid')) + # No length change assert len(aa) == 3 diff --git a/test/test_attach_http.py b/test/test_attach_http.py index 991cf910..1be04f6c 100644 --- a/test/test_attach_http.py +++ b/test/test_attach_http.py @@ -73,6 +73,39 @@ def test_attach_http_parse_url(): assert AttachHTTP.parse_url('http://') is None +def test_attach_http_query_string_dictionary(): + """ + API: AttachHTTP() Query String Dictionary + + """ + + # no qsd specified + results = AttachHTTP.parse_url('http://localhost') + assert isinstance(results, dict) + + # Create our object + obj = AttachHTTP(**results) + assert isinstance(obj, AttachHTTP) + + assert re.search(r'[?&]verify=yes', obj.url()) + + # Now lets create a URL with a custom Query String entry + + # some custom qsd entries specified + results = AttachHTTP.parse_url('http://localhost?dl=1&_var=test') + assert isinstance(results, dict) + + # Create our object + obj = AttachHTTP(**results) + assert isinstance(obj, AttachHTTP) + + assert re.search(r'[?&]verify=yes', obj.url()) + + # But now test that our custom arguments have also been set + assert re.search(r'[?&]dl=1', obj.url()) + assert re.search(r'[?&]_var=test', obj.url()) + + @mock.patch('requests.get') def test_attach_http(mock_get): """ @@ -147,8 +180,26 @@ def test_attach_http(mock_get): dummy_response = DummyResponse() mock_get.return_value = dummy_response + # Test custom url get parameters results = AttachHTTP.parse_url( - 'http://user:pass@localhost/apprise.gif?+key=value') + 'http://user:pass@localhost/apprise.gif?dl=1&cache=300') + assert isinstance(results, dict) + attachment = AttachHTTP(**results) + assert isinstance(attachment.url(), six.string_types) is True + + # Test that our extended variables are passed along + assert mock_get.call_count == 0 + assert attachment + assert mock_get.call_count == 1 + assert 'params' in mock_get.call_args_list[0][1] + assert 'dl' in mock_get.call_args_list[0][1]['params'] + + # Verify that arguments that are reserved for apprise are not + # passed along + assert 'cache' not in mock_get.call_args_list[0][1]['params'] + + results = AttachHTTP.parse_url( + 'http://user:pass@localhost/apprise.gif?+key=value&cache=True') assert isinstance(results, dict) attachment = AttachHTTP(**results) assert isinstance(attachment.url(), six.string_types) is True