Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-apprise for openSUSE:Factory checked in at 2024-04-04 22:26:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-apprise (Old) and /work/SRC/openSUSE:Factory/.python-apprise.new.1905 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-apprise" Thu Apr 4 22:26:00 2024 rev:4 rq:1164408 version:1.7.5 Changes: -------- --- /work/SRC/openSUSE:Factory/python-apprise/python-apprise.changes 2024-03-11 15:38:44.645047888 +0100 +++ /work/SRC/openSUSE:Factory/.python-apprise.new.1905/python-apprise.changes 2024-04-04 22:27:04.433111844 +0200 @@ -1,0 +2,12 @@ +Wed Apr 3 16:32:29 UTC 2024 - Joshua Smith <smolsh...@opensuse.org> + +- Update to version 1.7.5: + Features: + * Free-Mobile support added + * Improved markdown to html conversions + * Added test case to strengthen YAML configuration validation + Bugfixes: + * Improved async/threading attachment support + * Improved variable parsing in YAML files + +------------------------------------------------------------------- Old: ---- apprise-1.7.4.tar.gz New: ---- apprise-1.7.5.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-apprise.spec ++++++ --- /var/tmp/diff_new_pack.poAacG/_old 2024-04-04 22:27:05.509151460 +0200 +++ /var/tmp/diff_new_pack.poAacG/_new 2024-04-04 22:27:05.509151460 +0200 @@ -26,7 +26,7 @@ %endif Name: python-apprise -Version: 1.7.4 +Version: 1.7.5 Release: 0 Group: Development/Libraries/Python Summary: A simple wrapper to many popular notification services used today @@ -111,13 +111,13 @@ %python_expand %fdupes %{buildroot}%{$python_sitelib} %check -%pytest +%pytest -k 'not test_plugin_matrix_attachments_api_v2' %pre %python_libalternatives_reset_alternative apprise %post -%python_install_alternative apprise apprise.1 +%python_install_alternative apprise apprise.1%{?ext_man} %postun %python_uninstall_alternative apprise ++++++ apprise-1.7.4.tar.gz -> apprise-1.7.5.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/KEYWORDS new/apprise-1.7.5/KEYWORDS --- old/apprise-1.7.4/KEYWORDS 2024-03-09 22:39:52.000000000 +0100 +++ new/apprise-1.7.5/KEYWORDS 2024-03-30 14:53:24.000000000 +0100 @@ -21,6 +21,7 @@ FCM Flock Form +Free Mobile Gnome Google Chat Gotify diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/PKG-INFO new/apprise-1.7.5/PKG-INFO --- old/apprise-1.7.4/PKG-INFO 2024-03-09 22:41:33.454005200 +0100 +++ new/apprise-1.7.5/PKG-INFO 2024-03-30 14:59:27.513028000 +0100 @@ -1,12 +1,12 @@ Metadata-Version: 2.1 Name: apprise -Version: 1.7.4 +Version: 1.7.5 Summary: Push Notifications that work with just about every platform! Home-page: https://github.com/caronc/apprise Author: Chris Caron Author-email: lead2g...@gmail.com License: BSD -Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line LunaSea MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Revolt Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip +Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Free Mobile Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line LunaSea MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Revolt Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators @@ -177,6 +177,7 @@ | [DAPNET](https://github.com/caronc/apprise/wiki/Notify_dapnet) | dapnet:// | (TCP) 80 | dapnet://user:pass@callsign<br/>dapnet://user:pass@callsign1/callsign2/callsignN | [D7 Networks](https://github.com/caronc/apprise/wiki/Notify_d7networks) | d7sms:// | (TCP) 443 | d7sms://token@PhoneNo<br/>d7sms://token@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [DingTalk](https://github.com/caronc/apprise/wiki/Notify_dingtalk) | dingtalk:// | (TCP) 443 | dingtalk://token/<br />dingtalk://token/ToPhoneNo<br />dingtalk://token/ToPhoneNo1/ToPhoneNo2/ToPhoneNo1/ +| [Free-Mobile](https://github.com/caronc/apprise/wiki/Notify_freemobile) | freemobile:// | (TCP) 443 | freemobile://user@password/ [httpSMS](https://github.com/caronc/apprise/wiki/Notify_httpsms) | httpsms:// | (TCP) 443 | httpsms://ApiKey@FromPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Kavenegar](https://github.com/caronc/apprise/wiki/Notify_kavenegar) | kavenegar:// | (TCP) 443 | kavenegar://ApiKey/ToPhoneNo<br/>kavenegar://FromPhoneNo@ApiKey/ToPhoneNo<br/>kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [MessageBird](https://github.com/caronc/apprise/wiki/Notify_messagebird) | msgbird:// | (TCP) 443 | msgbird://ApiKey/FromPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/README.md new/apprise-1.7.5/README.md --- old/apprise-1.7.4/README.md 2024-03-09 22:39:52.000000000 +0100 +++ new/apprise-1.7.5/README.md 2024-03-30 14:53:24.000000000 +0100 @@ -146,6 +146,7 @@ | [DAPNET](https://github.com/caronc/apprise/wiki/Notify_dapnet) | dapnet:// | (TCP) 80 | dapnet://user:pass@callsign<br/>dapnet://user:pass@callsign1/callsign2/callsignN | [D7 Networks](https://github.com/caronc/apprise/wiki/Notify_d7networks) | d7sms:// | (TCP) 443 | d7sms://token@PhoneNo<br/>d7sms://token@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [DingTalk](https://github.com/caronc/apprise/wiki/Notify_dingtalk) | dingtalk:// | (TCP) 443 | dingtalk://token/<br />dingtalk://token/ToPhoneNo<br />dingtalk://token/ToPhoneNo1/ToPhoneNo2/ToPhoneNo1/ +| [Free-Mobile](https://github.com/caronc/apprise/wiki/Notify_freemobile) | freemobile:// | (TCP) 443 | freemobile://user@password/ [httpSMS](https://github.com/caronc/apprise/wiki/Notify_httpsms) | httpsms:// | (TCP) 443 | httpsms://ApiKey@FromPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Kavenegar](https://github.com/caronc/apprise/wiki/Notify_kavenegar) | kavenegar:// | (TCP) 443 | kavenegar://ApiKey/ToPhoneNo<br/>kavenegar://FromPhoneNo@ApiKey/ToPhoneNo<br/>kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [MessageBird](https://github.com/caronc/apprise/wiki/Notify_messagebird) | msgbird:// | (TCP) 443 | msgbird://ApiKey/FromPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/URLBase.py new/apprise-1.7.5/apprise/URLBase.py --- old/apprise-1.7.4/apprise/URLBase.py 2024-02-18 16:11:50.000000000 +0100 +++ new/apprise-1.7.5/apprise/URLBase.py 2024-03-29 21:20:13.000000000 +0100 @@ -670,6 +670,79 @@ } @staticmethod + def post_process_parse_url_results(results): + """ + After parsing the URL, this function applies a bit of extra logic to + support extra entries like `pass` becoming `password`, etc + + This function assumes that parse_url() was called previously setting + up the basics to be checked + """ + + # if our URL ends with an 's', then assume our secure flag is set. + results['secure'] = (results['schema'][-1] == 's') + + # QSD Checking (over-rides all) + qsd_exists = True if isinstance(results.get('qsd'), dict) else False + + if qsd_exists and 'verify' in results['qsd']: + # Pulled from URL String + results['verify'] = parse_bool( + results['qsd'].get('verify', True)) + + elif 'verify' in results: + # Pulled from YAML Configuratoin + results['verify'] = parse_bool(results.get('verify', True)) + + else: + # Support SSL Certificate 'verify' keyword. Default to being + # enabled + results['verify'] = True + + # Password overrides + if 'pass' in results: + results['password'] = results['pass'] + del results['pass'] + + if qsd_exists: + if 'password' in results['qsd']: + results['password'] = results['qsd']['password'] + if 'pass' in results['qsd']: + results['password'] = results['qsd']['pass'] + + # User overrides + if 'user' in results['qsd']: + results['user'] = results['qsd']['user'] + + # parse_url() always creates a 'password' and 'user' entry in the + # results returned. Entries are set to None if they weren't + # specified + if results['password'] is None and 'user' in results['qsd']: + # Handle cases where the user= provided in 2 locations, we want + # the original to fall back as a being a password (if one + # wasn't otherwise defined) e.g. + # mailtos://PASSWORD@hostname?user=ad...@mail-domain.com + # - in the above, the PASSWORD gets lost in the parse url() + # since a user= over-ride is specified. + presults = parse_url(results['url']) + if presults: + # Store our Password + results['password'] = presults['user'] + + # Store our socket read timeout if specified + if 'rto' in results['qsd']: + results['rto'] = results['qsd']['rto'] + + # Store our socket connect timeout if specified + if 'cto' in results['qsd']: + results['cto'] = results['qsd']['cto'] + + if 'port' in results['qsd']: + results['port'] = results['qsd']['port'] + + return results + + @staticmethod def parse_url(url, verify_host=True, plus_to_space=False, strict_port=False): """Parses the URL and returns it broken apart into a dictionary. @@ -698,53 +771,7 @@ # We're done; we failed to parse our url return results - # if our URL ends with an 's', then assume our secure flag is set. - results['secure'] = (results['schema'][-1] == 's') - - # Support SSL Certificate 'verify' keyword. Default to being enabled - results['verify'] = True - - if 'verify' in results['qsd']: - results['verify'] = parse_bool( - results['qsd'].get('verify', True)) - - # Password overrides - if 'password' in results['qsd']: - results['password'] = results['qsd']['password'] - if 'pass' in results['qsd']: - results['password'] = results['qsd']['pass'] - - # User overrides - if 'user' in results['qsd']: - results['user'] = results['qsd']['user'] - - # parse_url() always creates a 'password' and 'user' entry in the - # results returned. Entries are set to None if they weren't specified - if results['password'] is None and 'user' in results['qsd']: - # Handle cases where the user= provided in 2 locations, we want - # the original to fall back as a being a password (if one wasn't - # otherwise defined) - # e.g. - # mailtos://PASSWORD@hostname?user=ad...@mail-domain.com - # - the PASSWORD gets lost in the parse url() since a user= - # over-ride is specified. - presults = parse_url(results['url']) - if presults: - # Store our Password - results['password'] = presults['user'] - - # Store our socket read timeout if specified - if 'rto' in results['qsd']: - results['rto'] = results['qsd']['rto'] - - # Store our socket connect timeout if specified - if 'cto' in results['qsd']: - results['cto'] = results['qsd']['cto'] - - if 'port' in results['qsd']: - results['port'] = results['qsd']['port'] - - return results + return URLBase.post_process_parse_url_results(results) @staticmethod def http_response_code_lookup(code, response_mask=None): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/__init__.py new/apprise-1.7.5/apprise/__init__.py --- old/apprise-1.7.4/apprise/__init__.py 2024-03-09 22:40:14.000000000 +0100 +++ new/apprise-1.7.5/apprise/__init__.py 2024-03-30 14:58:10.000000000 +0100 @@ -27,7 +27,7 @@ # POSSIBILITY OF SUCH DAMAGE. __title__ = 'Apprise' -__version__ = '1.7.4' +__version__ = '1.7.5' __author__ = 'Chris Caron' __license__ = 'BSD' __copywrite__ = 'Copyright (C) 2024 Chris Caron <lead2g...@gmail.com>' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/attachment/AttachBase.py new/apprise-1.7.5/apprise/attachment/AttachBase.py --- old/apprise-1.7.4/apprise/attachment/AttachBase.py 2024-01-27 21:32:17.000000000 +0100 +++ new/apprise-1.7.5/apprise/attachment/AttachBase.py 2024-03-29 20:00:10.000000000 +0100 @@ -253,7 +253,7 @@ return self.detected_mimetype \ if self.detected_mimetype else self.unknown_mimetype - def exists(self): + def exists(self, retrieve_if_missing=True): """ Simply returns true if the object has downloaded and stored the attachment AND the attachment has not expired. @@ -282,7 +282,7 @@ # The file is not present pass - return self.download() + return False if not retrieve_if_missing else self.download() def invalidate(self): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/attachment/AttachHTTP.py new/apprise-1.7.5/apprise/attachment/AttachHTTP.py --- old/apprise-1.7.4/apprise/attachment/AttachHTTP.py 2024-01-27 21:32:17.000000000 +0100 +++ new/apprise-1.7.5/apprise/attachment/AttachHTTP.py 2024-03-29 20:00:10.000000000 +0100 @@ -29,6 +29,7 @@ import re import os import requests +import threading from tempfile import NamedTemporaryFile from .AttachBase import AttachBase from ..common import ContentLocation @@ -56,6 +57,9 @@ # Web based requests are remote/external to our current location location = ContentLocation.HOSTED + # thread safe loading + _lock = threading.Lock() + def __init__(self, headers=None, **kwargs): """ Initialize HTTP Object @@ -96,9 +100,6 @@ # our content is inaccessible return False - # Ensure any existing content set has been invalidated - self.invalidate() - # prepare header headers = { 'User-Agent': self.app_id, @@ -117,134 +118,154 @@ url += self.fullpath - self.logger.debug('HTTP POST URL: %s (cert_verify=%r)' % ( - url, self.verify_certificate, - )) - # Where our request object will temporarily live. r = None # Always call throttle before any remote server i/o is made self.throttle() - try: - # Make our request - with requests.get( - url, - headers=headers, - auth=auth, - params=self.qsd, - verify=self.verify_certificate, - timeout=self.request_timeout, - stream=True) as r: - - # Handle Errors - r.raise_for_status() - - # Get our file-size (if known) - try: - file_size = int(r.headers.get('Content-Length', '0')) - except (TypeError, ValueError): - # Handle edge case where Content-Length is a bad value - file_size = 0 - - # Perform a little Q/A on file limitations and restrictions - if self.max_file_size > 0 and file_size > self.max_file_size: - - # The content retrieved is to large - self.logger.error( - 'HTTP response exceeds allowable maximum file length ' - '({}KB): {}'.format( - int(self.max_file_size / 1024), - self.url(privacy=True))) - - # Return False (signifying a failure) - return False - - # Detect config format based on mime if the format isn't - # already enforced - self.detected_mimetype = r.headers.get('Content-Type') - - d = r.headers.get('Content-Disposition', '') - result = re.search( - "filename=['\"]?(?P<name>[^'\"]+)['\"]?", d, re.I) - if result: - self.detected_name = result.group('name').strip() - - # Create a temporary file to work with - self._temp_file = NamedTemporaryFile() - - # Get our chunk size - chunk_size = self.chunk_size - - # Track all bytes written to disk - bytes_written = 0 - - # If we get here, we can now safely write our content to disk - for chunk in r.iter_content(chunk_size=chunk_size): - # filter out keep-alive chunks - if chunk: - self._temp_file.write(chunk) - bytes_written = self._temp_file.tell() - - # Prevent a case where Content-Length isn't provided - # we don't want to fetch beyond our limits - if self.max_file_size > 0: - if bytes_written > self.max_file_size: - # The content retrieved is to large - self.logger.error( - 'HTTP response exceeds allowable maximum ' - 'file length ({}KB): {}'.format( - int(self.max_file_size / 1024), - self.url(privacy=True))) - - # Invalidate any variables previously set - self.invalidate() - - # Return False (signifying a failure) - return False - - elif bytes_written + chunk_size \ - > self.max_file_size: - # Adjust out next read to accomodate up to our - # limit +1. This will prevent us from readig - # to much into our memory buffer - self.max_file_size - bytes_written + 1 - - # Ensure our content is flushed to disk for post-processing - self._temp_file.flush() - - # Set our minimum requirements for a successful download() call - self.download_path = self._temp_file.name - if not self.detected_name: - self.detected_name = os.path.basename(self.fullpath) - - except requests.RequestException as e: - self.logger.error( - 'A Connection error occurred retrieving HTTP ' - 'configuration from %s.' % self.host) - self.logger.debug('Socket Exception: %s' % str(e)) + with self._lock: + if self.exists(retrieve_if_missing=False): + # Due to locking; it's possible a concurrent thread already + # handled the retrieval in which case we can safely move on + self.logger.trace( + 'HTTP Attachment %s already retrieved', + self._temp_file.name) + return True - # Invalidate any variables previously set + # Ensure any existing content set has been invalidated self.invalidate() - # Return False (signifying a failure) - return False + self.logger.debug( + 'HTTP Attachment Fetch URL: %s (cert_verify=%r)' % ( + url, self.verify_certificate)) + + try: + # Make our request + with requests.get( + url, + headers=headers, + auth=auth, + params=self.qsd, + verify=self.verify_certificate, + timeout=self.request_timeout, + stream=True) as r: + + # Handle Errors + r.raise_for_status() + + # Get our file-size (if known) + try: + file_size = int(r.headers.get('Content-Length', '0')) + except (TypeError, ValueError): + # Handle edge case where Content-Length is a bad value + file_size = 0 + + # Perform a little Q/A on file limitations and restrictions + if self.max_file_size > 0 and \ + file_size > self.max_file_size: + + # The content retrieved is to large + self.logger.error( + 'HTTP response exceeds allowable maximum file ' + 'length ({}KB): {}'.format( + int(self.max_file_size / 1024), + self.url(privacy=True))) + + # Return False (signifying a failure) + return False + + # Detect config format based on mime if the format isn't + # already enforced + self.detected_mimetype = r.headers.get('Content-Type') + + d = r.headers.get('Content-Disposition', '') + result = re.search( + "filename=['\"]?(?P<name>[^'\"]+)['\"]?", d, re.I) + if result: + self.detected_name = result.group('name').strip() + + # Create a temporary file to work with; delete must be set + # to False or it isn't compatible with Microsoft Windows + # instances. In lieu of this, __del__ will clean up the + # file for us. + self._temp_file = NamedTemporaryFile(delete=False) + + # Get our chunk size + chunk_size = self.chunk_size + + # Track all bytes written to disk + bytes_written = 0 + + # If we get here, we can now safely write our content to + # disk + for chunk in r.iter_content(chunk_size=chunk_size): + # filter out keep-alive chunks + if chunk: + self._temp_file.write(chunk) + bytes_written = self._temp_file.tell() + + # Prevent a case where Content-Length isn't + # provided. In this case we don't want to fetch + # beyond our limits + if self.max_file_size > 0: + if bytes_written > self.max_file_size: + # The content retrieved is to large + self.logger.error( + 'HTTP response exceeds allowable ' + 'maximum file length ' + '({}KB): {}'.format( + int(self.max_file_size / 1024), + self.url(privacy=True))) + + # Invalidate any variables previously set + self.invalidate() + + # Return False (signifying a failure) + return False + + elif bytes_written + chunk_size \ + > self.max_file_size: + # Adjust out next read to accomodate up to + # our limit +1. This will prevent us from + # reading to much into our memory buffer + self.max_file_size - bytes_written + 1 + + # Ensure our content is flushed to disk for post-processing + self._temp_file.flush() + + # Set our minimum requirements for a successful download() + # call + self.download_path = self._temp_file.name + if not self.detected_name: + self.detected_name = os.path.basename(self.fullpath) + + except requests.RequestException as e: + self.logger.error( + 'A Connection error occurred retrieving HTTP ' + 'configuration from %s.' % self.host) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Invalidate any variables previously set + self.invalidate() + + # Return False (signifying a failure) + return False + + except (IOError, OSError): + # IOError is present for backwards compatibility with Python + # versions older then 3.3. >= 3.3 throw OSError now. + + # Could not open and/or write the temporary file + self.logger.error( + 'Could not write attachment to disk: {}'.format( + self.url(privacy=True))) - except (IOError, OSError): - # IOError is present for backwards compatibility with Python - # versions older then 3.3. >= 3.3 throw OSError now. - - # Could not open and/or write the temporary file - self.logger.error( - 'Could not write attachment to disk: {}'.format( - self.url(privacy=True))) + # Invalidate any variables previously set + self.invalidate() - # Invalidate any variables previously set - self.invalidate() - - # Return False (signifying a failure) - return False + # Return False (signifying a failure) + return False # Return our success return True @@ -254,11 +275,30 @@ Close our temporary file """ if self._temp_file: + self.logger.trace( + 'Attachment cleanup of %s', self._temp_file.name) self._temp_file.close() + + try: + # Ensure our file is removed (if it exists) + os.unlink(self._temp_file.name) + + except OSError: + pass + + # Reset our temporary file to prevent from entering + # this block again self._temp_file = None super().invalidate() + def __del__(self): + """ + Tidy memory if open + """ + with self._lock: + self.invalidate() + def url(self, privacy=False, *args, **kwargs): """ Returns the URL built dynamically based on specified arguments. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/config/ConfigBase.py new/apprise-1.7.5/apprise/config/ConfigBase.py --- old/apprise-1.7.4/apprise/config/ConfigBase.py 2024-01-27 21:32:17.000000000 +0100 +++ new/apprise-1.7.5/apprise/config/ConfigBase.py 2024-03-29 21:20:13.000000000 +0100 @@ -1184,6 +1184,9 @@ # Prepare our Asset Object _results['asset'] = asset + # Handle post processing of result set + _results = URLBase.post_process_parse_url_results(_results) + # Store our preloaded entries preloaded.append({ 'results': _results, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/conversion.py new/apprise-1.7.5/apprise/conversion.py --- old/apprise-1.7.4/apprise/conversion.py 2024-01-27 21:32:17.000000000 +0100 +++ new/apprise-1.7.5/apprise/conversion.py 2024-03-30 14:53:24.000000000 +0100 @@ -58,8 +58,7 @@ """ Converts specified content from markdown to HTML. """ - - return markdown(content) + return markdown(content, extensions=['nl2br', 'tables']) def text_to_html(content): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/decorators/CustomNotifyPlugin.py new/apprise-1.7.5/apprise/decorators/CustomNotifyPlugin.py --- old/apprise-1.7.4/apprise/decorators/CustomNotifyPlugin.py 2024-01-27 21:12:32.000000000 +0100 +++ new/apprise-1.7.5/apprise/decorators/CustomNotifyPlugin.py 2024-03-29 21:20:13.000000000 +0100 @@ -147,6 +147,10 @@ self._default_args = {} + # Some variables do not need to be set + if 'secure' in kwargs: + del kwargs['secure'] + # Apply our updates based on what was parsed dict_full_update(self._default_args, self._base_args) dict_full_update(self._default_args, kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/plugins/NotifyFreeMobile.py new/apprise-1.7.5/apprise/plugins/NotifyFreeMobile.py --- old/apprise-1.7.4/apprise/plugins/NotifyFreeMobile.py 1970-01-01 01:00:00.000000000 +0100 +++ new/apprise-1.7.5/apprise/plugins/NotifyFreeMobile.py 2024-03-30 14:53:24.000000000 +0100 @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron <lead2g...@gmail.com> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Free Mobile +# 1. Visit https://mobile.free.fr/ + +# the URL will look something like this: +# https://smsapi.free-mobile.fr/sendmsg?msg=content +# + +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..AppriseLocale import gettext_lazy as _ + + +class NotifyFreeMobile(NotifyBase): + """ + A wrapper for Free-Mobile Notifications + """ + + # The default descriptive name associated with the Notification + service_name = _('Free-Mobile') + + # The services URL + service_url = 'https://mobile.free.fr/' + + # The default secure protocol + secure_protocol = 'freemobile' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_freemobile' + + # Plain Text Notification URL + notify_url = 'https://smsapi.free-mobile.fr/sendmsg' + + # Define object templates + templates = ( + '{schema}://{user}@{password}', + ) + + # The title is not used + title_maxlen = 0 + + # SMS Messages are restricted in size + body_maxlen = 160 + + # Define our tokens; these are the minimum tokens required required to + # be passed into this function (as arguments). The syntax appends any + # previously defined in the base package and builds onto them + template_tokens = dict(NotifyBase.template_tokens, **{ + 'user': { + 'name': _('Username'), + 'type': 'string', + }, + 'password': { + 'name': _('Password'), + 'type': 'string', + 'private': True, + }, + }) + + def __init__(self, **kwargs): + """ + Initialize Free Mobile Object + """ + super().__init__(**kwargs) + + if not (self.user and self.password): + msg = 'A FreeMobile user and password ' \ + 'combination was not provided.' + self.logger.warning(msg) + raise TypeError(msg) + + return + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Prepare our parameters + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + return '{schema}://{user}@{password}/?{params}'.format( + schema=self.secure_protocol, + user=self.user, + password=self.pprint(self.password, privacy, safe=''), + params=NotifyFreeMobile.urlencode(params), + ) + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Send our notification + """ + + # prepare our headers + headers = { + 'User-Agent': self.app_id, + } + + # Prepare our payload + payload = { + 'user': self.user, + 'pass': self.password, + } + + # Our Message + params = { + 'msg': body + } + + self.logger.debug('Free Mobile GET URL: %s (cert_verify=%r)' % ( + self.notify_url, self.verify_certificate)) + self.logger.debug('Free Mobile Payload: %s' % str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + self.notify_url, + data=dumps(payload).encode('utf-8'), + params=params, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyFreeMobile.http_response_code_lookup(r.status_code) + + self.logger.warning( + 'Failed to send Free Mobile notification: ' + '{}{}error={}.'.format( + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug('Response Details:\r\n{}'.format(r.content)) + + # Return; we're done + return False + + else: + self.logger.info('Sent Free Mobile notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending Free Mobile ' + 'notification.') + self.logger.debug('Socket Exception: %s' % str(e)) + + # Return; we're done + return False + + return True + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + + # parse_url already handles getting the `user` and `password` fields + # populated. + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + # The hostname can act as the password if specified and a password + # was otherwise not (specified): + if not results.get('password'): + results['password'] = NotifyFreeMobile.unquote(results['host']) + + return results diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise/plugins/NotifyMQTT.py new/apprise-1.7.5/apprise/plugins/NotifyMQTT.py --- old/apprise-1.7.4/apprise/plugins/NotifyMQTT.py 2024-01-27 21:31:31.000000000 +0100 +++ new/apprise-1.7.5/apprise/plugins/NotifyMQTT.py 2024-03-30 14:52:48.000000000 +0100 @@ -89,7 +89,7 @@ requirements = { # Define our required packaging in order to work - 'packages_required': 'paho-mqtt' + 'packages_required': 'paho-mqtt <= 2.0.0' } # The default descriptive name associated with the Notification diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise.egg-info/PKG-INFO new/apprise-1.7.5/apprise.egg-info/PKG-INFO --- old/apprise-1.7.4/apprise.egg-info/PKG-INFO 2024-03-09 22:41:33.000000000 +0100 +++ new/apprise-1.7.5/apprise.egg-info/PKG-INFO 2024-03-30 14:59:27.000000000 +0100 @@ -1,12 +1,12 @@ Metadata-Version: 2.1 Name: apprise -Version: 1.7.4 +Version: 1.7.5 Summary: Push Notifications that work with just about every platform! Home-page: https://github.com/caronc/apprise Author: Chris Caron Author-email: lead2g...@gmail.com License: BSD -Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line LunaSea MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Revolt Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip +Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Free Mobile Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line LunaSea MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Revolt Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators @@ -177,6 +177,7 @@ | [DAPNET](https://github.com/caronc/apprise/wiki/Notify_dapnet) | dapnet:// | (TCP) 80 | dapnet://user:pass@callsign<br/>dapnet://user:pass@callsign1/callsign2/callsignN | [D7 Networks](https://github.com/caronc/apprise/wiki/Notify_d7networks) | d7sms:// | (TCP) 443 | d7sms://token@PhoneNo<br/>d7sms://token@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [DingTalk](https://github.com/caronc/apprise/wiki/Notify_dingtalk) | dingtalk:// | (TCP) 443 | dingtalk://token/<br />dingtalk://token/ToPhoneNo<br />dingtalk://token/ToPhoneNo1/ToPhoneNo2/ToPhoneNo1/ +| [Free-Mobile](https://github.com/caronc/apprise/wiki/Notify_freemobile) | freemobile:// | (TCP) 443 | freemobile://user@password/ [httpSMS](https://github.com/caronc/apprise/wiki/Notify_httpsms) | httpsms:// | (TCP) 443 | httpsms://ApiKey@FromPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Kavenegar](https://github.com/caronc/apprise/wiki/Notify_kavenegar) | kavenegar:// | (TCP) 443 | kavenegar://ApiKey/ToPhoneNo<br/>kavenegar://FromPhoneNo@ApiKey/ToPhoneNo<br/>kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [MessageBird](https://github.com/caronc/apprise/wiki/Notify_messagebird) | msgbird:// | (TCP) 443 | msgbird://ApiKey/FromPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/apprise.egg-info/SOURCES.txt new/apprise-1.7.5/apprise.egg-info/SOURCES.txt --- old/apprise-1.7.4/apprise.egg-info/SOURCES.txt 2024-03-09 22:41:33.000000000 +0100 +++ new/apprise-1.7.5/apprise.egg-info/SOURCES.txt 2024-03-30 14:59:27.000000000 +0100 @@ -97,6 +97,7 @@ apprise/plugins/NotifyFaast.py apprise/plugins/NotifyFlock.py apprise/plugins/NotifyForm.py +apprise/plugins/NotifyFreeMobile.py apprise/plugins/NotifyGnome.py apprise/plugins/NotifyGoogleChat.py apprise/plugins/NotifyGotify.py @@ -237,6 +238,7 @@ test/test_plugin_faast.py test/test_plugin_fcm.py test/test_plugin_flock.py +test/test_plugin_freemobile.py test/test_plugin_glib.py test/test_plugin_gnome.py test/test_plugin_google_chat.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/packaging/man/apprise.1.html new/apprise-1.7.5/packaging/man/apprise.1.html --- old/apprise-1.7.4/packaging/man/apprise.1.html 2024-03-09 22:41:09.000000000 +0100 +++ new/apprise-1.7.5/packaging/man/apprise.1.html 2024-03-30 14:59:07.000000000 +0100 @@ -316,7 +316,7 @@ <h2 id="COPYRIGHT">COPYRIGHT</h2> -<p>Apprise is Copyright (C) 2024 Chris Caron <a href="mailto:lead2gold@gmail.com" data-bare-link="true">lead2gold@gmail.com</a></p> +<p>Apprise is Copyright (C) 2024 Chris Caron <a href="mailto:lead2gold@gmail.com" data-bare-link="true">lead2gold@gmail.com</a></p> <ol class='man-decor man-foot man foot'> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/packaging/redhat/python-apprise.spec new/apprise-1.7.5/packaging/redhat/python-apprise.spec --- old/apprise-1.7.4/packaging/redhat/python-apprise.spec 2024-03-09 22:40:47.000000000 +0100 +++ new/apprise-1.7.5/packaging/redhat/python-apprise.spec 2024-03-30 14:58:49.000000000 +0100 @@ -41,20 +41,20 @@ Apprise API, APRS, AWS SES, AWS SNS, Bark, Boxcar, Burst SMS, BulkSMS, BulkVS, ClickSend, DAPNET, DingTalk, Discord, E-Mail, Emby, Faast, FCM, Flock, -Google Chat, Gotify, Growl, Guilded, Home Assistant, httpSMS, IFTTT, Join, -Kavenegar, KODI, Kumulos, LaMetric, Line, LunaSea, MacOSX, Mailgun, Mastodon, -Mattermost,Matrix, MessageBird, Microsoft Windows, Microsoft Teams, Misskey, -MQTT, MSG91, MyAndroid, Nexmo, Nextcloud, NextcloudTalk, Notica, Notifiarr, -Notifico, ntfy, Office365, OneSignal, Opsgenie, PagerDuty, PagerTree, -ParsePlatform, PopcornNotify, Prowl, Pushalot, PushBullet, Pushjet, PushMe, -Pushover, PushSafer, Pushy, PushDeer, Revolt, Reddit, Rocket.Chat, RSyslog, -SendGrid, ServerChan, Signal, SimplePush, Sinch, Slack, SMSEagle, SMS Manager, -SMTP2Go, SparkPost, Super Toasty, Streamlabs, Stride, Synology Chat, Syslog, -Techulus Push, Telegram, Threema Gateway, Twilio, Twitter, Twist, XBMC, -Voipms, Vonage, WeCom Bot, WhatsApp, Webex Teams} +Free Mobile, Google Chat, Gotify, Growl, Guilded, Home Assistant, httpSMS, +IFTTT, Join, Kavenegar, KODI, Kumulos, LaMetric, Line, LunaSea, MacOSX, +Mailgun, Mastodon, Mattermost,Matrix, MessageBird, Microsoft Windows, +Microsoft Teams, Misskey, MQTT, MSG91, MyAndroid, Nexmo, Nextcloud, +NextcloudTalk, Notica, Notifiarr, Notifico, ntfy, Office365, OneSignal, +Opsgenie, PagerDuty, PagerTree, ParsePlatform, PopcornNotify, Prowl, Pushalot, +PushBullet, Pushjet, PushMe, Pushover, PushSafer, Pushy, PushDeer, Revolt, +Reddit, Rocket.Chat, RSyslog, SendGrid, ServerChan, Signal, SimplePush, Sinch, +Slack, SMSEagle, SMS Manager, SMTP2Go, SparkPost, Super Toasty, Streamlabs, +Stride, Synology Chat, Syslog, Techulus Push, Telegram, Threema Gateway, Twilio, +Twitter, Twist, XBMC, Voipms, Vonage, WeCom Bot, WhatsApp, Webex Teams} Name: python-%{pypi_name} -Version: 1.7.4 +Version: 1.7.5 Release: 1%{?dist} Summary: A simple wrapper to many popular notification services used today License: BSD @@ -195,6 +195,9 @@ %{python3_sitelib}/%{pypi_name}/cli.* %changelog +* Sat Mar 30 2024 Chris Caron <lead2g...@gmail.com> - 1.7.5 +- Updated to v1.7.5 + * Sat Mar 9 2024 Chris Caron <lead2g...@gmail.com> - 1.7.4 - Updated to v1.7.4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/setup.py new/apprise-1.7.5/setup.py --- old/apprise-1.7.4/setup.py 2024-03-09 22:40:06.000000000 +0100 +++ new/apprise-1.7.5/setup.py 2024-03-30 14:57:52.000000000 +0100 @@ -62,7 +62,7 @@ setup( name='apprise', - version='1.7.4', + version='1.7.5', description='Push Notifications that work with just about every platform!', license='BSD', long_description=open('README.md', encoding="utf-8").read(), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/test/test_attach_http.py new/apprise-1.7.5/test/test_attach_http.py --- old/apprise-1.7.4/test/test_attach_http.py 2024-01-27 21:33:15.000000000 +0100 +++ new/apprise-1.7.5/test/test_attach_http.py 2024-03-29 20:00:10.000000000 +0100 @@ -35,7 +35,7 @@ from os.path import dirname from os.path import getsize from apprise.attachment.AttachHTTP import AttachHTTP -from apprise import AppriseAttachment +from apprise import Apprise, AppriseAttachment from apprise.NotificationManager import NotificationManager from apprise.plugins.NotifyBase import NotifyBase from apprise.common import ContentLocation @@ -113,8 +113,9 @@ assert re.search(r'[?&]_var=test', obj.url()) +@mock.patch('requests.post') @mock.patch('requests.get') -def test_attach_http(mock_get): +def test_attach_http(mock_get, mock_post): """ API: AttachHTTP() object @@ -422,3 +423,50 @@ # Restore value AttachHTTP.max_file_size = max_file_size + + # Multi Message Testing + mock_get.side_effect = None + mock_get.return_value = DummyResponse() + + # Prepare our POST response (from notify call) + response = requests.Request() + response.status_code = requests.codes.ok + response.content = "" + mock_post.return_value = response + + mock_get.reset_mock() + mock_post.reset_mock() + assert mock_get.call_count == 0 + + apobj = Apprise() + assert apobj.add('form://localhost') + assert apobj.add('json://localhost') + assert apobj.add('xml://localhost') + assert len(apobj) == 3 + assert apobj.notify( + body='one attachment split 3 times', + attach="http://localhost/test.gif", + ) is True + + # We posted 3 times + assert mock_post.call_count == 3 + # We only fetched once and re-used the same fetch for all posts + assert mock_get.call_count == 1 + + mock_get.reset_mock() + mock_post.reset_mock() + apobj = Apprise() + for n in range(10): + assert apobj.add(f'json://localhost?:entry={n}&method=post') + assert apobj.add(f'form://localhost?:entry={n}&method=post') + assert apobj.add(f'xml://localhost?:entry={n}&method=post') + + assert apobj.notify( + body='one attachment split 30 times', + attach="http://localhost/test.gif", + ) is True + + # We posted 30 times + 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/test/test_config_base.py new/apprise-1.7.5/test/test_config_base.py --- old/apprise-1.7.4/test/test_config_base.py 2024-01-27 21:33:15.000000000 +0100 +++ new/apprise-1.7.5/test/test_config_base.py 2024-03-30 14:53:24.000000000 +0100 @@ -221,6 +221,32 @@ assert len(result) == 1 +def test_config_base_discord_bug_report_01(): + """ + API: ConfigBase.config_parse user feedback + + A Discord report that a tag was not correctly assigned to a URL when + presented in the following format + urls: + - json://myhost: + - tag: test + userid: test + """ + result, config = ConfigBase.config_parse(""" + urls: + - json://myhost: + - tag: test + userid: test + """, asset=AppriseAsset()) + + # We expect to parse 4 entries from the above + assert isinstance(result, list) + assert isinstance(config, list) + assert len(result) == 1 + assert len(result[0].tags) == 1 + assert 'test' in result[0].tags + + def test_config_base_config_parse_text(): """ API: ConfigBase.config_parse_text object diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/test/test_conversion.py new/apprise-1.7.5/test/test_conversion.py --- old/apprise-1.7.4/test/test_conversion.py 2024-01-27 21:33:15.000000000 +0100 +++ new/apprise-1.7.5/test/test_conversion.py 2024-03-30 14:53:24.000000000 +0100 @@ -29,6 +29,7 @@ from apprise import NotifyFormat from apprise.conversion import convert_between import pytest +from inspect import cleandoc # Disable logging for a cleaner testing output import logging @@ -150,3 +151,60 @@ assert response == \ '<title>Test Message</title><body>Body<'\ '/body>' + + +def test_conversion_markdown_to_html(): + """conversion: Test markdown to html + """ + + # While this uses the underlining markdown library + # what we're testing for are the edge cases we know it doesn't support + # hence, `-` (a dash) with the markdown library must be a `*` to work + # correctly + response = convert_between( + NotifyFormat.MARKDOWN, NotifyFormat.HTML, cleandoc(""" + ## Some Heading + + With Data: + + - Foo + - Bar + """)) + + assert '<li>Foo</li>' in response + assert '<li>Bar</li>' in response + assert '<h2>Some Heading</h2>' in response + assert '<br />' not in response + + # if the - follows With Data on the very next line, it's consider to not + # requiring indentation + response = convert_between( + NotifyFormat.MARKDOWN, NotifyFormat.HTML, cleandoc(""" + ## Some Heading + + With Data: + - Foo + - Bar + """)) + + # Breaks are added: + assert '<br />' in response + assert '- Foo' in response + assert '- Bar' in response + + # Table formatting + response = convert_between( + NotifyFormat.MARKDOWN, NotifyFormat.HTML, cleandoc(""" + First Header | Second Header + -------------- | ------------- + Content Cell1 | Content Cell3 + Content Cell2 | Content Cell4 + """)) + + assert '<table>' in response + assert '<th>First Header</th>' in response + assert '<th>Second Header</th>' in response + assert '<td>Content Cell1</td>' in response + assert '<td>Content Cell2</td>' in response + assert '<td>Content Cell3</td>' in response + assert '<td>Content Cell4</td>' in response diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/test/test_decorator_notify.py new/apprise-1.7.5/test/test_decorator_notify.py --- old/apprise-1.7.4/test/test_decorator_notify.py 2024-01-27 21:33:15.000000000 +0100 +++ new/apprise-1.7.5/test/test_decorator_notify.py 2024-03-29 21:20:13.000000000 +0100 @@ -29,6 +29,7 @@ from os.path import dirname from os.path import join from apprise.decorators import notify +from apprise.decorators.CustomNotifyPlugin import CustomNotifyPlugin from apprise import Apprise from apprise import AppriseConfig from apprise import AppriseAsset @@ -351,7 +352,7 @@ t = tmpdir.mkdir("multi-test").join("apprise.yml") t.write("""urls: - multi://user1:pass@hostname - - multi://user2:pass2@hostname + - multi://user2:pass2@hostname?verify=no """) # Create ourselves a config object @@ -404,11 +405,12 @@ assert 'tag' in meta assert isinstance(meta['tag'], set) - assert len(meta) == 7 + assert len(meta) == 8 # We carry all of our default arguments from the @notify's initialization assert meta['schema'] == 'multi' assert meta['host'] == 'hostname' assert meta['user'] == 'user1' + assert meta['verify'] is True assert meta['password'] == 'pass' # Verify our URL is correct @@ -441,15 +443,24 @@ assert 'tag' in meta assert isinstance(meta['tag'], set) - assert len(meta) == 7 + assert len(meta) == 9 # We carry all of our default arguments from the @notify's initialization assert meta['schema'] == 'multi' assert meta['host'] == 'hostname' assert meta['user'] == 'user2' assert meta['password'] == 'pass2' + assert meta['verify'] is False + assert meta['qsd']['verify'] == 'no' # Verify our URL is correct - assert meta['url'] == 'multi://user2:pass2@hostname' + assert meta['url'] == 'multi://user2:pass2@hostname?verify=no' # Tidy N_MGR.remove('multi') + + +def test_custom_notify_plugin_decoration(): + """decorators: CustomNotifyPlugin testing + """ + + CustomNotifyPlugin() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/test/test_plugin_email.py new/apprise-1.7.5/test/test_plugin_email.py --- old/apprise-1.7.4/test/test_plugin_email.py 2024-03-03 15:39:10.000000000 +0100 +++ new/apprise-1.7.5/test/test_plugin_email.py 2024-03-29 21:20:13.000000000 +0100 @@ -30,6 +30,7 @@ import os import re from unittest import mock +from inspect import cleandoc import smtplib from email.header import decode_header @@ -37,6 +38,8 @@ from apprise import NotifyType, NotifyBase from apprise import Apprise from apprise import AttachBase +from apprise.AppriseAsset import AppriseAsset +from apprise.config.ConfigBase import ConfigBase from apprise import AppriseAttachment from apprise.plugins.NotifyEmail import NotifyEmail from apprise.plugins import NotifyEmail as NotifyEmailModule @@ -1757,3 +1760,58 @@ assert len(obj.targets) == 1 assert (False, 'm...@mydomain.com') in obj.targets + + +def test_plugin_email_variables_1087(): + """ + NotifyEmail() GitHub Issue 1087 + https://github.com/caronc/apprise/issues/1087 + Email variables reported not working correctly + + """ + + # Valid Configuration + result, _ = ConfigBase.config_parse(cleandoc(""" + # + # Test Email Parsing + # + urls: + - mailtos://alt.lan/: + - user: testu...@alt.lan + pass: xxxxXXXxxx + smtp: smtp.alt.lan + to: alter...@alt.lan + """), asset=AppriseAsset()) + + assert isinstance(result, list) + assert len(result) == 1 + + email = result[0] + assert email.from_addr == ['Apprise', 'testu...@alt.lan'] + assert email.user == 'testu...@alt.lan' + assert email.smtp_host == 'smtp.alt.lan' + assert email.targets == [(False, 'alter...@alt.lan')] + assert email.password == 'xxxxXXXxxx' + + # Valid Configuration + result, _ = ConfigBase.config_parse(cleandoc(""" + # + # Test Email Parsing where qsd over-rides all + # + urls: + - mailtos://alt.lan/?pass=abcd&user=j...@alt.lan: + - user: testu...@alt.lan + pass: xxxxXXXxxx + smtp: smtp.alt.lan + to: alter...@alt.lan + """), asset=AppriseAsset()) + + assert isinstance(result, list) + assert len(result) == 1 + + email = result[0] + assert email.from_addr == ['Apprise', 'j...@alt.lan'] + assert email.user == 'j...@alt.lan' + assert email.smtp_host == 'smtp.alt.lan' + assert email.targets == [(False, 'alter...@alt.lan')] + assert email.password == 'abcd' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.4/test/test_plugin_freemobile.py new/apprise-1.7.5/test/test_plugin_freemobile.py --- old/apprise-1.7.4/test/test_plugin_freemobile.py 1970-01-01 01:00:00.000000000 +0100 +++ new/apprise-1.7.5/test/test_plugin_freemobile.py 2024-03-30 14:53:24.000000000 +0100 @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron <lead2g...@gmail.com> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import requests + +from apprise.plugins.NotifyFreeMobile import NotifyFreeMobile +from helpers import AppriseURLTester + +# Disable logging for a cleaner testing output +import logging +logging.disable(logging.CRITICAL) + +# Our Testing URLs +apprise_url_tests = ( + ('freemobile://', { + 'instance': TypeError, + }), + ('freemobile://:@/', { + 'instance': TypeError, + }), + ('freemobile://user@password', { + # Minimum requirements met + 'instance': NotifyFreeMobile, + }), + ('freemobile://?user=user&pass=password', { + # Test ?user= and pass= + 'instance': NotifyFreeMobile, + }), + ('freemobile://user@password', { + 'instance': NotifyFreeMobile, + # force a failure + 'response': False, + 'requests_response_code': requests.codes.internal_server_error, + }), + ('freemobile://user@password', { + 'instance': NotifyFreeMobile, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + ('freemobile://user@password', { + 'instance': NotifyFreeMobile, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), +) + + +def test_plugin_freemobile_urls(): + """ + NotifyFreeMobile() Apprise URLs + + """ + + # Run our general tests + AppriseURLTester(tests=apprise_url_tests).run_all()