From c0adcf5933619c9e91e2a867b8f3a8c7c341a8f2 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 12 Apr 2023 13:38:43 +0100 Subject: [PATCH 01/36] =?UTF-8?q?Update=20SimpleJSON=203.18.1=20(c891b95)?= =?UTF-8?q?=20=E2=86=92=203.19.1=20(aeb63ee).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 7 ++- lib/simplejson/__init__.py | 106 +++++++++++++++---------------------- lib/simplejson/_speedups.c | 30 ++++++----- lib/simplejson/decoder.py | 82 ++++++++++++++++------------ lib/simplejson/encoder.py | 30 +++++------ lib/simplejson/scanner.py | 6 +-- 6 files changed, 132 insertions(+), 129 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d9ba3777..bd5f9be3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,9 @@ -### 3.28.0 (2023-04-12 13:05:00 UTC) +### 3.29.0 (2023-xx-xx xx:xx:00 UTC) + +* Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) + + +### 3.28.0 (2023-04-12 13:05:00 UTC) * Update html5lib 1.1 (f87487a) to 1.2-dev (3e500bb) * Update package resource API 63.2.0 (3ae44cd) to 67.5.1 (f51eccd) diff --git a/lib/simplejson/__init__.py b/lib/simplejson/__init__.py index b3e8a81a..7e533a24 100644 --- a/lib/simplejson/__init__.py +++ b/lib/simplejson/__init__.py @@ -118,7 +118,7 @@ Serializing multiple objects to JSON lines (newline-delimited JSON):: """ from __future__ import absolute_import -__version__ = '3.18.1' +__version__ = '3.19.1' __all__ = [ 'dump', 'dumps', 'load', 'loads', 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', @@ -149,28 +149,10 @@ def _import_c_make_encoder(): except ImportError: return None -_default_encoder = JSONEncoder( - skipkeys=False, - ensure_ascii=True, - check_circular=True, - allow_nan=True, - indent=None, - separators=None, - encoding='utf-8', - default=None, - use_decimal=True, - namedtuple_as_object=True, - tuple_as_array=True, - iterable_as_array=False, - bigint_as_string=False, - item_sort_key=None, - for_json=False, - ignore_nan=False, - int_as_string_bitcount=None, -) +_default_encoder = JSONEncoder() def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, + allow_nan=False, cls=None, indent=None, separators=None, encoding='utf-8', default=None, use_decimal=True, namedtuple_as_object=True, tuple_as_array=True, bigint_as_string=False, sort_keys=False, item_sort_key=None, @@ -187,10 +169,10 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, contain non-ASCII characters, so long as they do not need to be escaped by JSON. When it is true, all non-ASCII characters are escaped. - If *allow_nan* is false, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) - in strict compliance of the original JSON specification, instead of using - the JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). See + If *allow_nan* is true (default: ``False``), then out of range ``float`` + values (``nan``, ``inf``, ``-inf``) will be serialized to + their JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``) + instead of raising a ValueError. See *ignore_nan* for ECMA-262 compliant behavior. If *indent* is a string, then JSON array elements and object members @@ -258,7 +240,7 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, """ # cached encoder if (not skipkeys and ensure_ascii and - check_circular and allow_nan and + check_circular and not allow_nan and cls is None and indent is None and separators is None and encoding == 'utf-8' and default is None and use_decimal and namedtuple_as_object and tuple_as_array and not iterable_as_array @@ -292,7 +274,7 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, + allow_nan=False, cls=None, indent=None, separators=None, encoding='utf-8', default=None, use_decimal=True, namedtuple_as_object=True, tuple_as_array=True, bigint_as_string=False, sort_keys=False, item_sort_key=None, @@ -312,10 +294,11 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, for container types will be skipped and a circular reference will result in an ``OverflowError`` (or worse). - If ``allow_nan`` is false, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in - strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + If *allow_nan* is true (default: ``False``), then out of range ``float`` + values (``nan``, ``inf``, ``-inf``) will be serialized to + their JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``) + instead of raising a ValueError. See + *ignore_nan* for ECMA-262 compliant behavior. If ``indent`` is a string, then JSON array elements and object members will be pretty-printed with a newline followed by that string repeated @@ -383,7 +366,7 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, """ # cached encoder if (not skipkeys and ensure_ascii and - check_circular and allow_nan and + check_circular and not allow_nan and cls is None and indent is None and separators is None and encoding == 'utf-8' and default is None and use_decimal and namedtuple_as_object and tuple_as_array and not iterable_as_array @@ -412,14 +395,12 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, **kw).encode(obj) -_default_decoder = JSONDecoder(encoding=None, object_hook=None, - object_pairs_hook=None) +_default_decoder = JSONDecoder() def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, - use_decimal=False, namedtuple_as_object=True, tuple_as_array=True, - **kw): + use_decimal=False, allow_nan=False, **kw): """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing a JSON document as `str` or `bytes`) to a Python object. @@ -442,23 +423,27 @@ def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, takes priority. *parse_float*, if specified, will be called with the string of every - JSON float to be decoded. By default, this is equivalent to + JSON float to be decoded. By default, this is equivalent to ``float(num_str)``. This can be used to use another datatype or parser for JSON floats (e.g. :class:`decimal.Decimal`). *parse_int*, if specified, will be called with the string of every - JSON int to be decoded. By default, this is equivalent to + JSON int to be decoded. By default, this is equivalent to ``int(num_str)``. This can be used to use another datatype or parser for JSON integers (e.g. :class:`float`). - *parse_constant*, if specified, will be called with one of the - following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This - can be used to raise an exception if invalid JSON numbers are - encountered. + *allow_nan*, if True (default false), will allow the parser to + accept the non-standard floats ``NaN``, ``Infinity``, and ``-Infinity`` + and enable the use of the deprecated *parse_constant*. If *use_decimal* is true (default: ``False``) then it implies parse_float=decimal.Decimal for parity with ``dump``. + *parse_constant*, if specified, will be + called with one of the following strings: ``'-Infinity'``, + ``'Infinity'``, ``'NaN'``. It is not recommended to use this feature, + as it is rare to parse non-compliant JSON containing these values. + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` kwarg. NOTE: You should use *object_hook* or *object_pairs_hook* instead of subclassing whenever possible. @@ -468,12 +453,12 @@ def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, encoding=encoding, cls=cls, object_hook=object_hook, parse_float=parse_float, parse_int=parse_int, parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, - use_decimal=use_decimal, **kw) + use_decimal=use_decimal, allow_nan=allow_nan, **kw) def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, - use_decimal=False, **kw): + use_decimal=False, allow_nan=False, **kw): """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON document) to a Python object. @@ -505,14 +490,18 @@ def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, ``int(num_str)``. This can be used to use another datatype or parser for JSON integers (e.g. :class:`float`). - *parse_constant*, if specified, will be called with one of the - following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This - can be used to raise an exception if invalid JSON numbers are - encountered. + *allow_nan*, if True (default false), will allow the parser to + accept the non-standard floats ``NaN``, ``Infinity``, and ``-Infinity`` + and enable the use of the deprecated *parse_constant*. If *use_decimal* is true (default: ``False``) then it implies parse_float=decimal.Decimal for parity with ``dump``. + *parse_constant*, if specified, will be + called with one of the following strings: ``'-Infinity'``, + ``'Infinity'``, ``'NaN'``. It is not recommended to use this feature, + as it is rare to parse non-compliant JSON containing these values. + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` kwarg. NOTE: You should use *object_hook* or *object_pairs_hook* instead of subclassing whenever possible. @@ -521,7 +510,7 @@ def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, if (cls is None and encoding is None and object_hook is None and parse_int is None and parse_float is None and parse_constant is None and object_pairs_hook is None - and not use_decimal and not kw): + and not use_decimal and not allow_nan and not kw): return _default_decoder.decode(s) if cls is None: cls = JSONDecoder @@ -539,6 +528,8 @@ def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, if parse_float is not None: raise TypeError("use_decimal=True implies parse_float=Decimal") kw['parse_float'] = Decimal + if allow_nan: + kw['allow_nan'] = True return cls(encoding=encoding, **kw).decode(s) @@ -560,22 +551,9 @@ def _toggle_speedups(enabled): scan.make_scanner = scan.py_make_scanner dec.make_scanner = scan.make_scanner global _default_decoder - _default_decoder = JSONDecoder( - encoding=None, - object_hook=None, - object_pairs_hook=None, - ) + _default_decoder = JSONDecoder() global _default_encoder - _default_encoder = JSONEncoder( - skipkeys=False, - ensure_ascii=True, - check_circular=True, - allow_nan=True, - indent=None, - separators=None, - encoding='utf-8', - default=None, - ) + _default_encoder = JSONEncoder() def simple_first(kv): """Helper function to pass to item_sort_key to sort simple diff --git a/lib/simplejson/_speedups.c b/lib/simplejson/_speedups.c index ec054c72..bd56b4dc 100644 --- a/lib/simplejson/_speedups.c +++ b/lib/simplejson/_speedups.c @@ -1843,7 +1843,7 @@ bail: } static PyObject * -_parse_constant(PyScannerObject *s, PyObject *constant, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) +_parse_constant(PyScannerObject *s, PyObject *pystr, PyObject *constant, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { /* Read a JSON constant from PyString pystr. constant is the Python string that was found @@ -1855,6 +1855,10 @@ _parse_constant(PyScannerObject *s, PyObject *constant, Py_ssize_t idx, Py_ssize Returns the result of parse_constant */ PyObject *rval; + if (s->parse_constant == Py_None) { + raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx); + return NULL; + } /* rval = parse_constant(constant) */ rval = PyObject_CallOneArg(s->parse_constant, constant); @@ -1886,7 +1890,7 @@ _match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssiz /* read a sign if it's there, make sure it's not the end of the string */ if (str[idx] == '-') { if (idx >= end_idx) { - raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx); + raise_errmsg(ERR_EXPECTING_VALUE, pystr, start); return NULL; } idx++; @@ -1903,7 +1907,7 @@ _match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssiz } /* no integer digits, error */ else { - raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx); + raise_errmsg(ERR_EXPECTING_VALUE, pystr, start); return NULL; } @@ -1949,8 +1953,10 @@ _match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssiz /* rval = PyFloat_FromDouble(PyOS_ascii_atof(PyString_AS_STRING(numstr))); */ double d = PyOS_string_to_double(PyString_AS_STRING(numstr), NULL, NULL); - if (d == -1.0 && PyErr_Occurred()) + if (d == -1.0 && PyErr_Occurred()) { + Py_DECREF(numstr); return NULL; + } rval = PyFloat_FromDouble(d); } } @@ -1993,7 +1999,7 @@ _match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ /* read a sign if it's there, make sure it's not the end of the string */ if (PyUnicode_READ(kind, str, idx) == '-') { if (idx >= end_idx) { - raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx); + raise_errmsg(ERR_EXPECTING_VALUE, pystr, start); return NULL; } idx++; @@ -2013,7 +2019,7 @@ _match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ } else { /* no integer digits, error */ - raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx); + raise_errmsg(ERR_EXPECTING_VALUE, pystr, start); return NULL; } @@ -2156,7 +2162,7 @@ scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *n case 'N': /* NaN */ if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { - rval = _parse_constant(s, JSON_NaN, idx, next_idx_ptr); + rval = _parse_constant(s, pystr, JSON_NaN, idx, next_idx_ptr); } else fallthrough = 1; @@ -2164,7 +2170,7 @@ scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *n case 'I': /* Infinity */ if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { - rval = _parse_constant(s, JSON_Infinity, idx, next_idx_ptr); + rval = _parse_constant(s, pystr, JSON_Infinity, idx, next_idx_ptr); } else fallthrough = 1; @@ -2172,7 +2178,7 @@ scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *n case '-': /* -Infinity */ if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { - rval = _parse_constant(s, JSON_NegInfinity, idx, next_idx_ptr); + rval = _parse_constant(s, pystr, JSON_NegInfinity, idx, next_idx_ptr); } else fallthrough = 1; @@ -2275,7 +2281,7 @@ scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_ if ((idx + 2 < length) && PyUnicode_READ(kind, str, idx + 1) == 'a' && PyUnicode_READ(kind, str, idx + 2) == 'N') { - rval = _parse_constant(s, JSON_NaN, idx, next_idx_ptr); + rval = _parse_constant(s, pystr, JSON_NaN, idx, next_idx_ptr); } else fallthrough = 1; @@ -2290,7 +2296,7 @@ scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_ PyUnicode_READ(kind, str, idx + 5) == 'i' && PyUnicode_READ(kind, str, idx + 6) == 't' && PyUnicode_READ(kind, str, idx + 7) == 'y') { - rval = _parse_constant(s, JSON_Infinity, idx, next_idx_ptr); + rval = _parse_constant(s, pystr, JSON_Infinity, idx, next_idx_ptr); } else fallthrough = 1; @@ -2306,7 +2312,7 @@ scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_ PyUnicode_READ(kind, str, idx + 6) == 'i' && PyUnicode_READ(kind, str, idx + 7) == 't' && PyUnicode_READ(kind, str, idx + 8) == 'y') { - rval = _parse_constant(s, JSON_NegInfinity, idx, next_idx_ptr); + rval = _parse_constant(s, pystr, JSON_NegInfinity, idx, next_idx_ptr); } else fallthrough = 1; diff --git a/lib/simplejson/decoder.py b/lib/simplejson/decoder.py index 1a8f772f..c99a976d 100644 --- a/lib/simplejson/decoder.py +++ b/lib/simplejson/decoder.py @@ -46,9 +46,35 @@ BACKSLASH = { DEFAULT_ENCODING = "utf-8" +if hasattr(sys, 'get_int_max_str_digits'): + bounded_int = int +else: + def bounded_int(s, INT_MAX_STR_DIGITS=4300): + """Backport of the integer string length conversion limitation + + https://docs.python.org/3/library/stdtypes.html#int-max-str-digits + """ + if len(s) > INT_MAX_STR_DIGITS: + raise ValueError("Exceeds the limit (%s) for integer string conversion: value has %s digits" % (INT_MAX_STR_DIGITS, len(s))) + return int(s) + + +def scan_four_digit_hex(s, end, _m=re.compile(r'^[0-9a-fA-F]{4}$').match): + """Scan a four digit hex number from s[end:end + 4] + """ + msg = "Invalid \\uXXXX escape sequence" + esc = s[end:end + 4] + if not _m(esc): + raise JSONDecodeError(msg, s, end - 2) + try: + return int(esc, 16), end + 4 + except ValueError: + raise JSONDecodeError(msg, s, end - 2) + def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match, _join=u''.join, - _PY3=PY3, _maxunicode=sys.maxunicode): + _PY3=PY3, _maxunicode=sys.maxunicode, + _scan_four_digit_hex=scan_four_digit_hex): """Scan the string s for a JSON string. End is the index of the character in s after the quote that started the JSON string. Unescapes all valid JSON string escape sequences and raises ValueError @@ -67,6 +93,7 @@ def py_scanstring(s, end, encoding=None, strict=True, if chunk is None: raise JSONDecodeError( "Unterminated string starting at", s, begin) + prev_end = end end = chunk.end() content, terminator = chunk.groups() # Content is contains zero or more unescaped string characters @@ -81,7 +108,7 @@ def py_scanstring(s, end, encoding=None, strict=True, elif terminator != '\\': if strict: msg = "Invalid control character %r at" - raise JSONDecodeError(msg, s, end) + raise JSONDecodeError(msg, s, prev_end) else: _append(terminator) continue @@ -100,35 +127,18 @@ def py_scanstring(s, end, encoding=None, strict=True, end += 1 else: # Unicode escape sequence - msg = "Invalid \\uXXXX escape sequence" - esc = s[end + 1:end + 5] - escX = esc[1:2] - if len(esc) != 4 or escX == 'x' or escX == 'X': - raise JSONDecodeError(msg, s, end - 1) - try: - uni = int(esc, 16) - except ValueError: - raise JSONDecodeError(msg, s, end - 1) - if uni < 0 or uni > _maxunicode: - raise JSONDecodeError(msg, s, end - 1) - end += 5 + uni, end = _scan_four_digit_hex(s, end + 1) # Check for surrogate pair on UCS-4 systems # Note that this will join high/low surrogate pairs # but will also pass unpaired surrogates through if (_maxunicode > 65535 and uni & 0xfc00 == 0xd800 and s[end:end + 2] == '\\u'): - esc2 = s[end + 2:end + 6] - escX = esc2[1:2] - if len(esc2) == 4 and not (escX == 'x' or escX == 'X'): - try: - uni2 = int(esc2, 16) - except ValueError: - raise JSONDecodeError(msg, s, end) - if uni2 & 0xfc00 == 0xdc00: - uni = 0x10000 + (((uni - 0xd800) << 10) | - (uni2 - 0xdc00)) - end += 6 + uni2, end2 = _scan_four_digit_hex(s, end + 2) + if uni2 & 0xfc00 == 0xdc00: + uni = 0x10000 + (((uni - 0xd800) << 10) | + (uni2 - 0xdc00)) + end = end2 char = unichr(uni) # Append the unescaped character _append(char) @@ -169,7 +179,7 @@ def JSONObject(state, encoding, strict, scan_once, object_hook, return pairs, end + 1 elif nextchar != '"': raise JSONDecodeError( - "Expecting property name enclosed in double quotes", + "Expecting property name enclosed in double quotes or '}'", s, end) end += 1 while True: @@ -296,14 +306,15 @@ class JSONDecoder(object): | null | None | +---------------+-------------------+ - It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + When allow_nan=True, it also understands + ``NaN``, ``Infinity``, and ``-Infinity`` as their corresponding ``float`` values, which is outside the JSON spec. """ def __init__(self, encoding=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, strict=True, - object_pairs_hook=None): + object_pairs_hook=None, allow_nan=False): """ *encoding* determines the encoding used to interpret any :class:`str` objects decoded by this instance (``'utf-8'`` by @@ -336,10 +347,13 @@ class JSONDecoder(object): ``int(num_str)``. This can be used to use another datatype or parser for JSON integers (e.g. :class:`float`). - *parse_constant*, if specified, will be called with one of the - following strings: ``'-Infinity'``, ``'Infinity'``, ``'NaN'``. This - can be used to raise an exception if invalid JSON numbers are - encountered. + *allow_nan*, if True (default false), will allow the parser to + accept the non-standard floats ``NaN``, ``Infinity``, and ``-Infinity``. + + *parse_constant*, if specified, will be + called with one of the following strings: ``'-Infinity'``, + ``'Infinity'``, ``'NaN'``. It is not recommended to use this feature, + as it is rare to parse non-compliant JSON containing these values. *strict* controls the parser's behavior when it encounters an invalid control character in a string. The default setting of @@ -353,8 +367,8 @@ class JSONDecoder(object): self.object_hook = object_hook self.object_pairs_hook = object_pairs_hook self.parse_float = parse_float or float - self.parse_int = parse_int or int - self.parse_constant = parse_constant or _CONSTANTS.__getitem__ + self.parse_int = parse_int or bounded_int + self.parse_constant = parse_constant or (allow_nan and _CONSTANTS.__getitem__ or None) self.strict = strict self.parse_object = JSONObject self.parse_array = JSONArray diff --git a/lib/simplejson/encoder.py b/lib/simplejson/encoder.py index e93fe43f..661ff361 100644 --- a/lib/simplejson/encoder.py +++ b/lib/simplejson/encoder.py @@ -5,7 +5,7 @@ import re from operator import itemgetter # Do not import Decimal directly to avoid reload issues import decimal -from .compat import unichr, binary_type, text_type, string_types, integer_types, PY3 +from .compat import binary_type, text_type, string_types, integer_types, PY3 def _import_speedups(): try: from . import _speedups @@ -140,7 +140,7 @@ class JSONEncoder(object): key_separator = ': ' def __init__(self, skipkeys=False, ensure_ascii=True, - check_circular=True, allow_nan=True, sort_keys=False, + check_circular=True, allow_nan=False, sort_keys=False, indent=None, separators=None, encoding='utf-8', default=None, use_decimal=True, namedtuple_as_object=True, tuple_as_array=True, bigint_as_string=False, @@ -161,10 +161,11 @@ class JSONEncoder(object): prevent an infinite recursion (which would cause an OverflowError). Otherwise, no such check takes place. - If allow_nan is true, then NaN, Infinity, and -Infinity will be - encoded as such. This behavior is not JSON specification compliant, - but is consistent with most JavaScript based encoders and decoders. - Otherwise, it will be a ValueError to encode such floats. + If allow_nan is true (default: False), then out of range float + values (nan, inf, -inf) will be serialized to + their JavaScript equivalents (NaN, Infinity, -Infinity) + instead of raising a ValueError. See + ignore_nan for ECMA-262 compliant behavior. If sort_keys is true, then the output of dictionaries will be sorted by key; this is useful for regression tests to ensure @@ -294,7 +295,7 @@ class JSONEncoder(object): # This doesn't pass the iterator directly to ''.join() because the # exceptions aren't as detailed. The list call should be roughly # equivalent to the PySequence_Fast that ''.join() would do. - chunks = self.iterencode(o, _one_shot=True) + chunks = self.iterencode(o) if not isinstance(chunks, (list, tuple)): chunks = list(chunks) if self.ensure_ascii: @@ -302,7 +303,7 @@ class JSONEncoder(object): else: return u''.join(chunks) - def iterencode(self, o, _one_shot=False): + def iterencode(self, o): """Encode the given object and yield each string representation as available. @@ -356,8 +357,7 @@ class JSONEncoder(object): key_memo = {} int_as_string_bitcount = ( 53 if self.bigint_as_string else self.int_as_string_bitcount) - if (_one_shot and c_make_encoder is not None - and self.indent is None): + if (c_make_encoder is not None and self.indent is None): _iterencode = c_make_encoder( markers, self.default, _encoder, self.indent, self.key_separator, self.item_separator, self.sort_keys, @@ -370,7 +370,7 @@ class JSONEncoder(object): _iterencode = _make_iterencode( markers, self.default, _encoder, self.indent, floatstr, self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, _one_shot, self.use_decimal, + self.skipkeys, self.use_decimal, self.namedtuple_as_object, self.tuple_as_array, int_as_string_bitcount, self.item_sort_key, self.encoding, self.for_json, @@ -398,14 +398,14 @@ class JSONEncoderForHTML(JSONEncoder): def encode(self, o): # Override JSONEncoder.encode because it has hacks for # performance that make things more complicated. - chunks = self.iterencode(o, True) + chunks = self.iterencode(o) if self.ensure_ascii: return ''.join(chunks) else: return u''.join(chunks) - def iterencode(self, o, _one_shot=False): - chunks = super(JSONEncoderForHTML, self).iterencode(o, _one_shot) + def iterencode(self, o): + chunks = super(JSONEncoderForHTML, self).iterencode(o) for chunk in chunks: chunk = chunk.replace('&', '\\u0026') chunk = chunk.replace('<', '\\u003c') @@ -419,7 +419,7 @@ class JSONEncoderForHTML(JSONEncoder): def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, - _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + _key_separator, _item_separator, _sort_keys, _skipkeys, _use_decimal, _namedtuple_as_object, _tuple_as_array, _int_as_string_bitcount, _item_sort_key, _encoding,_for_json, diff --git a/lib/simplejson/scanner.py b/lib/simplejson/scanner.py index 85e385e1..34710d68 100644 --- a/lib/simplejson/scanner.py +++ b/lib/simplejson/scanner.py @@ -60,11 +60,11 @@ def py_make_scanner(context): else: res = parse_int(integer) return res, m.end() - elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': + elif parse_constant and nextchar == 'N' and string[idx:idx + 3] == 'NaN': return parse_constant('NaN'), idx + 3 - elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': + elif parse_constant and nextchar == 'I' and string[idx:idx + 8] == 'Infinity': return parse_constant('Infinity'), idx + 8 - elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': + elif parse_constant and nextchar == '-' and string[idx:idx + 9] == '-Infinity': return parse_constant('-Infinity'), idx + 9 else: raise JSONDecodeError(errmsg, string, idx) From 1c6a5bb59abbe5b491d3401c3743c966977a814b Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 12 Apr 2023 14:10:23 +0100 Subject: [PATCH 02/36] =?UTF-8?q?Update=20Tornado=20Web=20Server=206.3.0?= =?UTF-8?q?=20(7186b86)=20=E2=86=92=206.3.1=20(419838b).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 3 ++- lib/tornado/__init__.py | 4 ++-- lib/tornado/web.py | 25 +++++++++++++++++++++---- lib/tornado/websocket.py | 18 +++++++++--------- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bd5f9be3..83f7a7fc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ### 3.29.0 (2023-xx-xx xx:xx:00 UTC) * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) +* Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) ### 3.28.0 (2023-04-12 13:05:00 UTC) @@ -20,7 +21,7 @@ * Add optional "freespace" parameter to endpoints: sg.getrootdirs, sg.addrootdir, sg.deleterootdir * Change update help of affected endpoints * Fix explicitly save rootdirs after adding or deleting via Web API -* Change add Rarbg UHD search category +* Change add Rarbg UHD search category ### 3.27.13 (2023-04-12 10:15:00 UTC) diff --git a/lib/tornado/__init__.py b/lib/tornado/__init__.py index 060b836a..afbd7150 100644 --- a/lib/tornado/__init__.py +++ b/lib/tornado/__init__.py @@ -22,8 +22,8 @@ # is zero for an official release, positive for a development branch, # or negative for a release candidate or beta (after the base version # number has been incremented) -version = "6.3.dev1" -version_info = (6, 3, 0, -100) +version = "6.3.1" +version_info = (6, 3, 1, 0) import importlib import typing diff --git a/lib/tornado/web.py b/lib/tornado/web.py index 18634d89..3b676e3c 100644 --- a/lib/tornado/web.py +++ b/lib/tornado/web.py @@ -79,6 +79,7 @@ import socket import sys import threading import time +import warnings import tornado import traceback import types @@ -91,10 +92,8 @@ from tornado import gen from tornado.httpserver import HTTPServer from tornado import httputil from tornado import iostream -import tornado.locale from tornado import locale from tornado.log import access_log, app_log, gen_log -import tornado.netutil from tornado import template from tornado.escape import utf8, _unicode from tornado.routing import ( @@ -609,6 +608,7 @@ class RequestHandler(object): httponly: bool = False, secure: bool = False, samesite: Optional[str] = None, + **kwargs: Any, ) -> None: """Sets an outgoing cookie name/value with the given options. @@ -625,6 +625,10 @@ class RequestHandler(object): to set an expiration time in days from today (if both are set, ``expires`` is used). + .. deprecated:: 6.3 + Keyword arguments are currently accepted case-insensitively. + In Tornado 7.0 this will be changed to only accept lowercase + arguments. """ # The cookie library only accepts type str, in both python 2 and 3 name = escape.native_str(name) @@ -659,6 +663,17 @@ class RequestHandler(object): morsel["secure"] = True if samesite: morsel["samesite"] = samesite + if kwargs: + # The setitem interface is case-insensitive, so continue to support + # kwargs for backwards compatibility until we can remove deprecated + # features. + for k, v in kwargs.items(): + morsel[k] = v + warnings.warn( + f"Deprecated arguments to set_cookie: {set(kwargs.keys())} " + "(should be lowercase)", + DeprecationWarning, + ) def clear_cookie(self, name: str, **kwargs: Any) -> None: """Deletes the cookie with the given name. @@ -1486,7 +1501,8 @@ class RequestHandler(object): if version is None: if self.current_user and "expires_days" not in cookie_kwargs: cookie_kwargs["expires_days"] = 30 - self.set_cookie("_xsrf", self._xsrf_token, **cookie_kwargs) + cookie_name = self.settings.get("xsrf_cookie_name", "_xsrf") + self.set_cookie(cookie_name, self._xsrf_token, **cookie_kwargs) return self._xsrf_token def _get_raw_xsrf_token(self) -> Tuple[Optional[int], bytes, float]: @@ -1501,7 +1517,8 @@ class RequestHandler(object): for version 1 cookies) """ if not hasattr(self, "_raw_xsrf_token"): - cookie = self.get_cookie("_xsrf") + cookie_name = self.settings.get("xsrf_cookie_name", "_xsrf") + cookie = self.get_cookie(cookie_name) if cookie: version, token, timestamp = self._decode_xsrf_token(cookie) else: diff --git a/lib/tornado/websocket.py b/lib/tornado/websocket.py index 1d42e10b..d0abd425 100644 --- a/lib/tornado/websocket.py +++ b/lib/tornado/websocket.py @@ -1,16 +1,11 @@ """Implementation of the WebSocket protocol. `WebSockets `_ allow for bidirectional -communication between the browser and server. - -WebSockets are supported in the current versions of all major browsers, -although older versions that do not support WebSockets are still in use -(refer to http://caniuse.com/websockets for details). +communication between the browser and server. WebSockets are supported in the +current versions of all major browsers. This module implements the final version of the WebSocket protocol as -defined in `RFC 6455 `_. Certain -browser versions (notably Safari 5.x) implemented an earlier draft of -the protocol (known as "draft 76") and are not compatible with this module. +defined in `RFC 6455 `_. .. versionchanged:: 4.0 Removed support for the draft 76 protocol version. @@ -23,7 +18,7 @@ import hashlib import os import sys import struct -import tornado.web +import tornado from urllib.parse import urlparse import zlib @@ -1589,6 +1584,7 @@ def websocket_connect( ping_timeout: Optional[float] = None, max_message_size: int = _default_max_message_size, subprotocols: Optional[List[str]] = None, + resolver: Optional[Resolver] = None, ) -> "Awaitable[WebSocketClientConnection]": """Client-side websocket support. @@ -1632,6 +1628,9 @@ def websocket_connect( .. versionchanged:: 5.1 Added the ``subprotocols`` argument. + + .. versionchanged:: 6.3 + Added the ``resolver`` argument. """ if isinstance(url, httpclient.HTTPRequest): assert connect_timeout is None @@ -1653,6 +1652,7 @@ def websocket_connect( ping_timeout=ping_timeout, max_message_size=max_message_size, subprotocols=subprotocols, + resolver=resolver, ) if callback is not None: IOLoop.current().add_future(conn.connect_future, callback) From 59fba38600054c8bc8d736259c8b0ea9bc72e902 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 12 Apr 2023 14:37:13 +0100 Subject: [PATCH 03/36] =?UTF-8?q?Update=20Msgpack=201.0.4=20(b5acfd5)=20?= =?UTF-8?q?=E2=86=92=201.0.5=20(0516c2c).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/msgpack/__init__.py | 4 ++-- lib/msgpack/ext.py | 2 +- lib/msgpack/fallback.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 83f7a7fc..a80d17c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.29.0 (2023-xx-xx xx:xx:00 UTC) +* Update Msgpack 1.0.4 (b5acfd5) to 1.0.5 (0516c2c) * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) * Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) diff --git a/lib/msgpack/__init__.py b/lib/msgpack/__init__.py index 50710218..1300b866 100644 --- a/lib/msgpack/__init__.py +++ b/lib/msgpack/__init__.py @@ -6,8 +6,8 @@ import os import sys -version = (1, 0, 4) -__version__ = "1.0.4" +version = (1, 0, 5) +__version__ = "1.0.5" if os.environ.get("MSGPACK_PUREPYTHON") or sys.version_info[0] == 2: diff --git a/lib/msgpack/ext.py b/lib/msgpack/ext.py index 25544c55..23e0d6b4 100644 --- a/lib/msgpack/ext.py +++ b/lib/msgpack/ext.py @@ -56,7 +56,7 @@ class Timestamp(object): Note: Negative times (before the UNIX epoch) are represented as negative seconds + positive ns. """ if not isinstance(seconds, int_types): - raise TypeError("seconds must be an interger") + raise TypeError("seconds must be an integer") if not isinstance(nanoseconds, int_types): raise TypeError("nanoseconds must be an integer") if not (0 <= nanoseconds < 10**9): diff --git a/lib/msgpack/fallback.py b/lib/msgpack/fallback.py index f560c7b5..e8cebc1b 100644 --- a/lib/msgpack/fallback.py +++ b/lib/msgpack/fallback.py @@ -814,7 +814,7 @@ class Packer(object): self._pack_raw_header(n) return self._buffer.write(obj) if check(obj, memoryview): - n = len(obj) * obj.itemsize + n = obj.nbytes if n >= 2**32: raise ValueError("Memoryview is too large") self._pack_bin_header(n) From e037e4b53b089d7123742b9932d31b06441a9ede Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 12 Apr 2023 14:51:13 +0100 Subject: [PATCH 04/36] =?UTF-8?q?Update=20filelock=203.9.0=20(ce3e891)=20?= =?UTF-8?q?=E2=86=92=203.11.0=20(d3241b9).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/filelock/__init__.py | 2 +- lib/filelock/_api.py | 52 ++++++++++++++++++++-------------------- lib/filelock/_error.py | 19 ++++++++++++--- lib/filelock/_soft.py | 20 +++++++--------- lib/filelock/_unix.py | 13 +++++++--- lib/filelock/_windows.py | 4 ++-- lib/filelock/version.py | 4 ++-- 8 files changed, 67 insertions(+), 48 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a80d17c0..6603a5db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.29.0 (2023-xx-xx xx:xx:00 UTC) +* Update filelock 3.9.0 (ce3e891) to 3.11.0 (d3241b9) * Update Msgpack 1.0.4 (b5acfd5) to 1.0.5 (0516c2c) * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) * Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) diff --git a/lib/filelock/__init__.py b/lib/filelock/__init__.py index 36fe7e43..31d2bce1 100644 --- a/lib/filelock/__init__.py +++ b/lib/filelock/__init__.py @@ -30,7 +30,7 @@ else: # pragma: win32 no cover else: _FileLock = SoftFileLock if warnings is not None: - warnings.warn("only soft file lock is available") + warnings.warn("only soft file lock is available", stacklevel=2) #: Alias for the lock, which should be used for the current platform. On Windows, this is an alias for # :class:`WindowsFileLock`, on Unix for :class:`UnixFileLock` and otherwise for :class:`SoftFileLock`. diff --git a/lib/filelock/_api.py b/lib/filelock/_api.py index 273b82e6..958369a6 100644 --- a/lib/filelock/_api.py +++ b/lib/filelock/_api.py @@ -6,7 +6,7 @@ import os import time import warnings from abc import ABC, abstractmethod -from threading import Lock +from threading import local from types import TracebackType from typing import Any @@ -36,10 +36,15 @@ class AcquireReturnProxy: self.lock.release() -class BaseFileLock(ABC, contextlib.ContextDecorator): +class BaseFileLock(ABC, contextlib.ContextDecorator, local): """Abstract base class for a file lock object.""" - def __init__(self, lock_file: str | os.PathLike[Any], timeout: float = -1) -> None: + def __init__( + self, + lock_file: str | os.PathLike[Any], + timeout: float = -1, + mode: int = 0o644, + ) -> None: """ Create a new lock object. @@ -47,6 +52,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it to a negative value. A timeout of 0 means, that there is exactly one attempt to acquire the file lock. + : param mode: file permissions for the lockfile. """ # The path to the lock file. self._lock_file: str = os.fspath(lock_file) @@ -58,8 +64,8 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): # The default timeout value. self._timeout: float = timeout - # We use this lock primarily for the lock counter. - self._thread_lock: Lock = Lock() + # The mode for the lock files + self._mode: int = mode # The lock counter is used for implementing the nested locking mechanism. Whenever the lock is acquired, the # counter is increased and the lock is only released, when this value is 0 again. @@ -159,26 +165,23 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): poll_interval = poll_intervall # Increment the number right at the beginning. We can still undo it, if something fails. - with self._thread_lock: - self._lock_counter += 1 + self._lock_counter += 1 lock_id = id(self) lock_filename = self._lock_file - start_time = time.monotonic() + start_time = time.perf_counter() try: while True: - with self._thread_lock: - if not self.is_locked: - _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) - self._acquire() - + if not self.is_locked: + _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) + self._acquire() if self.is_locked: _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) break elif blocking is False: _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename) raise Timeout(self._lock_file) - elif 0 <= timeout < time.monotonic() - start_time: + elif 0 <= timeout < time.perf_counter() - start_time: _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename) raise Timeout(self._lock_file) else: @@ -186,8 +189,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): _LOGGER.debug(msg, lock_id, lock_filename, poll_interval) time.sleep(poll_interval) except BaseException: # Something did go wrong, so decrement the counter. - with self._thread_lock: - self._lock_counter = max(0, self._lock_counter - 1) + self._lock_counter = max(0, self._lock_counter - 1) raise return AcquireReturnProxy(lock=self) @@ -198,18 +200,16 @@ class BaseFileLock(ABC, contextlib.ContextDecorator): :param force: If true, the lock counter is ignored and the lock is released in every case/ """ - with self._thread_lock: + if self.is_locked: + self._lock_counter -= 1 - if self.is_locked: - self._lock_counter -= 1 + if self._lock_counter == 0 or force: + lock_id, lock_filename = id(self), self._lock_file - if self._lock_counter == 0 or force: - lock_id, lock_filename = id(self), self._lock_file - - _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename) - self._release() - self._lock_counter = 0 - _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) + _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename) + self._release() + self._lock_counter = 0 + _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) def __enter__(self) -> BaseFileLock: """ diff --git a/lib/filelock/_error.py b/lib/filelock/_error.py index b3885214..e2bd6530 100644 --- a/lib/filelock/_error.py +++ b/lib/filelock/_error.py @@ -1,15 +1,28 @@ from __future__ import annotations +from typing import Any + class Timeout(TimeoutError): """Raised when the lock could not be acquired in *timeout* seconds.""" def __init__(self, lock_file: str) -> None: - #: The path of the file lock. - self.lock_file = lock_file + super().__init__() + self._lock_file = lock_file + + def __reduce__(self) -> str | tuple[Any, ...]: + return self.__class__, (self._lock_file,) # Properly pickle the exception def __str__(self) -> str: - return f"The file lock '{self.lock_file}' could not be acquired." + return f"The file lock '{self._lock_file}' could not be acquired." + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.lock_file!r})" + + @property + def lock_file(self) -> str: + """:return: The path of the file lock.""" + return self._lock_file __all__ = [ diff --git a/lib/filelock/_soft.py b/lib/filelock/_soft.py index cb09799a..697f6c4c 100644 --- a/lib/filelock/_soft.py +++ b/lib/filelock/_soft.py @@ -2,7 +2,7 @@ from __future__ import annotations import os import sys -from errno import EACCES, EEXIST, ENOENT +from errno import EACCES, EEXIST from ._api import BaseFileLock from ._util import raise_on_exist_ro_file @@ -14,24 +14,22 @@ class SoftFileLock(BaseFileLock): def _acquire(self) -> None: raise_on_exist_ro_file(self._lock_file) # first check for exists and read-only mode as the open will mask this case as EEXIST - mode = ( + flags = ( os.O_WRONLY # open for writing only | os.O_CREAT | os.O_EXCL # together with above raise EEXIST if the file specified by filename exists | os.O_TRUNC # truncate the file to zero byte ) try: - fd = os.open(self._lock_file, mode) - except OSError as exception: - if exception.errno == EEXIST: # expected if cannot lock - pass - elif exception.errno == ENOENT: # No such file or directory - parent directory is missing + file_handler = os.open(self._lock_file, flags, self._mode) + except OSError as exception: # re-raise unless expected exception + if not ( + exception.errno == EEXIST # lock already exist + or (exception.errno == EACCES and sys.platform == "win32") # has no access to this lock + ): # pragma: win32 no cover raise - elif exception.errno == EACCES and sys.platform != "win32": # pragma: win32 no cover - # Permission denied - parent dir is R/O - raise # note windows does not allow you to make a folder r/o only files else: - self._lock_file_fd = fd + self._lock_file_fd = file_handler def _release(self) -> None: os.close(self._lock_file_fd) # type: ignore # the lock file is definitely not None diff --git a/lib/filelock/_unix.py b/lib/filelock/_unix.py index 03b612c9..4c0a2603 100644 --- a/lib/filelock/_unix.py +++ b/lib/filelock/_unix.py @@ -2,6 +2,7 @@ from __future__ import annotations import os import sys +from errno import ENOSYS from typing import cast from ._api import BaseFileLock @@ -31,12 +32,18 @@ else: # pragma: win32 no cover """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" def _acquire(self) -> None: - open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC - fd = os.open(self._lock_file, open_mode) + open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC + fd = os.open(self._lock_file, open_flags, self._mode) + try: + os.fchmod(fd, self._mode) + except PermissionError: + pass # This locked is not owned by this UID try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except OSError: + except OSError as exception: os.close(fd) + if exception.errno == ENOSYS: # NotImplemented error + raise NotImplementedError("FileSystem does not appear to support flock; user SoftFileLock instead") else: self._lock_file_fd = fd diff --git a/lib/filelock/_windows.py b/lib/filelock/_windows.py index 60e68cb9..cb324c94 100644 --- a/lib/filelock/_windows.py +++ b/lib/filelock/_windows.py @@ -16,13 +16,13 @@ if sys.platform == "win32": # pragma: win32 cover def _acquire(self) -> None: raise_on_exist_ro_file(self._lock_file) - mode = ( + flags = ( os.O_RDWR # open for read and write | os.O_CREAT # create file if not exists | os.O_TRUNC # truncate file if not empty ) try: - fd = os.open(self._lock_file, mode) + fd = os.open(self._lock_file, flags, self._mode) except OSError as exception: if exception.errno == ENOENT: # No such file or directory raise diff --git a/lib/filelock/version.py b/lib/filelock/version.py index d20e218b..16805e82 100644 --- a/lib/filelock/version.py +++ b/lib/filelock/version.py @@ -1,4 +1,4 @@ # file generated by setuptools_scm # don't change, don't track in version control -__version__ = version = '3.9.0' -__version_tuple__ = version_tuple = (3, 9, 0) +__version__ = version = '3.11.0' +__version_tuple__ = version_tuple = (3, 11, 0) From de209c9acfd0f884780a53b06a8713e0f1746ec3 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 12 Apr 2023 15:10:56 +0100 Subject: [PATCH 05/36] =?UTF-8?q?Update=20Requests=20library=202.28.1=20(e?= =?UTF-8?q?c553c2)=20=E2=86=92=202.29.0=20(87d63de).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/requests/__version__.py | 6 +-- lib/requests/_internal_utils.py | 6 ++- lib/requests/adapters.py | 71 ++++++--------------------------- lib/requests/api.py | 6 +-- lib/requests/utils.py | 32 +++++++++------ 6 files changed, 44 insertions(+), 78 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6603a5db..d0b79138 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ * Update filelock 3.9.0 (ce3e891) to 3.11.0 (d3241b9) * Update Msgpack 1.0.4 (b5acfd5) to 1.0.5 (0516c2c) +* Update Requests library 2.28.1 (ec553c2) to 2.29.0 (87d63de) * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) * Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) diff --git a/lib/requests/__version__.py b/lib/requests/__version__.py index e725ada6..4775ae32 100644 --- a/lib/requests/__version__.py +++ b/lib/requests/__version__.py @@ -5,10 +5,10 @@ __title__ = "requests" __description__ = "Python HTTP for Humans." __url__ = "https://requests.readthedocs.io" -__version__ = "2.28.1" -__build__ = 0x022801 +__version__ = "2.29.0" +__build__ = 0x022900 __author__ = "Kenneth Reitz" __author_email__ = "me@kennethreitz.org" __license__ = "Apache 2.0" -__copyright__ = "Copyright 2022 Kenneth Reitz" +__copyright__ = "Copyright Kenneth Reitz" __cake__ = "\u2728 \U0001f370 \u2728" diff --git a/lib/requests/_internal_utils.py b/lib/requests/_internal_utils.py index 7dc9bc53..f2cf635e 100644 --- a/lib/requests/_internal_utils.py +++ b/lib/requests/_internal_utils.py @@ -14,9 +14,11 @@ _VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*$") _VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*$|^$") _VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*$|^$") +_HEADER_VALIDATORS_STR = (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR) +_HEADER_VALIDATORS_BYTE = (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE) HEADER_VALIDATORS = { - bytes: (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE), - str: (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR), + bytes: _HEADER_VALIDATORS_BYTE, + str: _HEADER_VALIDATORS_STR, } diff --git a/lib/requests/adapters.py b/lib/requests/adapters.py index d3b2d5bb..f13ae4e5 100644 --- a/lib/requests/adapters.py +++ b/lib/requests/adapters.py @@ -22,7 +22,6 @@ from urllib3.exceptions import ProxyError as _ProxyError from urllib3.exceptions import ReadTimeoutError, ResponseError from urllib3.exceptions import SSLError as _SSLError from urllib3.poolmanager import PoolManager, proxy_from_url -from urllib3.response import HTTPResponse from urllib3.util import Timeout as TimeoutSauce from urllib3.util import parse_url from urllib3.util.retry import Retry @@ -485,63 +484,19 @@ class HTTPAdapter(BaseAdapter): timeout = TimeoutSauce(connect=timeout, read=timeout) try: - if not chunked: - resp = conn.urlopen( - method=request.method, - url=url, - body=request.body, - headers=request.headers, - redirect=False, - assert_same_host=False, - preload_content=False, - decode_content=False, - retries=self.max_retries, - timeout=timeout, - ) - - # Send the request. - else: - if hasattr(conn, "proxy_pool"): - conn = conn.proxy_pool - - low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT) - - try: - skip_host = "Host" in request.headers - low_conn.putrequest( - request.method, - url, - skip_accept_encoding=True, - skip_host=skip_host, - ) - - for header, value in request.headers.items(): - low_conn.putheader(header, value) - - low_conn.endheaders() - - for i in request.body: - low_conn.send(hex(len(i))[2:].encode("utf-8")) - low_conn.send(b"\r\n") - low_conn.send(i) - low_conn.send(b"\r\n") - low_conn.send(b"0\r\n\r\n") - - # Receive the response from the server - r = low_conn.getresponse() - - resp = HTTPResponse.from_httplib( - r, - pool=conn, - connection=low_conn, - preload_content=False, - decode_content=False, - ) - except Exception: - # If we hit any problems here, clean up the connection. - # Then, raise so that we can handle the actual exception. - low_conn.close() - raise + resp = conn.urlopen( + method=request.method, + url=url, + body=request.body, + headers=request.headers, + redirect=False, + assert_same_host=False, + preload_content=False, + decode_content=False, + retries=self.max_retries, + timeout=timeout, + chunked=chunked, + ) except (ProtocolError, OSError) as err: raise ConnectionError(err, request=request) diff --git a/lib/requests/api.py b/lib/requests/api.py index 2f71aaed..cd0b3eea 100644 --- a/lib/requests/api.py +++ b/lib/requests/api.py @@ -106,7 +106,7 @@ def post(url, data=None, json=None, **kwargs): :param url: URL for the new :class:`Request` object. :param data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the :class:`Request`. - :param json: (optional) json data to send in the body of the :class:`Request`. + :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. :param \*\*kwargs: Optional arguments that ``request`` takes. :return: :class:`Response ` object :rtype: requests.Response @@ -121,7 +121,7 @@ def put(url, data=None, **kwargs): :param url: URL for the new :class:`Request` object. :param data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the :class:`Request`. - :param json: (optional) json data to send in the body of the :class:`Request`. + :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. :param \*\*kwargs: Optional arguments that ``request`` takes. :return: :class:`Response ` object :rtype: requests.Response @@ -136,7 +136,7 @@ def patch(url, data=None, **kwargs): :param url: URL for the new :class:`Request` object. :param data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the :class:`Request`. - :param json: (optional) json data to send in the body of the :class:`Request`. + :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. :param \*\*kwargs: Optional arguments that ``request`` takes. :return: :class:`Response ` object :rtype: requests.Response diff --git a/lib/requests/utils.py b/lib/requests/utils.py index ad535838..a367417f 100644 --- a/lib/requests/utils.py +++ b/lib/requests/utils.py @@ -25,7 +25,12 @@ from . import certs from .__version__ import __version__ # to_native_string is unused here, but imported here for backwards compatibility -from ._internal_utils import HEADER_VALIDATORS, to_native_string # noqa: F401 +from ._internal_utils import ( # noqa: F401 + _HEADER_VALIDATORS_BYTE, + _HEADER_VALIDATORS_STR, + HEADER_VALIDATORS, + to_native_string, +) from .compat import ( Mapping, basestring, @@ -1031,20 +1036,23 @@ def check_header_validity(header): :param header: tuple, in the format (name, value). """ name, value = header - - for part in header: - if type(part) not in HEADER_VALIDATORS: - raise InvalidHeader( - f"Header part ({part!r}) from {{{name!r}: {value!r}}} must be " - f"of type str or bytes, not {type(part)}" - ) - - _validate_header_part(name, "name", HEADER_VALIDATORS[type(name)][0]) - _validate_header_part(value, "value", HEADER_VALIDATORS[type(value)][1]) + _validate_header_part(header, name, 0) + _validate_header_part(header, value, 1) -def _validate_header_part(header_part, header_kind, validator): +def _validate_header_part(header, header_part, header_validator_index): + if isinstance(header_part, str): + validator = _HEADER_VALIDATORS_STR[header_validator_index] + elif isinstance(header_part, bytes): + validator = _HEADER_VALIDATORS_BYTE[header_validator_index] + else: + raise InvalidHeader( + f"Header part ({header_part!r}) from {header} " + f"must be of type str or bytes, not {type(header_part)}" + ) + if not validator.match(header_part): + header_kind = "name" if header_validator_index == 0 else "value" raise InvalidHeader( f"Invalid leading whitespace, reserved character(s), or return" f"character(s) in header {header_kind}: {header_part!r}" From 7ea6a684bf987350c7f9bcd420e3f677ccc33848 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Thu, 27 Apr 2023 12:22:46 +0100 Subject: [PATCH 06/36] =?UTF-8?q?Update=20urllib3=201.26.14=20(a06c05c)=20?= =?UTF-8?q?=E2=86=92=201.26.15=20(25cca389).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/urllib3/__init__.py | 17 ++ lib/urllib3/_version.py | 2 +- lib/urllib3/connectionpool.py | 38 ++++- .../packages/backports/weakref_finalize.py | 155 ++++++++++++++++++ lib/urllib3/poolmanager.py | 2 +- 6 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 lib/urllib3/packages/backports/weakref_finalize.py diff --git a/CHANGES.md b/CHANGES.md index d0b79138..995ec640 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ * Update Requests library 2.28.1 (ec553c2) to 2.29.0 (87d63de) * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) * Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) +* Update urllib3 1.26.14 (a06c05c) to 1.26.15 (25cca389) ### 3.28.0 (2023-04-12 13:05:00 UTC) diff --git a/lib/urllib3/__init__.py b/lib/urllib3/__init__.py index fe86b59d..c6fa3821 100644 --- a/lib/urllib3/__init__.py +++ b/lib/urllib3/__init__.py @@ -19,6 +19,23 @@ from .util.retry import Retry from .util.timeout import Timeout from .util.url import get_host +# === NOTE TO REPACKAGERS AND VENDORS === +# Please delete this block, this logic is only +# for urllib3 being distributed via PyPI. +# See: https://github.com/urllib3/urllib3/issues/2680 +try: + import urllib3_secure_extra # type: ignore # noqa: F401 +except ImportError: + pass +else: + warnings.warn( + "'urllib3[secure]' extra is deprecated and will be removed " + "in a future release of urllib3 2.x. Read more in this issue: " + "https://github.com/urllib3/urllib3/issues/2680", + category=DeprecationWarning, + stacklevel=2, + ) + __author__ = "Andrey Petrov (andrey.petrov@shazow.net)" __license__ = "MIT" __version__ = __version__ diff --git a/lib/urllib3/_version.py b/lib/urllib3/_version.py index 7c031661..e12dd0e7 100644 --- a/lib/urllib3/_version.py +++ b/lib/urllib3/_version.py @@ -1,2 +1,2 @@ # This file is protected via CODEOWNERS -__version__ = "1.26.14" +__version__ = "1.26.15" diff --git a/lib/urllib3/connectionpool.py b/lib/urllib3/connectionpool.py index c23d736b..96844d93 100644 --- a/lib/urllib3/connectionpool.py +++ b/lib/urllib3/connectionpool.py @@ -50,6 +50,13 @@ from .util.url import Url, _encode_target from .util.url import _normalize_host as normalize_host from .util.url import get_host, parse_url +try: # Platform-specific: Python 3 + import weakref + + weakref_finalize = weakref.finalize +except AttributeError: # Platform-specific: Python 2 + from .packages.backports.weakref_finalize import weakref_finalize + xrange = six.moves.xrange log = logging.getLogger(__name__) @@ -220,6 +227,16 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): self.conn_kw["proxy"] = self.proxy self.conn_kw["proxy_config"] = self.proxy_config + # Do not pass 'self' as callback to 'finalize'. + # Then the 'finalize' would keep an endless living (leak) to self. + # By just passing a reference to the pool allows the garbage collector + # to free self if nobody else has a reference to it. + pool = self.pool + + # Close all the HTTPConnections in the pool before the + # HTTPConnectionPool object is garbage collected. + weakref_finalize(self, _close_pool_connections, pool) + def _new_conn(self): """ Return a fresh :class:`HTTPConnection`. @@ -489,14 +506,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): # Disable access to the pool old_pool, self.pool = self.pool, None - try: - while True: - conn = old_pool.get(block=False) - if conn: - conn.close() - - except queue.Empty: - pass # Done. + # Close all the HTTPConnections in the pool. + _close_pool_connections(old_pool) def is_same_host(self, url): """ @@ -1108,3 +1119,14 @@ def _normalize_host(host, scheme): if host.startswith("[") and host.endswith("]"): host = host[1:-1] return host + + +def _close_pool_connections(pool): + """Drains a queue of connections and closes each one.""" + try: + while True: + conn = pool.get(block=False) + if conn: + conn.close() + except queue.Empty: + pass # Done. diff --git a/lib/urllib3/packages/backports/weakref_finalize.py b/lib/urllib3/packages/backports/weakref_finalize.py new file mode 100644 index 00000000..a2f2966e --- /dev/null +++ b/lib/urllib3/packages/backports/weakref_finalize.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +""" +backports.weakref_finalize +~~~~~~~~~~~~~~~~~~ + +Backports the Python 3 ``weakref.finalize`` method. +""" +from __future__ import absolute_import + +import itertools +import sys +from weakref import ref + +__all__ = ["weakref_finalize"] + + +class weakref_finalize(object): + """Class for finalization of weakrefable objects + finalize(obj, func, *args, **kwargs) returns a callable finalizer + object which will be called when obj is garbage collected. The + first time the finalizer is called it evaluates func(*arg, **kwargs) + and returns the result. After this the finalizer is dead, and + calling it just returns None. + When the program exits any remaining finalizers for which the + atexit attribute is true will be run in reverse order of creation. + By default atexit is true. + """ + + # Finalizer objects don't have any state of their own. They are + # just used as keys to lookup _Info objects in the registry. This + # ensures that they cannot be part of a ref-cycle. + + __slots__ = () + _registry = {} + _shutdown = False + _index_iter = itertools.count() + _dirty = False + _registered_with_atexit = False + + class _Info(object): + __slots__ = ("weakref", "func", "args", "kwargs", "atexit", "index") + + def __init__(self, obj, func, *args, **kwargs): + if not self._registered_with_atexit: + # We may register the exit function more than once because + # of a thread race, but that is harmless + import atexit + + atexit.register(self._exitfunc) + weakref_finalize._registered_with_atexit = True + info = self._Info() + info.weakref = ref(obj, self) + info.func = func + info.args = args + info.kwargs = kwargs or None + info.atexit = True + info.index = next(self._index_iter) + self._registry[self] = info + weakref_finalize._dirty = True + + def __call__(self, _=None): + """If alive then mark as dead and return func(*args, **kwargs); + otherwise return None""" + info = self._registry.pop(self, None) + if info and not self._shutdown: + return info.func(*info.args, **(info.kwargs or {})) + + def detach(self): + """If alive then mark as dead and return (obj, func, args, kwargs); + otherwise return None""" + info = self._registry.get(self) + obj = info and info.weakref() + if obj is not None and self._registry.pop(self, None): + return (obj, info.func, info.args, info.kwargs or {}) + + def peek(self): + """If alive then return (obj, func, args, kwargs); + otherwise return None""" + info = self._registry.get(self) + obj = info and info.weakref() + if obj is not None: + return (obj, info.func, info.args, info.kwargs or {}) + + @property + def alive(self): + """Whether finalizer is alive""" + return self in self._registry + + @property + def atexit(self): + """Whether finalizer should be called at exit""" + info = self._registry.get(self) + return bool(info) and info.atexit + + @atexit.setter + def atexit(self, value): + info = self._registry.get(self) + if info: + info.atexit = bool(value) + + def __repr__(self): + info = self._registry.get(self) + obj = info and info.weakref() + if obj is None: + return "<%s object at %#x; dead>" % (type(self).__name__, id(self)) + else: + return "<%s object at %#x; for %r at %#x>" % ( + type(self).__name__, + id(self), + type(obj).__name__, + id(obj), + ) + + @classmethod + def _select_for_exit(cls): + # Return live finalizers marked for exit, oldest first + L = [(f, i) for (f, i) in cls._registry.items() if i.atexit] + L.sort(key=lambda item: item[1].index) + return [f for (f, i) in L] + + @classmethod + def _exitfunc(cls): + # At shutdown invoke finalizers for which atexit is true. + # This is called once all other non-daemonic threads have been + # joined. + reenable_gc = False + try: + if cls._registry: + import gc + + if gc.isenabled(): + reenable_gc = True + gc.disable() + pending = None + while True: + if pending is None or weakref_finalize._dirty: + pending = cls._select_for_exit() + weakref_finalize._dirty = False + if not pending: + break + f = pending.pop() + try: + # gc is disabled, so (assuming no daemonic + # threads) the following is the only line in + # this function which might trigger creation + # of a new finalizer + f() + except Exception: + sys.excepthook(*sys.exc_info()) + assert f not in cls._registry + finally: + # prevent any more finalizers from executing during shutdown + weakref_finalize._shutdown = True + if reenable_gc: + gc.enable() diff --git a/lib/urllib3/poolmanager.py b/lib/urllib3/poolmanager.py index ca4ec341..14b10daf 100644 --- a/lib/urllib3/poolmanager.py +++ b/lib/urllib3/poolmanager.py @@ -171,7 +171,7 @@ class PoolManager(RequestMethods): def __init__(self, num_pools=10, headers=None, **connection_pool_kw): RequestMethods.__init__(self, headers) self.connection_pool_kw = connection_pool_kw - self.pools = RecentlyUsedContainer(num_pools, dispose_func=lambda p: p.close()) + self.pools = RecentlyUsedContainer(num_pools) # Locally set the pool classes and keys so other PoolManagers can # override them. From 6bdb46314788f62fa91dd88d87371bb76d8ef99c Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 12 Apr 2023 15:40:24 +0100 Subject: [PATCH 07/36] =?UTF-8?q?Update=20diskcache=205.4.0=20(1cb1425)=20?= =?UTF-8?q?=E2=86=92=205.6.1=20(4d30686).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/diskcache/__init__.py | 6 +-- lib/diskcache/core.py | 17 ++++--- lib/diskcache/djangocache.py | 5 +- lib/diskcache/fanout.py | 34 ++++++++++--- lib/diskcache/persistent.py | 95 ++++++++++++++++++++++++++++-------- lib/diskcache/recipes.py | 18 +++++++ 7 files changed, 136 insertions(+), 40 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 995ec640..ff81086c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.29.0 (2023-xx-xx xx:xx:00 UTC) +* Update diskcache 5.4.0 (1cb1425) to 5.6.1 (4d30686) * Update filelock 3.9.0 (ce3e891) to 3.11.0 (d3241b9) * Update Msgpack 1.0.4 (b5acfd5) to 1.0.5 (0516c2c) * Update Requests library 2.28.1 (ec553c2) to 2.29.0 (87d63de) diff --git a/lib/diskcache/__init__.py b/lib/diskcache/__init__.py index 2355128c..1931a0dd 100644 --- a/lib/diskcache/__init__.py +++ b/lib/diskcache/__init__.py @@ -61,8 +61,8 @@ except Exception: # pylint: disable=broad-except # pragma: no cover pass __title__ = 'diskcache' -__version__ = '5.4.0' -__build__ = 0x050400 +__version__ = '5.6.1' +__build__ = 0x050601 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' -__copyright__ = 'Copyright 2016-2022 Grant Jenks' +__copyright__ = 'Copyright 2016-2023 Grant Jenks' diff --git a/lib/diskcache/core.py b/lib/diskcache/core.py index fadd8490..85f2e72e 100644 --- a/lib/diskcache/core.py +++ b/lib/diskcache/core.py @@ -50,14 +50,14 @@ DEFAULT_SETTINGS = { 'statistics': 0, # False 'tag_index': 0, # False 'eviction_policy': 'least-recently-stored', - 'size_limit': 2 ** 30, # 1gb + 'size_limit': 2**30, # 1gb 'cull_limit': 10, 'sqlite_auto_vacuum': 1, # FULL - 'sqlite_cache_size': 2 ** 13, # 8,192 pages + 'sqlite_cache_size': 2**13, # 8,192 pages 'sqlite_journal_mode': 'wal', - 'sqlite_mmap_size': 2 ** 26, # 64mb + 'sqlite_mmap_size': 2**26, # 64mb 'sqlite_synchronous': 1, # NORMAL - 'disk_min_file_size': 2 ** 15, # 32kb + 'disk_min_file_size': 2**15, # 32kb 'disk_pickle_protocol': pickle.HIGHEST_PROTOCOL, } @@ -171,7 +171,7 @@ class Disk: :return: corresponding Python key """ - # pylint: disable=no-self-use,unidiomatic-typecheck + # pylint: disable=unidiomatic-typecheck if raw: return bytes(key) if type(key) is sqlite3.Binary else key else: @@ -213,7 +213,7 @@ class Disk: size = op.getsize(full_path) return size, MODE_TEXT, filename, None elif read: - reader = ft.partial(value.read, 2 ** 22) + reader = ft.partial(value.read, 2**22) filename, full_path = self.filename(key, value) iterator = iter(reader, b'') size = self._write(full_path, iterator, 'xb') @@ -229,7 +229,6 @@ class Disk: return len(result), MODE_PICKLE, filename, None def _write(self, full_path, iterator, mode, encoding=None): - # pylint: disable=no-self-use full_dir, _ = op.split(full_path) for count in range(1, 11): @@ -265,7 +264,7 @@ class Disk: :raises: IOError if the value cannot be read """ - # pylint: disable=no-self-use,unidiomatic-typecheck,consider-using-with + # pylint: disable=unidiomatic-typecheck,consider-using-with if mode == MODE_RAW: return bytes(value) if type(value) is sqlite3.Binary else value elif mode == MODE_BINARY: @@ -435,6 +434,7 @@ class Cache: if directory is None: directory = tempfile.mkdtemp(prefix='diskcache-') + directory = str(directory) directory = op.expanduser(directory) directory = op.expandvars(directory) @@ -1380,6 +1380,7 @@ class Cache: :raises Timeout: if database timeout occurs """ + # pylint: disable=unnecessary-dunder-call try: return self.__delitem__(key, retry=retry) except KeyError: diff --git a/lib/diskcache/djangocache.py b/lib/diskcache/djangocache.py index 8bf85ce6..5dc8ce2c 100644 --- a/lib/diskcache/djangocache.py +++ b/lib/diskcache/djangocache.py @@ -44,14 +44,15 @@ class DjangoCache(BaseCache): """ return self._cache.cache(name) - def deque(self, name): + def deque(self, name, maxlen=None): """Return Deque with given `name` in subdirectory. :param str name: subdirectory name for Deque + :param maxlen: max length (default None, no max) :return: Deque with given name """ - return self._cache.deque(name) + return self._cache.deque(name, maxlen=maxlen) def index(self, name): """Return Index with given `name` in subdirectory. diff --git a/lib/diskcache/fanout.py b/lib/diskcache/fanout.py index dc5240c5..9822ee4e 100644 --- a/lib/diskcache/fanout.py +++ b/lib/diskcache/fanout.py @@ -30,6 +30,7 @@ class FanoutCache: """ if directory is None: directory = tempfile.mkdtemp(prefix='diskcache-') + directory = str(directory) directory = op.expanduser(directory) directory = op.expandvars(directory) @@ -45,7 +46,7 @@ class FanoutCache: timeout=timeout, disk=disk, size_limit=size_limit, - **settings + **settings, ) for num in range(shards) ) @@ -573,9 +574,11 @@ class FanoutCache: break return result - def cache(self, name): + def cache(self, name, timeout=60, disk=None, **settings): """Return Cache with given `name` in subdirectory. + If disk is none (default), uses the fanout cache disk. + >>> fanout_cache = FanoutCache() >>> cache = fanout_cache.cache('test') >>> cache.set('abc', 123) @@ -588,6 +591,9 @@ class FanoutCache: True :param str name: subdirectory name for Cache + :param float timeout: SQLite connection timeout + :param disk: Disk type or subclass for serialization + :param settings: any of DEFAULT_SETTINGS :return: Cache with given name """ @@ -598,11 +604,16 @@ class FanoutCache: except KeyError: parts = name.split('/') directory = op.join(self._directory, 'cache', *parts) - temp = Cache(directory=directory, disk=self._disk) + temp = Cache( + directory=directory, + timeout=timeout, + disk=self._disk if disk is None else Disk, + **settings, + ) _caches[name] = temp return temp - def deque(self, name): + def deque(self, name, maxlen=None): """Return Deque with given `name` in subdirectory. >>> cache = FanoutCache() @@ -616,6 +627,7 @@ class FanoutCache: 1 :param str name: subdirectory name for Deque + :param maxlen: max length (default None, no max) :return: Deque with given name """ @@ -626,8 +638,12 @@ class FanoutCache: except KeyError: parts = name.split('/') directory = op.join(self._directory, 'deque', *parts) - cache = Cache(directory=directory, disk=self._disk) - deque = Deque.fromcache(cache) + cache = Cache( + directory=directory, + disk=self._disk, + eviction_policy='none', + ) + deque = Deque.fromcache(cache, maxlen=maxlen) _deques[name] = deque return deque @@ -658,7 +674,11 @@ class FanoutCache: except KeyError: parts = name.split('/') directory = op.join(self._directory, 'index', *parts) - cache = Cache(directory=directory, disk=self._disk) + cache = Cache( + directory=directory, + disk=self._disk, + eviction_policy='none', + ) index = Index.fromcache(cache) _indexes[name] = index return index diff --git a/lib/diskcache/persistent.py b/lib/diskcache/persistent.py index ed23398d..07507c6f 100644 --- a/lib/diskcache/persistent.py +++ b/lib/diskcache/persistent.py @@ -76,7 +76,7 @@ class Deque(Sequence): """ - def __init__(self, iterable=(), directory=None): + def __init__(self, iterable=(), directory=None, maxlen=None): """Initialize deque instance. If directory is None then temporary directory created. The directory @@ -87,10 +87,11 @@ class Deque(Sequence): """ self._cache = Cache(directory, eviction_policy='none') - self.extend(iterable) + self._maxlen = float('inf') if maxlen is None else maxlen + self._extend(iterable) @classmethod - def fromcache(cls, cache, iterable=()): + def fromcache(cls, cache, iterable=(), maxlen=None): """Initialize deque using `cache`. >>> cache = Cache() @@ -112,7 +113,8 @@ class Deque(Sequence): # pylint: disable=no-member,protected-access self = cls.__new__(cls) self._cache = cache - self.extend(iterable) + self._maxlen = float('inf') if maxlen is None else maxlen + self._extend(iterable) return self @property @@ -125,6 +127,31 @@ class Deque(Sequence): """Directory path where deque is stored.""" return self._cache.directory + @property + def maxlen(self): + """Max length of the deque.""" + return self._maxlen + + @maxlen.setter + def maxlen(self, value): + """Set max length of the deque. + + Pops items from left while length greater than max. + + >>> deque = Deque() + >>> deque.extendleft('abcde') + >>> deque.maxlen = 3 + >>> list(deque) + ['c', 'd', 'e'] + + :param value: max length + + """ + self._maxlen = value + with self._cache.transact(retry=True): + while len(self._cache) > self._maxlen: + self._popleft() + def _index(self, index, func): len_self = len(self) @@ -245,7 +272,7 @@ class Deque(Sequence): :return: deque with added items """ - self.extend(iterable) + self._extend(iterable) return self def __iter__(self): @@ -293,10 +320,11 @@ class Deque(Sequence): pass def __getstate__(self): - return self.directory + return self.directory, self.maxlen def __setstate__(self, state): - self.__init__(directory=state) + directory, maxlen = state + self.__init__(directory=directory, maxlen=maxlen) def append(self, value): """Add `value` to back of deque. @@ -311,7 +339,12 @@ class Deque(Sequence): :param value: value to add to back of deque """ - self._cache.push(value, retry=True) + with self._cache.transact(retry=True): + self._cache.push(value, retry=True) + if len(self._cache) > self._maxlen: + self._popleft() + + _append = append def appendleft(self, value): """Add `value` to front of deque. @@ -326,7 +359,12 @@ class Deque(Sequence): :param value: value to add to front of deque """ - self._cache.push(value, side='front', retry=True) + with self._cache.transact(retry=True): + self._cache.push(value, side='front', retry=True) + if len(self._cache) > self._maxlen: + self._pop() + + _appendleft = appendleft def clear(self): """Remove all elements from deque. @@ -341,6 +379,13 @@ class Deque(Sequence): """ self._cache.clear(retry=True) + _clear = clear + + def copy(self): + """Copy deque with same directory and max length.""" + TypeSelf = type(self) + return TypeSelf(directory=self.directory, maxlen=self.maxlen) + def count(self, value): """Return number of occurrences of `value` in deque. @@ -366,7 +411,9 @@ class Deque(Sequence): """ for value in iterable: - self.append(value) + self._append(value) + + _extend = extend def extendleft(self, iterable): """Extend front side of deque with value from `iterable`. @@ -380,7 +427,7 @@ class Deque(Sequence): """ for value in iterable: - self.appendleft(value) + self._appendleft(value) def peek(self): """Peek at value at back of deque. @@ -460,6 +507,8 @@ class Deque(Sequence): raise IndexError('pop from an empty deque') return value + _pop = pop + def popleft(self): """Remove and return value at front of deque. @@ -484,6 +533,8 @@ class Deque(Sequence): raise IndexError('pop from an empty deque') return value + _popleft = popleft + def remove(self, value): """Remove first occurrence of `value` in deque. @@ -531,15 +582,17 @@ class Deque(Sequence): ['c', 'b', 'a'] """ + # pylint: disable=protected-access # GrantJ 2019-03-22 Consider using an algorithm that swaps the values # at two keys. Like self._cache.swap(key1, key2, retry=True) The swap # method would exchange the values at two given keys. Then, using a - # forward iterator and a reverse iterator, the reversis method could + # forward iterator and a reverse iterator, the reverse method could # avoid making copies of the values. temp = Deque(iterable=reversed(self)) - self.clear() - self.extend(temp) + self._clear() + self._extend(temp) directory = temp.directory + temp._cache.close() del temp rmtree(directory) @@ -574,22 +627,22 @@ class Deque(Sequence): for _ in range(steps): try: - value = self.pop() + value = self._pop() except IndexError: return else: - self.appendleft(value) + self._appendleft(value) else: steps *= -1 steps %= len_self for _ in range(steps): try: - value = self.popleft() + value = self._popleft() except IndexError: return else: - self.append(value) + self._append(value) __hash__ = None # type: ignore @@ -668,7 +721,9 @@ class Index(MutableMapping): args = args[1:] directory = None self._cache = Cache(directory, eviction_policy='none') - self.update(*args, **kwargs) + self._update(*args, **kwargs) + + _update = MutableMapping.update @classmethod def fromcache(cls, cache, *args, **kwargs): @@ -694,7 +749,7 @@ class Index(MutableMapping): # pylint: disable=no-member,protected-access self = cls.__new__(cls) self._cache = cache - self.update(*args, **kwargs) + self._update(*args, **kwargs) return self @property diff --git a/lib/diskcache/recipes.py b/lib/diskcache/recipes.py index b5af6dd7..babb68f6 100644 --- a/lib/diskcache/recipes.py +++ b/lib/diskcache/recipes.py @@ -17,6 +17,9 @@ class Averager: Sometimes known as "online statistics," the running average maintains the total and count. The average can then be calculated at any time. + Assumes the key will not be evicted. Set the eviction policy to 'none' on + the cache to guarantee the key is not evicted. + >>> import diskcache >>> cache = diskcache.FanoutCache() >>> ave = Averager(cache, 'latency') @@ -65,6 +68,9 @@ class Averager: class Lock: """Recipe for cross-process and cross-thread lock. + Assumes the key will not be evicted. Set the eviction policy to 'none' on + the cache to guarantee the key is not evicted. + >>> import diskcache >>> cache = diskcache.Cache() >>> lock = Lock(cache, 'report-123') @@ -113,6 +119,9 @@ class Lock: class RLock: """Recipe for cross-process and cross-thread re-entrant lock. + Assumes the key will not be evicted. Set the eviction policy to 'none' on + the cache to guarantee the key is not evicted. + >>> import diskcache >>> cache = diskcache.Cache() >>> rlock = RLock(cache, 'user-123') @@ -181,6 +190,9 @@ class RLock: class BoundedSemaphore: """Recipe for cross-process and cross-thread bounded semaphore. + Assumes the key will not be evicted. Set the eviction policy to 'none' on + the cache to guarantee the key is not evicted. + >>> import diskcache >>> cache = diskcache.Cache() >>> semaphore = BoundedSemaphore(cache, 'max-cons', value=2) @@ -251,6 +263,9 @@ def throttle( ): """Decorator to throttle calls to function. + Assumes keys will not be evicted. Set the eviction policy to 'none' on the + cache to guarantee the keys are not evicted. + >>> import diskcache, time >>> cache = diskcache.Cache() >>> count = 0 @@ -305,6 +320,9 @@ def barrier(cache, lock_factory, name=None, expire=None, tag=None): Supports different kinds of locks: Lock, RLock, BoundedSemaphore. + Assumes keys will not be evicted. Set the eviction policy to 'none' on the + cache to guarantee the keys are not evicted. + >>> import diskcache, time >>> cache = diskcache.Cache() >>> @barrier(cache, Lock) From ef2d45c4cad2d77ea3b5dd47dbe98c328972dfb1 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Wed, 12 Apr 2023 21:50:20 +0100 Subject: [PATCH 08/36] =?UTF-8?q?Update=20attr=2022.2.0=20(a9960de)=20?= =?UTF-8?q?=E2=86=92=2022.2.0=20(683d056).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/attr/__init__.py | 91 +++++++++++++++++------- lib/attr/__init__.pyi | 68 +++++++++++++++++- lib/attr/_cmp.py | 28 ++++---- lib/attr/_compat.py | 47 ++++++++----- lib/attr/_funcs.py | 123 +++++++++++++++++++++++--------- lib/attr/_make.py | 150 +++++++++++++++++++++++----------------- lib/attr/_next_gen.py | 8 ++- lib/attr/exceptions.py | 15 ++-- lib/attr/validators.py | 30 ++++---- lib/attr/validators.pyi | 6 +- 11 files changed, 386 insertions(+), 181 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ff81086c..9efccead 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.29.0 (2023-xx-xx xx:xx:00 UTC) +* Update attr 22.2.0 (a9960de) to 22.2.0 (683d056) * Update diskcache 5.4.0 (1cb1425) to 5.6.1 (4d30686) * Update filelock 3.9.0 (ce3e891) to 3.11.0 (d3241b9) * Update Msgpack 1.0.4 (b5acfd5) to 1.0.5 (0516c2c) diff --git a/lib/attr/__init__.py b/lib/attr/__init__.py index 04243782..7cfa792f 100644 --- a/lib/attr/__init__.py +++ b/lib/attr/__init__.py @@ -1,9 +1,11 @@ # SPDX-License-Identifier: MIT -import sys -import warnings +""" +Classes Without Boilerplate +""" from functools import partial +from typing import Callable from . import converters, exceptions, filters, setters, validators from ._cmp import cmp_using @@ -24,30 +26,6 @@ from ._next_gen import define, field, frozen, mutable from ._version_info import VersionInfo -if sys.version_info < (3, 7): # pragma: no cover - warnings.warn( - "Running attrs on Python 3.6 is deprecated & we intend to drop " - "support soon. If that's a problem for you, please let us know why & " - "we MAY re-evaluate: ", - DeprecationWarning, - ) - -__version__ = "22.2.0" -__version_info__ = VersionInfo._from_version_string(__version__) - -__title__ = "attrs" -__description__ = "Classes Without Boilerplate" -__url__ = "https://www.attrs.org/" -__uri__ = __url__ -__doc__ = __description__ + " <" + __uri__ + ">" - -__author__ = "Hynek Schlawack" -__email__ = "hs@ox.cx" - -__license__ = "MIT" -__copyright__ = "Copyright (c) 2015 Hynek Schlawack" - - s = attributes = attrs ib = attr = attrib dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) @@ -91,3 +69,64 @@ __all__ = [ "validate", "validators", ] + + +def _make_getattr(mod_name: str) -> Callable: + """ + Create a metadata proxy for packaging information that uses *mod_name* in + its warnings and errors. + """ + + def __getattr__(name: str) -> str: + dunder_to_metadata = { + "__title__": "Name", + "__copyright__": "", + "__version__": "version", + "__version_info__": "version", + "__description__": "summary", + "__uri__": "", + "__url__": "", + "__author__": "", + "__email__": "", + "__license__": "license", + } + if name not in dunder_to_metadata.keys(): + raise AttributeError(f"module {mod_name} has no attribute {name}") + + import sys + import warnings + + if sys.version_info < (3, 8): + from importlib_metadata import metadata + else: + from importlib.metadata import metadata + + if name != "__version_info__": + warnings.warn( + f"Accessing {mod_name}.{name} is deprecated and will be " + "removed in a future release. Use importlib.metadata directly " + "to query for attrs's packaging metadata.", + DeprecationWarning, + stacklevel=2, + ) + + meta = metadata("attrs") + if name == "__license__": + return "MIT" + elif name == "__copyright__": + return "Copyright (c) 2015 Hynek Schlawack" + elif name in ("__uri__", "__url__"): + return meta["Project-URL"].split(" ", 1)[-1] + elif name == "__version_info__": + return VersionInfo._from_version_string(meta["version"]) + elif name == "__author__": + return meta["Author-email"].rsplit(" ", 1)[0] + elif name == "__email__": + return meta["Author-email"].rsplit("<", 1)[1][:-1] + + return meta[dunder_to_metadata[name]] + + return __getattr__ + + +__getattr__ = _make_getattr(__name__) diff --git a/lib/attr/__init__.pyi b/lib/attr/__init__.pyi index 42a2ee2c..ced5a3fd 100644 --- a/lib/attr/__init__.pyi +++ b/lib/attr/__init__.pyi @@ -69,6 +69,7 @@ _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] class AttrsInstance(AttrsInstance_, Protocol): pass +_A = TypeVar("_A", bound=AttrsInstance) # _make -- class _Nothing(enum.Enum): @@ -116,6 +117,7 @@ def __dataclass_transform__( eq_default: bool = True, order_default: bool = False, kw_only_default: bool = False, + frozen_default: bool = False, field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), ) -> Callable[[_T], _T]: ... @@ -257,6 +259,7 @@ def field( order: Optional[bool] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the @@ -277,6 +280,7 @@ def field( order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> _T: ... # This form catches an explicit default argument. @@ -296,6 +300,7 @@ def field( order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> _T: ... # This form covers type=non-Type: e.g. forward references (str), Any @@ -315,6 +320,7 @@ def field( order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> Any: ... @overload @__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) @@ -426,17 +432,73 @@ def define( ) -> Callable[[_C], _C]: ... mutable = define -frozen = define # they differ only in their defaults +@overload +@__dataclass_transform__( + frozen_default=True, field_descriptors=(attrib, field) +) +def frozen( + maybe_cls: _C, + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + unsafe_hash: Optional[bool] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@__dataclass_transform__( + frozen_default=True, field_descriptors=(attrib, field) +) +def frozen( + maybe_cls: None = ..., + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + unsafe_hash: Optional[bool] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... def fields(cls: Type[AttrsInstance]) -> Any: ... def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ... def validate(inst: AttrsInstance) -> None: ... def resolve_types( - cls: _C, + cls: _A, globalns: Optional[Dict[str, Any]] = ..., localns: Optional[Dict[str, Any]] = ..., attribs: Optional[List[Attribute[Any]]] = ..., -) -> _C: ... + include_extras: bool = ..., +) -> _A: ... # TODO: add support for returning a proper attrs class from the mypy plugin # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', diff --git a/lib/attr/_cmp.py b/lib/attr/_cmp.py index ad1e18c7..d9cbe22c 100644 --- a/lib/attr/_cmp.py +++ b/lib/attr/_cmp.py @@ -20,22 +20,22 @@ def cmp_using( class_name="Comparable", ): """ - Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and - ``cmp`` arguments to customize field comparison. + Create a class that can be passed into `attrs.field`'s ``eq``, ``order``, + and ``cmp`` arguments to customize field comparison. - The resulting class will have a full set of ordering methods if - at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + The resulting class will have a full set of ordering methods if at least + one of ``{lt, le, gt, ge}`` and ``eq`` are provided. - :param Optional[callable] eq: `callable` used to evaluate equality - of two objects. - :param Optional[callable] lt: `callable` used to evaluate whether - one object is less than another object. - :param Optional[callable] le: `callable` used to evaluate whether - one object is less than or equal to another object. - :param Optional[callable] gt: `callable` used to evaluate whether - one object is greater than another object. - :param Optional[callable] ge: `callable` used to evaluate whether - one object is greater than or equal to another object. + :param Optional[callable] eq: `callable` used to evaluate equality of two + objects. + :param Optional[callable] lt: `callable` used to evaluate whether one + object is less than another object. + :param Optional[callable] le: `callable` used to evaluate whether one + object is less than or equal to another object. + :param Optional[callable] gt: `callable` used to evaluate whether one + object is greater than another object. + :param Optional[callable] ge: `callable` used to evaluate whether one + object is greater than or equal to another object. :param bool require_same_type: When `True`, equality and ordering methods will return `NotImplemented` if objects are not of the same type. diff --git a/lib/attr/_compat.py b/lib/attr/_compat.py index 35a85a3f..c3bf5e33 100644 --- a/lib/attr/_compat.py +++ b/lib/attr/_compat.py @@ -9,9 +9,11 @@ import types import warnings from collections.abc import Mapping, Sequence # noqa +from typing import _GenericAlias PYPY = platform.python_implementation() == "PyPy" +PY_3_9_PLUS = sys.version_info[:2] >= (3, 9) PY310 = sys.version_info[:2] >= (3, 10) PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) @@ -81,32 +83,32 @@ def make_set_closure_cell(): # Otherwise gotta do it the hard way. - # Create a function that will set its first cellvar to `value`. - def set_first_cellvar_to(value): - x = value - return - - # This function will be eliminated as dead code, but - # not before its reference to `x` forces `x` to be - # represented as a closure cell rather than a local. - def force_x_to_be_a_cell(): # pragma: no cover - return x - try: - # Extract the code object and make sure our assumptions about - # the closure behavior are correct. - co = set_first_cellvar_to.__code__ - if co.co_cellvars != ("x",) or co.co_freevars != (): - raise AssertionError # pragma: no cover - - # Convert this code object to a code object that sets the - # function's first _freevar_ (not cellvar) to the argument. if sys.version_info >= (3, 8): def set_closure_cell(cell, value): cell.cell_contents = value else: + # Create a function that will set its first cellvar to `value`. + def set_first_cellvar_to(value): + x = value + return + + # This function will be eliminated as dead code, but + # not before its reference to `x` forces `x` to be + # represented as a closure cell rather than a local. + def force_x_to_be_a_cell(): # pragma: no cover + return x + + # Extract the code object and make sure our assumptions about + # the closure behavior are correct. + co = set_first_cellvar_to.__code__ + if co.co_cellvars != ("x",) or co.co_freevars != (): + raise AssertionError # pragma: no cover + + # Convert this code object to a code object that sets the + # function's first _freevar_ (not cellvar) to the argument. args = [co.co_argcount] args.append(co.co_kwonlyargcount) args.extend( @@ -174,3 +176,10 @@ set_closure_cell = make_set_closure_cell() # don't have a direct reference to the thread-local in their globals dict. # If they have such a reference, it breaks cloudpickle. repr_context = threading.local() + + +def get_generic_base(cl): + """If this is a generic class (A[str]), return the generic base for it.""" + if cl.__class__ is _GenericAlias: + return cl.__origin__ + return None diff --git a/lib/attr/_funcs.py b/lib/attr/_funcs.py index 1f573c11..7f5d9610 100644 --- a/lib/attr/_funcs.py +++ b/lib/attr/_funcs.py @@ -3,6 +3,7 @@ import copy +from ._compat import PY_3_9_PLUS, get_generic_base from ._make import NOTHING, _obj_setattr, fields from .exceptions import AttrsAttributeNotFoundError @@ -16,13 +17,13 @@ def asdict( value_serializer=None, ): """ - Return the ``attrs`` attribute values of *inst* as a dict. + Return the *attrs* attribute values of *inst* as a dict. - Optionally recurse into other ``attrs``-decorated classes. + Optionally recurse into other *attrs*-decorated classes. - :param inst: Instance of an ``attrs``-decorated class. + :param inst: Instance of an *attrs*-decorated class. :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. + *attrs*-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the `attrs.Attribute` as the first argument and the @@ -40,7 +41,7 @@ def asdict( :rtype: return type of *dict_factory* - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 16.0.0 *dict_factory* @@ -195,13 +196,13 @@ def astuple( retain_collection_types=False, ): """ - Return the ``attrs`` attribute values of *inst* as a tuple. + Return the *attrs* attribute values of *inst* as a tuple. - Optionally recurse into other ``attrs``-decorated classes. + Optionally recurse into other *attrs*-decorated classes. - :param inst: Instance of an ``attrs``-decorated class. + :param inst: Instance of an *attrs*-decorated class. :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. + *attrs*-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the `attrs.Attribute` as the first argument and the @@ -215,7 +216,7 @@ def astuple( :rtype: return type of *tuple_factory* - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 16.2.0 @@ -289,28 +290,48 @@ def astuple( def has(cls): """ - Check whether *cls* is a class with ``attrs`` attributes. + Check whether *cls* is a class with *attrs* attributes. :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. :rtype: bool """ - return getattr(cls, "__attrs_attrs__", None) is not None + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is not None: + return True + + # No attrs, maybe it's a specialized generic (A[str])? + generic_base = get_generic_base(cls) + if generic_base is not None: + generic_attrs = getattr(generic_base, "__attrs_attrs__", None) + if generic_attrs is not None: + # Stick it on here for speed next time. + cls.__attrs_attrs__ = generic_attrs + return generic_attrs is not None + return False def assoc(inst, **changes): """ Copy *inst* and apply *changes*. - :param inst: Instance of a class with ``attrs`` attributes. + This is different from `evolve` that applies the changes to the arguments + that create the new instance. + + `evolve`'s behavior is preferable, but there are `edge cases`_ where it + doesn't work. Therefore `assoc` is deprecated, but will not be removed. + + .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 + + :param inst: Instance of a class with *attrs* attributes. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. - :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't - be found on *cls*. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.AttrsAttributeNotFoundError: If *attr_name* + couldn't be found on *cls*. + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. deprecated:: 17.1.0 @@ -318,13 +339,6 @@ def assoc(inst, **changes): This function will not be removed du to the slightly different approach compared to `attrs.evolve`. """ - import warnings - - warnings.warn( - "assoc is deprecated and will be removed after 2018/01.", - DeprecationWarning, - stacklevel=2, - ) new = copy.copy(inst) attrs = fields(inst.__class__) for k, v in changes.items(): @@ -337,22 +351,55 @@ def assoc(inst, **changes): return new -def evolve(inst, **changes): +def evolve(*args, **changes): """ - Create a new instance, based on *inst* with *changes* applied. + Create a new instance, based on the first positional argument with + *changes* applied. - :param inst: Instance of a class with ``attrs`` attributes. + :param inst: Instance of a class with *attrs* attributes. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. :raise TypeError: If *attr_name* couldn't be found in the class ``__init__``. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. - .. versionadded:: 17.1.0 + .. versionadded:: 17.1.0 + .. deprecated:: 23.1.0 + It is now deprecated to pass the instance using the keyword argument + *inst*. It will raise a warning until at least April 2024, after which + it will become an error. Always pass the instance as a positional + argument. """ + # Try to get instance by positional argument first. + # Use changes otherwise and warn it'll break. + if args: + try: + (inst,) = args + except ValueError: + raise TypeError( + f"evolve() takes 1 positional argument, but {len(args)} " + "were given" + ) from None + else: + try: + inst = changes.pop("inst") + except KeyError: + raise TypeError( + "evolve() missing 1 required positional argument: 'inst'" + ) from None + + import warnings + + warnings.warn( + "Passing the instance per keyword argument is deprecated and " + "will stop working in, or after, April 2024.", + DeprecationWarning, + stacklevel=2, + ) + cls = inst.__class__ attrs = fields(cls) for a in attrs: @@ -366,7 +413,9 @@ def evolve(inst, **changes): return cls(**changes) -def resolve_types(cls, globalns=None, localns=None, attribs=None): +def resolve_types( + cls, globalns=None, localns=None, attribs=None, include_extras=True +): """ Resolve any strings and forward annotations in type annotations. @@ -385,10 +434,14 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): :param Optional[dict] localns: Dictionary containing local variables. :param Optional[list] attribs: List of attribs for the given class. This is necessary when calling from inside a ``field_transformer`` - since *cls* is not an ``attrs`` class yet. + since *cls* is not an *attrs* class yet. + :param bool include_extras: Resolve more accurately, if possible. + Pass ``include_extras`` to ``typing.get_hints``, if supported by the + typing module. On supported Python versions (3.9+), this resolves the + types more accurately. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class and you didn't pass any attribs. :raise NameError: If types cannot be resolved because of missing variables. @@ -398,6 +451,7 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): .. versionadded:: 20.1.0 .. versionadded:: 21.1.0 *attribs* + .. versionadded:: 23.1.0 *include_extras* """ # Since calling get_type_hints is expensive we cache whether we've @@ -405,7 +459,12 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): if getattr(cls, "__attrs_types_resolved__", None) != cls: import typing - hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) + kwargs = {"globalns": globalns, "localns": localns} + + if PY_3_9_PLUS: + kwargs["include_extras"] = include_extras + + hints = typing.get_type_hints(cls, **kwargs) for field in fields(cls) if attribs is None else attribs: if field.name in hints: # Since fields have been frozen we must work around it. diff --git a/lib/attr/_make.py b/lib/attr/_make.py index 9ee22005..d72f738e 100644 --- a/lib/attr/_make.py +++ b/lib/attr/_make.py @@ -12,7 +12,12 @@ from operator import itemgetter # We need to import _compat itself in addition to the _compat members to avoid # having the thread-local in the globals here. from . import _compat, _config, setters -from ._compat import PY310, PYPY, _AnnotationExtractor, set_closure_cell +from ._compat import ( + PY310, + _AnnotationExtractor, + get_generic_base, + set_closure_cell, +) from .exceptions import ( DefaultAlreadySetError, FrozenInstanceError, @@ -109,9 +114,12 @@ def attrib( .. warning:: Does *not* do anything unless the class is also decorated with - `attr.s`! + `attr.s` / `attrs.define` / et cetera! - :param default: A value that is used if an ``attrs``-generated ``__init__`` + Please consider using `attrs.field` in new code (``attr.ib`` will *never* + go away, though). + + :param default: A value that is used if an *attrs*-generated ``__init__`` is used and no value is passed while instantiating or the attribute is excluded using ``init=False``. @@ -130,7 +138,7 @@ def attrib( :param callable factory: Syntactic sugar for ``default=attr.Factory(factory)``. - :param validator: `callable` that is called by ``attrs``-generated + :param validator: `callable` that is called by *attrs*-generated ``__init__`` methods after the instance has been initialized. They receive the initialized instance, the :func:`~attrs.Attribute`, and the passed value. @@ -142,7 +150,7 @@ def attrib( all pass. Validators can be globally disabled and re-enabled using - `get_run_validators`. + `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. The validator can also be set using decorator notation as shown below. @@ -184,7 +192,7 @@ def attrib( value. In that case this attributed is unconditionally initialized with the specified default value or factory. :param callable converter: `callable` that is called by - ``attrs``-generated ``__init__`` methods to convert attribute's value + *attrs*-generated ``__init__`` methods to convert attribute's value to the desired format. It is given the passed-in value, and the returned value will be used as the new value of the attribute. The value is converted before being passed to the validator, if any. @@ -197,7 +205,7 @@ def attrib( Regardless of the approach used, the type will be stored on ``Attribute.type``. - Please note that ``attrs`` doesn't do anything with this metadata by + Please note that *attrs* doesn't do anything with this metadata by itself. You can use it as part of your own code or for `static type checking `. :param kw_only: Make this attribute keyword-only in the generated @@ -582,28 +590,19 @@ def _transform_attrs( return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) -if PYPY: +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + "__traceback__", + ): + BaseException.__setattr__(self, name, value) + return - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - if isinstance(self, BaseException) and name in ( - "__cause__", - "__context__", - ): - BaseException.__setattr__(self, name, value) - return - - raise FrozenInstanceError() - -else: - - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - raise FrozenInstanceError() + raise FrozenInstanceError() def _frozen_delattrs(self, name): @@ -940,9 +939,15 @@ class _ClassBuilder: Automatically created by attrs. """ __bound_setattr = _obj_setattr.__get__(self) - for name in state_attr_names: - if name in state: - __bound_setattr(name, state[name]) + if isinstance(state, tuple): + # Backward compatibility with attrs instances pickled with + # attrs versions before v22.2.0 which stored tuples. + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + else: + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) # The hash code cache is not included when the object is # serialized, but it still needs to be initialized to None to @@ -1220,12 +1225,15 @@ def attrs( A class decorator that adds :term:`dunder methods` according to the specified attributes using `attr.ib` or the *these* argument. + Please consider using `attrs.define` / `attrs.frozen` in new code + (``attr.s`` will *never* go away, though). + :param these: A dictionary of name to `attr.ib` mappings. This is useful to avoid the definition of your attributes within the class body because you can't (e.g. if you want to add ``__repr__`` methods to Django models) or don't want to. - If *these* is not ``None``, ``attrs`` will *not* search the class body + If *these* is not ``None``, *attrs* will *not* search the class body for attributes and will *not* remove any attributes from it. The order is deduced from the order of the attributes inside *these*. @@ -1242,14 +1250,14 @@ def attrs( inherited from some base class). So for example by implementing ``__eq__`` on a class yourself, - ``attrs`` will deduce ``eq=False`` and will create *neither* + *attrs* will deduce ``eq=False`` and will create *neither* ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible ``__ne__`` by default, so it *should* be enough to only implement ``__eq__`` in most cases). .. warning:: - If you prevent ``attrs`` from creating the ordering methods for you + If you prevent *attrs* from creating the ordering methods for you (``order=False``, e.g. by implementing ``__le__``), it becomes *your* responsibility to make sure its ordering is sound. The best way is to use the `functools.total_ordering` decorator. @@ -1259,14 +1267,14 @@ def attrs( *cmp*, or *hash* overrides whatever *auto_detect* would determine. :param bool repr: Create a ``__repr__`` method with a human readable - representation of ``attrs`` attributes.. + representation of *attrs* attributes.. :param bool str: Create a ``__str__`` method that is identical to ``__repr__``. This is usually not necessary except for `Exception`\ s. :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` and ``__ne__`` methods that check two instances for equality. - They compare the instances as if they were tuples of their ``attrs`` + They compare the instances as if they were tuples of their *attrs* attributes if and only if the types of both classes are *identical*! :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` methods that behave like *eq* above and @@ -1277,7 +1285,7 @@ def attrs( :param Optional[bool] unsafe_hash: If ``None`` (default), the ``__hash__`` method is generated according how *eq* and *frozen* are set. - 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. + 1. If *both* are True, *attrs* will generate a ``__hash__`` for you. 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to None, marking it unhashable (which it is). 3. If *eq* is False, ``__hash__`` will be left untouched meaning the @@ -1285,7 +1293,7 @@ def attrs( ``object``, this means it will fall back to id-based hashing.). Although not recommended, you can decide for yourself and force - ``attrs`` to create one (e.g. if the class is immutable even though you + *attrs* to create one (e.g. if the class is immutable even though you didn't freeze it programmatically) by passing ``True`` or not. Both of these cases are rather special and should be used carefully. @@ -1296,7 +1304,7 @@ def attrs( :param Optional[bool] hash: Alias for *unsafe_hash*. *unsafe_hash* takes precedence. :param bool init: Create a ``__init__`` method that initializes the - ``attrs`` attributes. Leading underscores are stripped for the argument + *attrs* attributes. Leading underscores are stripped for the argument name. If a ``__attrs_pre_init__`` method exists on the class, it will be called before the class is initialized. If a ``__attrs_post_init__`` method exists on the class, it will be called after the class is fully @@ -1312,7 +1320,7 @@ def attrs( we encourage you to read the :term:`glossary entry `. :param bool frozen: Make instances immutable after initialization. If someone attempts to modify a frozen instance, - `attr.exceptions.FrozenInstanceError` is raised. + `attrs.exceptions.FrozenInstanceError` is raised. .. note:: @@ -1337,7 +1345,7 @@ def attrs( :param bool auto_attribs: If ``True``, collect :pep:`526`-annotated attributes from the class body. - In this case, you **must** annotate every field. If ``attrs`` + In this case, you **must** annotate every field. If *attrs* encounters a field that is set to an `attr.ib` but lacks a type annotation, an `attr.exceptions.UnannotatedAttributeError` is raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't @@ -1353,9 +1361,9 @@ def attrs( .. warning:: For features that use the attribute name to create decorators (e.g. - `validators `), you still *must* assign `attr.ib` to - them. Otherwise Python will either not find the name or try to use - the default value to call e.g. ``validator`` on it. + :ref:`validators `), you still *must* assign `attr.ib` + to them. Otherwise Python will either not find the name or try to + use the default value to call e.g. ``validator`` on it. These errors can be quite confusing and probably the most common bug report on our bug tracker. @@ -1376,14 +1384,14 @@ def attrs( class: - the values for *eq*, *order*, and *hash* are ignored and the - instances compare and hash by the instance's ids (N.B. ``attrs`` will + instances compare and hash by the instance's ids (N.B. *attrs* will *not* remove existing implementations of ``__hash__`` or the equality methods. It just won't add own ones.), - all attributes that are either passed into ``__init__`` or have a default value are additionally available as a tuple in the ``args`` attribute, - the value of *str* is ignored leaving ``__str__`` to base classes. - :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` + :param bool collect_by_mro: Setting this to `True` fixes the way *attrs* collects attributes from base classes. The default behavior is incorrect in certain cases of multiple inheritance. It should be on by default but is kept off for backward-compatibility. @@ -1422,7 +1430,7 @@ def attrs( :param Optional[callable] field_transformer: A function that is called with the original class object and all - fields right before ``attrs`` finalizes the class. You can use + fields right before *attrs* finalizes the class. You can use this, e.g., to automatically add converters or validators to fields based on their types. See `transform-fields` for more details. @@ -1900,7 +1908,7 @@ def _add_repr(cls, ns=None, attrs=None): def fields(cls): """ - Return the tuple of ``attrs`` attributes for a class. + Return the tuple of *attrs* attributes for a class. The tuple also allows accessing the fields by their names (see below for examples). @@ -1908,31 +1916,45 @@ def fields(cls): :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. :rtype: tuple (with name accessors) of `attrs.Attribute` - .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields - by name. + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + .. versionchanged:: 23.1.0 Add support for generic classes. """ - if not isinstance(cls, type): + generic_base = get_generic_base(cls) + + if generic_base is None and not isinstance(cls, type): raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + if generic_base is not None: + attrs = getattr(generic_base, "__attrs_attrs__", None) + if attrs is not None: + # Even though this is global state, stick it on here to speed + # it up. We rely on `cls` being cached for this to be + # efficient. + cls.__attrs_attrs__ = attrs + return attrs raise NotAnAttrsClassError(f"{cls!r} is not an attrs-decorated class.") + return attrs def fields_dict(cls): """ - Return an ordered dictionary of ``attrs`` attributes for a class, whose + Return an ordered dictionary of *attrs* attributes for a class, whose keys are the attribute names. :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. :rtype: dict @@ -1953,7 +1975,7 @@ def validate(inst): Leaves all exceptions through. - :param inst: Instance of a class with ``attrs`` attributes. + :param inst: Instance of a class with *attrs* attributes. """ if _config._run_validators is False: return @@ -2391,6 +2413,10 @@ class Attribute: """ *Read-only* representation of an attribute. + .. warning:: + + You should never instantiate this class yourself. + The class has *all* arguments of `attr.ib` (except for ``factory`` which is only syntactic sugar for ``default=Factory(...)`` plus the following: @@ -2536,13 +2562,13 @@ class Attribute: **inst_dict, ) - # Don't use attr.evolve since fields(Attribute) doesn't work + # Don't use attrs.evolve since fields(Attribute) doesn't work def evolve(self, **changes): """ Copy *self* and apply *changes*. - This works similarly to `attr.evolve` but that function does not work - with ``Attribute``. + This works similarly to `attrs.evolve` but that function does not work + with `Attribute`. It is mainly meant to be used for `transform-fields`. @@ -2777,10 +2803,6 @@ class Factory: __slots__ = ("factory", "takes_self") def __init__(self, factory, takes_self=False): - """ - `Factory` is part of the default machinery so if we want a default - value here, we have to implement it ourselves. - """ self.factory = factory self.takes_self = takes_self @@ -2818,13 +2840,13 @@ Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) def make_class(name, attrs, bases=(object,), **attributes_arguments): - """ + r""" A quick way to create a new class called *name* with *attrs*. :param str name: The name for the new class. :param attrs: A list of names or a dictionary of mappings of names to - attributes. + `attr.ib`\ s / `attrs.field`\ s. The order is deduced from the order of the names or attributes inside *attrs*. Otherwise the order of the definition of the attributes is diff --git a/lib/attr/_next_gen.py b/lib/attr/_next_gen.py index c59d8486..7c4d5db0 100644 --- a/lib/attr/_next_gen.py +++ b/lib/attr/_next_gen.py @@ -46,7 +46,7 @@ def define( match_args=True, ): r""" - Define an ``attrs`` class. + Define an *attrs* class. Differences to the classic `attr.s` that it uses underneath: @@ -167,6 +167,7 @@ def field( hash=None, init=True, metadata=None, + type=None, converter=None, factory=None, kw_only=False, @@ -179,6 +180,10 @@ def field( Identical to `attr.ib`, except keyword-only and with some arguments removed. + .. versionadded:: 22.3.0 + The *type* parameter has been re-added; mostly for + {func}`attrs.make_class`. Please note that type checkers ignore this + metadata. .. versionadded:: 20.1.0 """ return attrib( @@ -188,6 +193,7 @@ def field( hash=hash, init=init, metadata=metadata, + type=type, converter=converter, factory=factory, kw_only=kw_only, diff --git a/lib/attr/exceptions.py b/lib/attr/exceptions.py index 5dc51e0a..28834930 100644 --- a/lib/attr/exceptions.py +++ b/lib/attr/exceptions.py @@ -34,7 +34,7 @@ class FrozenAttributeError(FrozenError): class AttrsAttributeNotFoundError(ValueError): """ - An ``attrs`` function couldn't find an attribute that the user asked for. + An *attrs* function couldn't find an attribute that the user asked for. .. versionadded:: 16.2.0 """ @@ -42,7 +42,7 @@ class AttrsAttributeNotFoundError(ValueError): class NotAnAttrsClassError(ValueError): """ - A non-``attrs`` class has been passed into an ``attrs`` function. + A non-*attrs* class has been passed into an *attrs* function. .. versionadded:: 16.2.0 """ @@ -50,7 +50,7 @@ class NotAnAttrsClassError(ValueError): class DefaultAlreadySetError(RuntimeError): """ - A default has been set using ``attr.ib()`` and is attempted to be reset + A default has been set when defining the field and is attempted to be reset using the decorator. .. versionadded:: 17.1.0 @@ -59,8 +59,7 @@ class DefaultAlreadySetError(RuntimeError): class UnannotatedAttributeError(RuntimeError): """ - A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type - annotation. + A class with ``auto_attribs=True`` has a field without a type annotation. .. versionadded:: 17.3.0 """ @@ -68,7 +67,7 @@ class UnannotatedAttributeError(RuntimeError): class PythonTooOldError(RuntimeError): """ - It was attempted to use an ``attrs`` feature that requires a newer Python + It was attempted to use an *attrs* feature that requires a newer Python version. .. versionadded:: 18.2.0 @@ -77,8 +76,8 @@ class PythonTooOldError(RuntimeError): class NotCallableError(TypeError): """ - A ``attr.ib()`` requiring a callable has been set with a value - that is not callable. + A field requiring a callable has been set with a value that is not + callable. .. versionadded:: 19.2.0 """ diff --git a/lib/attr/validators.py b/lib/attr/validators.py index 852ae965..1488554f 100644 --- a/lib/attr/validators.py +++ b/lib/attr/validators.py @@ -9,6 +9,7 @@ import operator import re from contextlib import contextmanager +from re import Pattern from ._config import get_run_validators, set_run_validators from ._make import _AndValidator, and_, attrib, attrs @@ -16,12 +17,6 @@ from .converters import default_if_none from .exceptions import NotCallableError -try: - Pattern = re.Pattern -except AttributeError: # Python <3.7 lacks a Pattern type. - Pattern = type(re.compile("")) - - __all__ = [ "and_", "deep_iterable", @@ -249,7 +244,17 @@ def provides(interface): :raises TypeError: With a human readable error message, the attribute (of type `attrs.Attribute`), the expected interface, and the value it got. + + .. deprecated:: 23.1.0 """ + import warnings + + warnings.warn( + "attrs's zope-interface support is deprecated and will be removed in, " + "or after, April 2024.", + DeprecationWarning, + stacklevel=2, + ) return _ProvidesValidator(interface) @@ -275,15 +280,16 @@ def optional(validator): which can be set to ``None`` in addition to satisfying the requirements of the sub-validator. - :param validator: A validator (or a list of validators) that is used for - non-``None`` values. - :type validator: callable or `list` of callables. + :param Callable | tuple[Callable] | list[Callable] validator: A validator + (or validators) that is used for non-``None`` values. .. versionadded:: 15.1.0 .. versionchanged:: 17.1.0 *validator* can be a list of validators. + .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. """ - if isinstance(validator, list): + if isinstance(validator, (list, tuple)): return _OptionalValidator(_AndValidator(validator)) + return _OptionalValidator(validator) @@ -359,13 +365,13 @@ class _IsCallableValidator: def is_callable(): """ - A validator that raises a `attr.exceptions.NotCallableError` if the + A validator that raises a `attrs.exceptions.NotCallableError` if the initializer is called with a value for this particular attribute that is not callable. .. versionadded:: 19.1.0 - :raises `attr.exceptions.NotCallableError`: With a human readable error + :raises attrs.exceptions.NotCallableError: With a human readable error message containing the attribute (`attrs.Attribute`) name, and the value it got. """ diff --git a/lib/attr/validators.pyi b/lib/attr/validators.pyi index fd9206de..d194a75a 100644 --- a/lib/attr/validators.pyi +++ b/lib/attr/validators.pyi @@ -51,7 +51,9 @@ def instance_of( def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... def provides(interface: Any) -> _ValidatorType[Any]: ... def optional( - validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] + validator: Union[ + _ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T]] + ] ) -> _ValidatorType[Optional[_T]]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... @@ -82,5 +84,5 @@ def not_( validator: _ValidatorType[_T], *, msg: Optional[str] = None, - exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ... + exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ..., ) -> _ValidatorType[_T]: ... From 864d8fffac0c0294908a5997b7d8a684ab900c60 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Thu, 13 Apr 2023 08:04:58 +0100 Subject: [PATCH 09/36] =?UTF-8?q?Update=20feedparser=206.0.10=20(6d032b8)?= =?UTF-8?q?=20=E2=86=92=206.0.10=20(859ac57).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/feedparser/__init__.py | 33 +- lib/feedparser/api.py | 360 +++--- lib/feedparser/datetimes/__init__.py | 5 +- lib/feedparser/datetimes/asctime.py | 43 +- lib/feedparser/datetimes/greek.py | 82 +- lib/feedparser/datetimes/hungarian.py | 44 +- lib/feedparser/datetimes/iso8601.py | 114 +- lib/feedparser/datetimes/korean.py | 59 +- lib/feedparser/datetimes/perforce.py | 25 +- lib/feedparser/datetimes/rfc822.py | 71 +- lib/feedparser/datetimes/w3dtf.py | 58 +- lib/feedparser/encodings.py | 558 ++++++++-- lib/feedparser/exceptions.py | 12 +- lib/feedparser/html.py | 177 +-- lib/feedparser/http.py | 233 +--- lib/feedparser/mixin.py | 598 +++++----- lib/feedparser/namespaces/_base.py | 366 ++++--- lib/feedparser/namespaces/admin.py | 20 +- lib/feedparser/namespaces/cc.py | 39 +- lib/feedparser/namespaces/dc.py | 38 +- lib/feedparser/namespaces/georss.py | 577 ++++++++-- lib/feedparser/namespaces/itunes.py | 46 +- lib/feedparser/namespaces/mediarss.py | 73 +- lib/feedparser/namespaces/psc.py | 26 +- lib/feedparser/parsers/json.py | 102 +- lib/feedparser/parsers/loose.py | 56 +- lib/feedparser/parsers/strict.py | 62 +- lib/feedparser/py.typed | 0 lib/feedparser/sanitizer.py | 1462 +++++++++++++------------ lib/feedparser/sgml.py | 46 +- lib/feedparser/urls.py | 163 +-- lib/feedparser/util.py | 80 +- 33 files changed, 3280 insertions(+), 2349 deletions(-) create mode 100644 lib/feedparser/py.typed diff --git a/CHANGES.md b/CHANGES.md index 9efccead..50c5764c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ * Update attr 22.2.0 (a9960de) to 22.2.0 (683d056) * Update diskcache 5.4.0 (1cb1425) to 5.6.1 (4d30686) +* Update feedparser 6.0.10 (5fcb3ae) to 6.0.10 (6d032b8) * Update filelock 3.9.0 (ce3e891) to 3.11.0 (d3241b9) * Update Msgpack 1.0.4 (b5acfd5) to 1.0.5 (0516c2c) * Update Requests library 2.28.1 (ec553c2) to 2.29.0 (87d63de) diff --git a/lib/feedparser/__init__.py b/lib/feedparser/__init__.py index 1e8877c0..bdf8060c 100644 --- a/lib/feedparser/__init__.py +++ b/lib/feedparser/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -27,12 +27,18 @@ from .api import parse from .datetimes import registerDateHandler -from .exceptions import * +from .exceptions import ( + CharacterEncodingOverride, + CharacterEncodingUnknown, + FeedparserError, + NonXMLContentType, + UndeclaredNamespace, +) from .util import FeedParserDict -__author__ = 'Kurt McKee ' -__license__ = 'BSD 2-clause' -__version__ = '6.0.10' +__author__ = "Kurt McKee " +__license__ = "BSD 2-clause" +__version__ = "6.0.10" # HTTP "User-Agent" header to send to servers when downloading feeds. # If you are embedding feedparser in a larger application, you should @@ -46,3 +52,20 @@ RESOLVE_RELATIVE_URIS = 1 # If you want feedparser to automatically sanitize all potentially unsafe # HTML content, set this to 1. SANITIZE_HTML = 1 + + +# If you want feedparser to use only a prefix of the feed to detect encodings +# (uses less memory), set this to 1. +OPTIMISTIC_ENCODING_DETECTION = 1 + + +__all__ = ( + "parse", + "registerDateHandler", + "FeedParserDict", + "FeedparserError", + "CharacterEncodingOverride", + "CharacterEncodingUnknown", + "NonXMLContentType", + "UndeclaredNamespace", +) diff --git a/lib/feedparser/api.py b/lib/feedparser/api.py index c56237be..fe49c56d 100644 --- a/lib/feedparser/api.py +++ b/lib/feedparser/api.py @@ -1,5 +1,5 @@ # The public API for feedparser -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -26,29 +26,23 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -import datetime import io -import time -from typing import Dict, List, Union import urllib.error import urllib.parse import xml.sax +from typing import IO, Dict, Optional, Union -import sgmllib3k as sgmllib - -from .datetimes import registerDateHandler, _parse_date -from .encodings import convert_to_utf8 -from .html import BaseHTMLProcessor from . import http +from .encodings import MissingEncoding, convert_file_to_utf8 +from .html import BaseHTMLProcessor from .mixin import XMLParserMixin +from .parsers.json import JSONParser from .parsers.loose import LooseXMLParser from .parsers.strict import StrictXMLParser -from .parsers.json import JSONParser from .sanitizer import replace_doctype -from .urls import convert_to_idn, make_safe_absolute_uri +from .urls import make_safe_absolute_uri from .util import FeedParserDict - # List of preferred XML parsers, by SAX driver name. These will be tried first, # but if they're not installed, Python will keep searching through its own list # of pre-installed parsers until it finds one that supports everything we need. @@ -57,27 +51,30 @@ PREFERRED_XML_PARSERS = ["drv_libxml2"] _XML_AVAILABLE = True SUPPORTED_VERSIONS = { - '': 'unknown', - 'rss090': 'RSS 0.90', - 'rss091n': 'RSS 0.91 (Netscape)', - 'rss091u': 'RSS 0.91 (Userland)', - 'rss092': 'RSS 0.92', - 'rss093': 'RSS 0.93', - 'rss094': 'RSS 0.94', - 'rss20': 'RSS 2.0', - 'rss10': 'RSS 1.0', - 'rss': 'RSS (unknown version)', - 'atom01': 'Atom 0.1', - 'atom02': 'Atom 0.2', - 'atom03': 'Atom 0.3', - 'atom10': 'Atom 1.0', - 'atom': 'Atom (unknown version)', - 'cdf': 'CDF', - 'json1': 'JSON feed 1', + "": "unknown", + "rss090": "RSS 0.90", + "rss091n": "RSS 0.91 (Netscape)", + "rss091u": "RSS 0.91 (Userland)", + "rss092": "RSS 0.92", + "rss093": "RSS 0.93", + "rss094": "RSS 0.94", + "rss20": "RSS 2.0", + "rss10": "RSS 1.0", + "rss": "RSS (unknown version)", + "atom01": "Atom 0.1", + "atom02": "Atom 0.2", + "atom03": "Atom 0.3", + "atom10": "Atom 1.0", + "atom": "Atom (unknown version)", + "cdf": "CDF", + "json1": "JSON feed 1", } -def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers, request_headers, result): +def _open_resource( + url_file_stream_or_string, + result, +): """URL, filename, or string --> stream This function lets you define parsers that take any input source @@ -86,43 +83,44 @@ def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, h to have all the basic stdio read methods (read, readline, readlines). Just .close() the object when you're done with it. - If the etag argument is supplied, it will be used as the value of an - If-None-Match request header. - - If the modified argument is supplied, it can be a tuple of 9 integers - (as returned by gmtime() in the standard Python time module) or a date - string in any format supported by feedparser. Regardless, it MUST - be in GMT (Greenwich Mean Time). It will be reformatted into an - RFC 1123-compliant date and used as the value of an If-Modified-Since - request header. - - If the agent argument is supplied, it will be used as the value of a - User-Agent request header. - - If the referrer argument is supplied, it will be used as the value of a - Referer[sic] request header. - - If handlers is supplied, it is a list of handlers used to build a - urllib2 opener. - - if request_headers is supplied it is a dictionary of HTTP request headers - that will override the values generated by FeedParser. - - :return: A bytes object. + :return: A seekable, readable file object. """ - if hasattr(url_file_stream_or_string, 'read'): - return url_file_stream_or_string.read() + # Some notes on the history of the implementation of _open_resource(). + # + # parse() might need to go over the feed content twice: + # if the strict parser fails, it tries again with the loose parser. + # + # In 5.2.0, this returned an open file, to be read() by parse(). + # By 6.0.8, this returned bytes directly. + # + # Since #296 (>6.0.8), this once again returns an open file + # (to reduce memory usage, see convert_file_to_utf8() for details). + # However, to accommodate parse() needing the content twice, + # the returned file is guaranteed to be seekable. + # (If the underlying resource is not seekable, + # the content is read and wrapped in a io.BytesIO/StringIO.) - if isinstance(url_file_stream_or_string, str) \ - and urllib.parse.urlparse(url_file_stream_or_string)[0] in ('http', 'https', 'ftp', 'file', 'feed'): - return http.get(url_file_stream_or_string, etag, modified, agent, referrer, handlers, request_headers, result) + if callable(getattr(url_file_stream_or_string, "read", None)): + if callable(getattr(url_file_stream_or_string, "seekable", None)): + if url_file_stream_or_string.seekable(): + return url_file_stream_or_string + return _to_in_memory_file(url_file_stream_or_string.read()) + + looks_like_url = isinstance( + url_file_stream_or_string, str + ) and urllib.parse.urlparse(url_file_stream_or_string)[0] in ( + "http", + "https", + ) + if looks_like_url: + data = http.get(url_file_stream_or_string, result) + return io.BytesIO(data) # try to open with native open function (if url_file_stream_or_string is a filename) try: - with open(url_file_stream_or_string, 'rb') as f: - data = f.read() - except (IOError, UnicodeEncodeError, TypeError, ValueError): + return open(url_file_stream_or_string, "rb") + except (OSError, TypeError, ValueError): # if url_file_stream_or_string is a str object that # cannot be converted to the encoding returned by # sys.getfilesystemencoding(), a UnicodeEncodeError @@ -131,33 +129,32 @@ def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, h # (such as an XML document encoded in UTF-32), TypeError will # be thrown. pass - else: - return data - # treat url_file_stream_or_string as string - if not isinstance(url_file_stream_or_string, bytes): - return url_file_stream_or_string.encode('utf-8') - return url_file_stream_or_string + # treat url_file_stream_or_string as bytes/string + return _to_in_memory_file(url_file_stream_or_string) + + +def _to_in_memory_file(data): + if isinstance(data, str): + return io.StringIO(data) + else: + return io.BytesIO(data) class LooseFeedParser(LooseXMLParser, XMLParserMixin, BaseHTMLProcessor): pass + class StrictFeedParser(StrictXMLParser, XMLParserMixin, xml.sax.handler.ContentHandler): pass def parse( - url_file_stream_or_string, - etag: str = None, - modified: Union[str, datetime.datetime, time.struct_time] = None, - agent: str = None, - referrer: str = None, - handlers: List = None, - request_headers: Dict[str, str] = None, - response_headers: Dict[str, str] = None, - resolve_relative_uris: bool = None, - sanitize_html: bool = None, + url_file_stream_or_string, + response_headers: Optional[Dict[str, str]] = None, + resolve_relative_uris: Optional[bool] = None, + sanitize_html: Optional[bool] = None, + optimistic_encoding_detection: Optional[bool] = None, ) -> FeedParserDict: """Parse a feed from a URL, file, stream, or string. @@ -174,20 +171,6 @@ def parse( When a URL is not passed the feed location to use in relative URL resolution should be passed in the ``Content-Location`` response header (see ``response_headers`` below). - :param etag: - HTTP ``ETag`` request header. - :param modified: - HTTP ``Last-Modified`` request header. - :param agent: - HTTP ``User-Agent`` request header, which defaults to - the value of :data:`feedparser.USER_AGENT`. - :param referrer: - HTTP ``Referer`` [sic] request header. - :param handlers: - A list of handlers that will be passed to urllib2. - :param request_headers: - A mapping of HTTP header name to HTTP header value to add to the - request, overriding internally generated values. :param response_headers: A mapping of HTTP header name to HTTP header value. Multiple values may be joined with a comma. If a HTTP request was made, these headers @@ -201,20 +184,14 @@ def parse( Should feedparser skip HTML sanitization? Only disable this if you know what you are doing! Defaults to the value of :data:`feedparser.SANITIZE_HTML`, which is ``True``. + :param optimistic_encoding_detection: + Should feedparser use only a prefix of the feed to detect encodings + (uses less memory, but the wrong encoding may be detected in rare cases). + Defaults to the value of + :data:`feedparser.OPTIMISTIC_ENCODING_DETECTION`, which is ``True``. """ - # Avoid a cyclic import. - if not agent: - import feedparser - agent = feedparser.USER_AGENT - if sanitize_html is None: - import feedparser - sanitize_html = bool(feedparser.SANITIZE_HTML) - if resolve_relative_uris is None: - import feedparser - resolve_relative_uris = bool(feedparser.RESOLVE_RELATIVE_URIS) - result = FeedParserDict( bozo=False, entries=[], @@ -223,50 +200,110 @@ def parse( ) try: - data = _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers, request_headers, result) + file = _open_resource( + url_file_stream_or_string, + result, + ) except urllib.error.URLError as error: - result.update({ - 'bozo': True, - 'bozo_exception': error, - }) + result.update( + { + "bozo": True, + "bozo_exception": error, + } + ) return result - if not data: + # at this point, the file is guaranteed to be seekable; + # we read 1 byte/character to see if it's empty and return early + # (this preserves the behavior in 6.0.8) + initial_file_offset = file.tell() + if not file.read(1): return result + file.seek(initial_file_offset) # overwrite existing headers using response_headers - result['headers'].update(response_headers or {}) + result["headers"].update(response_headers or {}) - data = convert_to_utf8(result['headers'], data, result) - use_json_parser = result['content-type'] == 'application/json' - use_strict_parser = result['encoding'] and True or False + try: + _parse_file_inplace( + file, + result, + resolve_relative_uris=resolve_relative_uris, + sanitize_html=sanitize_html, + optimistic_encoding_detection=optimistic_encoding_detection, + ) + finally: + if not hasattr(url_file_stream_or_string, "read"): + # the file does not come from the user, close it + file.close() - if not use_json_parser: - result['version'], data, entities = replace_doctype(data) + return result + + +def _parse_file_inplace( + file: Union[IO[bytes], IO[str]], + result: dict, + *, + resolve_relative_uris: Optional[bool] = None, + sanitize_html: Optional[bool] = None, + optimistic_encoding_detection: Optional[bool] = None, +) -> None: + # Avoid a cyclic import. + import feedparser + + if sanitize_html is None: + sanitize_html = bool(feedparser.SANITIZE_HTML) + if resolve_relative_uris is None: + resolve_relative_uris = bool(feedparser.RESOLVE_RELATIVE_URIS) + if optimistic_encoding_detection is None: + optimistic_encoding_detection = bool(feedparser.OPTIMISTIC_ENCODING_DETECTION) + + stream_factory = convert_file_to_utf8( + result["headers"], file, result, optimistic_encoding_detection + ) + # We're done with file, all access must happen through stream_factory. + del file + + # Some notes about the stream_factory.get_{text,binary}_file() methods: + # + # Calling them a second time will raise io.UnsupportedOperation + # if the underlying file was not seekable. + # + # Calling close() on the returned file is ignored + # (that is, the underlying file is *not* closed), + # because the SAX parser closes the file when done; + # we don't want that, since we might try again with the loose parser. + + use_json_parser = False + if result["content-type"] in {"application/json", "application/feed+json"}: + use_json_parser = True + use_strict_parser = bool(result["encoding"]) + + result["version"], stream_factory.prefix, entities = replace_doctype( + stream_factory.prefix + ) # Ensure that baseuri is an absolute URI using an acceptable URI scheme. - contentloc = result['headers'].get('content-location', '') - href = result.get('href', '') - baseuri = make_safe_absolute_uri(href, contentloc) or make_safe_absolute_uri(contentloc) or href + contentloc = result["headers"].get("content-location", "") + href = result.get("href", "") + baseuri = ( + make_safe_absolute_uri(href, contentloc) + or make_safe_absolute_uri(contentloc) + or href + ) - baselang = result['headers'].get('content-language', None) + baselang = result["headers"].get("content-language", None) if isinstance(baselang, bytes) and baselang is not None: - baselang = baselang.decode('utf-8', 'ignore') + baselang = baselang.decode("utf-8", "ignore") if not _XML_AVAILABLE: use_strict_parser = False + feed_parser: Union[JSONParser, StrictFeedParser, LooseFeedParser] - if use_json_parser: - result['version'] = None - feed_parser = JSONParser(baseuri, baselang, 'utf-8') - try: - feed_parser.feed(data) - except Exception as e: - result['bozo'] = 1 - result['bozo_exception'] = e - elif use_strict_parser: + + if use_strict_parser and not use_json_parser: # Initialize the SAX parser. - feed_parser = StrictFeedParser(baseuri, baselang, 'utf-8') + feed_parser = StrictFeedParser(baseuri, baselang, "utf-8") feed_parser.resolve_relative_uris = resolve_relative_uris feed_parser.sanitize_html = sanitize_html saxparser = xml.sax.make_parser(PREFERRED_XML_PARSERS) @@ -279,27 +316,62 @@ def parse( saxparser.setContentHandler(feed_parser) saxparser.setErrorHandler(feed_parser) source = xml.sax.xmlreader.InputSource() - source.setByteStream(io.BytesIO(data)) + + # If an encoding was detected, decode the file on the fly; + # otherwise, pass it as-is and let the SAX parser deal with it. + try: + source.setCharacterStream(stream_factory.get_text_file()) + except MissingEncoding: + source.setByteStream(stream_factory.get_binary_file()) + try: saxparser.parse(source) except xml.sax.SAXException as e: - result['bozo'] = 1 - result['bozo_exception'] = feed_parser.exc or e + result["bozo"] = 1 + result["bozo_exception"] = feed_parser.exc or e use_strict_parser = False - # The loose XML parser will be tried if the JSON parser was not used, - # and if the strict XML parser was not used (or if it failed). - if not use_json_parser and not use_strict_parser: - feed_parser = LooseFeedParser(baseuri, baselang, 'utf-8', entities) + # The loose XML parser will be tried if the strict XML parser was not used + # (or if it failed to parse the feed). + if not use_strict_parser and not use_json_parser: + feed_parser = LooseFeedParser(baseuri, baselang, "utf-8", entities) feed_parser.resolve_relative_uris = resolve_relative_uris feed_parser.sanitize_html = sanitize_html - feed_parser.feed(data.decode('utf-8', 'replace')) - result['feed'] = feed_parser.feeddata - result['entries'] = feed_parser.entries - result['version'] = result['version'] or feed_parser.version + # If an encoding was detected, use it; otherwise, assume utf-8 and do your best. + # Will raise io.UnsupportedOperation if the underlying file is not seekable. + data = stream_factory.get_text_file("utf-8", "replace").read() + + # As of 6.0.8, LooseFeedParser.feed() can be called exactly once + # with the entire data (it does some re.sub() and str.replace() on it). + # + # SGMLParser (of which LooseFeedParser is a subclass) + # *can* be fed in a streaming fashion, + # by calling feed() repeatedly with chunks of text. + # + # When/if LooseFeedParser will support being fed chunks, + # replace the read() call above with read(size)/feed() calls in a loop. + + feed_parser.feed(data) + + # If parsing with the loose XML parser resulted in no information, + # flag that the JSON parser should be tried. + if not (feed_parser.entries or feed_parser.feeddata or feed_parser.version): + use_json_parser = True + + if use_json_parser: + result["version"] = None + feed_parser = JSONParser(baseuri, baselang, "utf-8") + try: + feed_parser.feed(stream_factory.get_file()) + except Exception as e: + result["bozo"] = 1 + result["bozo_exception"] = e + + result["feed"] = feed_parser.feeddata + result["entries"] = feed_parser.entries + result["version"] = result["version"] or feed_parser.version if isinstance(feed_parser, JSONParser): - result['namespaces'] = {} - else: - result['namespaces'] = feed_parser.namespaces_in_use - return result + result["namespaces"] = {} + else: + result["namespaces"] = feed_parser.namespaces_in_use diff --git a/lib/feedparser/datetimes/__init__.py b/lib/feedparser/datetimes/__init__.py index 9e09ec27..36b4857d 100644 --- a/lib/feedparser/datetimes/__init__.py +++ b/lib/feedparser/datetimes/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -27,11 +27,12 @@ from time import struct_time from typing import Callable, List, Optional + from .asctime import _parse_date_asctime from .greek import _parse_date_greek from .hungarian import _parse_date_hungarian from .iso8601 import _parse_date_iso8601 -from .korean import _parse_date_onblog, _parse_date_nate +from .korean import _parse_date_nate, _parse_date_onblog from .perforce import _parse_date_perforce from .rfc822 import _parse_date_rfc822 from .w3dtf import _parse_date_w3dtf diff --git a/lib/feedparser/datetimes/asctime.py b/lib/feedparser/datetimes/asctime.py index c4b16249..5b75ca54 100644 --- a/lib/feedparser/datetimes/asctime.py +++ b/lib/feedparser/datetimes/asctime.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -28,18 +28,18 @@ from .rfc822 import _parse_date_rfc822 _months = [ - 'jan', - 'feb', - 'mar', - 'apr', - 'may', - 'jun', - 'jul', - 'aug', - 'sep', - 'oct', - 'nov', - 'dec', + "jan", + "feb", + "mar", + "apr", + "may", + "jun", + "jul", + "aug", + "sep", + "oct", + "nov", + "dec", ] @@ -59,13 +59,22 @@ def _parse_date_asctime(dt): # Insert a GMT timezone, if needed. if len(parts) == 5: - parts.insert(4, '+0000') + parts.insert(4, "+0000") # Exit if there are not six parts. if len(parts) != 6: return None # Reassemble the parts in an RFC822-compatible order and parse them. - return _parse_date_rfc822(' '.join([ - parts[0], parts[2], parts[1], parts[5], parts[3], parts[4], - ])) + return _parse_date_rfc822( + " ".join( + [ + parts[0], + parts[2], + parts[1], + parts[5], + parts[3], + parts[4], + ] + ) + ) diff --git a/lib/feedparser/datetimes/greek.py b/lib/feedparser/datetimes/greek.py index 7f433fed..a6a54da7 100644 --- a/lib/feedparser/datetimes/greek.py +++ b/lib/feedparser/datetimes/greek.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -31,38 +31,40 @@ from .rfc822 import _parse_date_rfc822 # Unicode strings for Greek date strings _greek_months = { - '\u0399\u03b1\u03bd': 'Jan', # c9e1ed in iso-8859-7 - '\u03a6\u03b5\u03b2': 'Feb', # d6e5e2 in iso-8859-7 - '\u039c\u03ac\u03ce': 'Mar', # ccdcfe in iso-8859-7 - '\u039c\u03b1\u03ce': 'Mar', # cce1fe in iso-8859-7 - '\u0391\u03c0\u03c1': 'Apr', # c1f0f1 in iso-8859-7 - '\u039c\u03ac\u03b9': 'May', # ccdce9 in iso-8859-7 - '\u039c\u03b1\u03ca': 'May', # cce1fa in iso-8859-7 - '\u039c\u03b1\u03b9': 'May', # cce1e9 in iso-8859-7 - '\u0399\u03bf\u03cd\u03bd': 'Jun', # c9effded in iso-8859-7 - '\u0399\u03bf\u03bd': 'Jun', # c9efed in iso-8859-7 - '\u0399\u03bf\u03cd\u03bb': 'Jul', # c9effdeb in iso-8859-7 - '\u0399\u03bf\u03bb': 'Jul', # c9f9eb in iso-8859-7 - '\u0391\u03cd\u03b3': 'Aug', # c1fde3 in iso-8859-7 - '\u0391\u03c5\u03b3': 'Aug', # c1f5e3 in iso-8859-7 - '\u03a3\u03b5\u03c0': 'Sep', # d3e5f0 in iso-8859-7 - '\u039f\u03ba\u03c4': 'Oct', # cfeaf4 in iso-8859-7 - '\u039d\u03bf\u03ad': 'Nov', # cdefdd in iso-8859-7 - '\u039d\u03bf\u03b5': 'Nov', # cdefe5 in iso-8859-7 - '\u0394\u03b5\u03ba': 'Dec', # c4e5ea in iso-8859-7 + "\u0399\u03b1\u03bd": "Jan", # c9e1ed in iso-8859-7 + "\u03a6\u03b5\u03b2": "Feb", # d6e5e2 in iso-8859-7 + "\u039c\u03ac\u03ce": "Mar", # ccdcfe in iso-8859-7 + "\u039c\u03b1\u03ce": "Mar", # cce1fe in iso-8859-7 + "\u0391\u03c0\u03c1": "Apr", # c1f0f1 in iso-8859-7 + "\u039c\u03ac\u03b9": "May", # ccdce9 in iso-8859-7 + "\u039c\u03b1\u03ca": "May", # cce1fa in iso-8859-7 + "\u039c\u03b1\u03b9": "May", # cce1e9 in iso-8859-7 + "\u0399\u03bf\u03cd\u03bd": "Jun", # c9effded in iso-8859-7 + "\u0399\u03bf\u03bd": "Jun", # c9efed in iso-8859-7 + "\u0399\u03bf\u03cd\u03bb": "Jul", # c9effdeb in iso-8859-7 + "\u0399\u03bf\u03bb": "Jul", # c9f9eb in iso-8859-7 + "\u0391\u03cd\u03b3": "Aug", # c1fde3 in iso-8859-7 + "\u0391\u03c5\u03b3": "Aug", # c1f5e3 in iso-8859-7 + "\u03a3\u03b5\u03c0": "Sep", # d3e5f0 in iso-8859-7 + "\u039f\u03ba\u03c4": "Oct", # cfeaf4 in iso-8859-7 + "\u039d\u03bf\u03ad": "Nov", # cdefdd in iso-8859-7 + "\u039d\u03bf\u03b5": "Nov", # cdefe5 in iso-8859-7 + "\u0394\u03b5\u03ba": "Dec", # c4e5ea in iso-8859-7 } _greek_wdays = { - '\u039a\u03c5\u03c1': 'Sun', # caf5f1 in iso-8859-7 - '\u0394\u03b5\u03c5': 'Mon', # c4e5f5 in iso-8859-7 - '\u03a4\u03c1\u03b9': 'Tue', # d4f1e9 in iso-8859-7 - '\u03a4\u03b5\u03c4': 'Wed', # d4e5f4 in iso-8859-7 - '\u03a0\u03b5\u03bc': 'Thu', # d0e5ec in iso-8859-7 - '\u03a0\u03b1\u03c1': 'Fri', # d0e1f1 in iso-8859-7 - '\u03a3\u03b1\u03b2': 'Sat', # d3e1e2 in iso-8859-7 + "\u039a\u03c5\u03c1": "Sun", # caf5f1 in iso-8859-7 + "\u0394\u03b5\u03c5": "Mon", # c4e5f5 in iso-8859-7 + "\u03a4\u03c1\u03b9": "Tue", # d4f1e9 in iso-8859-7 + "\u03a4\u03b5\u03c4": "Wed", # d4e5f4 in iso-8859-7 + "\u03a0\u03b5\u03bc": "Thu", # d0e5ec in iso-8859-7 + "\u03a0\u03b1\u03c1": "Fri", # d0e1f1 in iso-8859-7 + "\u03a3\u03b1\u03b2": "Sat", # d3e1e2 in iso-8859-7 } -_greek_date_format_re = re.compile(r'([^,]+),\s+(\d{2})\s+([^\s]+)\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+([^\s]+)') +_greek_date_format_re = re.compile( + r"([^,]+),\s+(\d{2})\s+([^\s]+)\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+([^\s]+)" +) def _parse_date_greek(date_string): @@ -72,15 +74,17 @@ def _parse_date_greek(date_string): return wday = _greek_wdays[m.group(1)] month = _greek_months[m.group(3)] - rfc822date = '%(wday)s, %(day)s %(month)s %(year)s %(hour)s:%(minute)s:%(second)s %(zonediff)s' % \ - { - 'wday': wday, - 'day': m.group(2), - 'month': month, - 'year': m.group(4), - 'hour': m.group(5), - 'minute': m.group(6), - 'second': m.group(7), - 'zonediff': m.group(8), - } + rfc822date = ( + "%(wday)s, %(day)s %(month)s %(year)s %(hour)s:%(minute)s:%(second)s %(offset)s" + % { + "wday": wday, + "day": m.group(2), + "month": month, + "year": m.group(4), + "hour": m.group(5), + "minute": m.group(6), + "second": m.group(7), + "offset": m.group(8), + } + ) return _parse_date_rfc822(rfc822date) diff --git a/lib/feedparser/datetimes/hungarian.py b/lib/feedparser/datetimes/hungarian.py index 691a6ebc..b4fa8436 100644 --- a/lib/feedparser/datetimes/hungarian.py +++ b/lib/feedparser/datetimes/hungarian.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -31,21 +31,23 @@ from .w3dtf import _parse_date_w3dtf # Unicode strings for Hungarian date strings _hungarian_months = { - 'janu\u00e1r': '01', # e1 in iso-8859-2 - 'febru\u00e1ri': '02', # e1 in iso-8859-2 - 'm\u00e1rcius': '03', # e1 in iso-8859-2 - '\u00e1prilis': '04', # e1 in iso-8859-2 - 'm\u00e1ujus': '05', # e1 in iso-8859-2 - 'j\u00fanius': '06', # fa in iso-8859-2 - 'j\u00falius': '07', # fa in iso-8859-2 - 'augusztus': '08', - 'szeptember': '09', - 'okt\u00f3ber': '10', # f3 in iso-8859-2 - 'november': '11', - 'december': '12', + "janu\u00e1r": "01", # e1 in iso-8859-2 + "febru\u00e1ri": "02", # e1 in iso-8859-2 + "m\u00e1rcius": "03", # e1 in iso-8859-2 + "\u00e1prilis": "04", # e1 in iso-8859-2 + "m\u00e1ujus": "05", # e1 in iso-8859-2 + "j\u00fanius": "06", # fa in iso-8859-2 + "j\u00falius": "07", # fa in iso-8859-2 + "augusztus": "08", + "szeptember": "09", + "okt\u00f3ber": "10", # f3 in iso-8859-2 + "november": "11", + "december": "12", } -_hungarian_date_format_re = re.compile(r'(\d{4})-([^-]+)-(\d{,2})T(\d{,2}):(\d{2})([+-](\d{,2}:\d{2}))') +_hungarian_date_format_re = re.compile( + r"(\d{4})-([^-]+)-(\d{,2})T(\d{,2}):(\d{2})([+-](\d{,2}:\d{2}))" +) def _parse_date_hungarian(date_string): @@ -56,17 +58,9 @@ def _parse_date_hungarian(date_string): month = _hungarian_months[m.group(2)] day = m.group(3) if len(day) == 1: - day = '0' + day + day = "0" + day hour = m.group(4) if len(hour) == 1: - hour = '0' + hour - w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s%(zonediff)s' % \ - { - 'year': m.group(1), - 'month': month, - 'day': day, - 'hour': hour, - 'minute': m.group(5), - 'zonediff': m.group(6), - } + hour = "0" + hour + w3dtfdate = f"{m.group(1)}-{month}-{day}T{hour}:{m.group(5)}{m.group(6)}" return _parse_date_w3dtf(w3dtfdate) diff --git a/lib/feedparser/datetimes/iso8601.py b/lib/feedparser/datetimes/iso8601.py index 3d3b3f96..8130cc9b 100644 --- a/lib/feedparser/datetimes/iso8601.py +++ b/lib/feedparser/datetimes/iso8601.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -38,36 +38,36 @@ import time # Please note the order in templates is significant because we need a # greedy match. _iso8601_tmpl = [ - 'YYYY-?MM-?DD', - 'YYYY-0MM?-?DD', - 'YYYY-MM', - 'YYYY-?OOO', - 'YY-?MM-?DD', - 'YY-?OOO', - 'YYYY', - '-YY-?MM', - '-OOO', - '-YY', - '--MM-?DD', - '--MM', - '---DD', - 'CC', - '', + "YYYY-?MM-?DD", + "YYYY-0MM?-?DD", + "YYYY-MM", + "YYYY-?OOO", + "YY-?MM-?DD", + "YY-?OOO", + "YYYY", + "-YY-?MM", + "-OOO", + "-YY", + "--MM-?DD", + "--MM", + "---DD", + "CC", + "", ] _iso8601_re = [ - tmpl.replace( - 'YYYY', r'(?P\d{4})').replace( - 'YY', r'(?P\d\d)').replace( - 'MM', r'(?P[01]\d)').replace( - 'DD', r'(?P[0123]\d)').replace( - 'OOO', r'(?P[0123]\d\d)').replace( - 'CC', r'(?P\d\d$)') - + r'(T?(?P\d{2}):(?P\d{2})' - + r'(:(?P\d{2}))?' - + r'(\.(?P\d+))?' - + r'(?P[+-](?P\d{2})(:(?P\d{2}))?|Z)?)?' - for tmpl in _iso8601_tmpl] + tmpl.replace("YYYY", r"(?P\d{4})") + .replace("YY", r"(?P\d\d)") + .replace("MM", r"(?P[01]\d)") + .replace("DD", r"(?P[0123]\d)") + .replace("OOO", r"(?P[0123]\d\d)") + .replace("CC", r"(?P\d\d$)") + + r"(T?(?P\d{2}):(?P\d{2})" + + r"(:(?P\d{2}))?" + + r"(\.(?P\d+))?" + + r"(?P[+-](?P\d{2})(:(?P\d{2}))?|Z)?)?" + for tmpl in _iso8601_tmpl +] _iso8601_matches = [re.compile(regex).match for regex in _iso8601_re] @@ -83,21 +83,21 @@ def _parse_date_iso8601(date_string): if m.span() == (0, 0): return params = m.groupdict() - ordinal = params.get('ordinal', 0) + ordinal = params.get("ordinal", 0) if ordinal: ordinal = int(ordinal) else: ordinal = 0 - year = params.get('year', '--') - if not year or year == '--': + year = params.get("year", "--") + if not year or year == "--": year = time.gmtime()[0] elif len(year) == 2: # ISO 8601 assumes current century, i.e. 93 -> 2093, NOT 1993 year = 100 * int(time.gmtime()[0] / 100) + int(year) else: year = int(year) - month = params.get('month', '-') - if not month or month == '-': + month = params.get("month", "-") + if not month or month == "-": # ordinals are NOT normalized by mktime, we simulate them # by setting month=1, day=ordinal if ordinal: @@ -105,13 +105,14 @@ def _parse_date_iso8601(date_string): else: month = time.gmtime()[1] month = int(month) - day = params.get('day', 0) + day = params.get("day", 0) if not day: # see above if ordinal: day = ordinal - elif params.get('century', 0) or \ - params.get('year', 0) or params.get('month', 0): + elif ( + params.get("century", 0) or params.get("year", 0) or params.get("month", 0) + ): day = 1 else: day = time.gmtime()[2] @@ -119,29 +120,38 @@ def _parse_date_iso8601(date_string): day = int(day) # special case of the century - is the first year of the 21st century # 2000 or 2001 ? The debate goes on... - if 'century' in params: - year = (int(params['century']) - 1) * 100 + 1 + if "century" in params: + year = (int(params["century"]) - 1) * 100 + 1 # in ISO 8601 most fields are optional - for field in ['hour', 'minute', 'second', 'tzhour', 'tzmin']: + for field in ["hour", "minute", "second", "tzhour", "tzmin"]: if not params.get(field, None): params[field] = 0 - hour = int(params.get('hour', 0)) - minute = int(params.get('minute', 0)) - second = int(float(params.get('second', 0))) + hour = int(params.get("hour", 0)) + minute = int(params.get("minute", 0)) + second = int(float(params.get("second", 0))) # weekday is normalized by mktime(), we can ignore it weekday = 0 daylight_savings_flag = -1 - tm = [year, month, day, hour, minute, second, weekday, - ordinal, daylight_savings_flag] + tm = [ + year, + month, + day, + hour, + minute, + second, + weekday, + ordinal, + daylight_savings_flag, + ] # ISO 8601 time zone adjustments - tz = params.get('tz') - if tz and tz != 'Z': - if tz[0] == '-': - tm[3] += int(params.get('tzhour', 0)) - tm[4] += int(params.get('tzmin', 0)) - elif tz[0] == '+': - tm[3] -= int(params.get('tzhour', 0)) - tm[4] -= int(params.get('tzmin', 0)) + tz = params.get("tz") + if tz and tz != "Z": + if tz[0] == "-": + tm[3] += int(params.get("tzhour", 0)) + tm[4] += int(params.get("tzmin", 0)) + elif tz[0] == "+": + tm[3] -= int(params.get("tzhour", 0)) + tm[4] -= int(params.get("tzmin", 0)) else: return None # Python's time.mktime() is a wrapper around the ANSI C mktime(3c) diff --git a/lib/feedparser/datetimes/korean.py b/lib/feedparser/datetimes/korean.py index 788d4666..4c5494f2 100644 --- a/lib/feedparser/datetimes/korean.py +++ b/lib/feedparser/datetimes/korean.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -30,20 +30,21 @@ import re from .w3dtf import _parse_date_w3dtf # 8-bit date handling routines written by ytrewq1. -_korean_year = '\ub144' # b3e2 in euc-kr -_korean_month = '\uc6d4' # bff9 in euc-kr -_korean_day = '\uc77c' # c0cf in euc-kr -_korean_am = '\uc624\uc804' # bfc0 c0fc in euc-kr -_korean_pm = '\uc624\ud6c4' # bfc0 c8c4 in euc-kr +_korean_year = "\ub144" # b3e2 in euc-kr +_korean_month = "\uc6d4" # bff9 in euc-kr +_korean_day = "\uc77c" # c0cf in euc-kr +_korean_am = "\uc624\uc804" # bfc0 c0fc in euc-kr +_korean_pm = "\uc624\ud6c4" # bfc0 c8c4 in euc-kr _korean_onblog_date_re = re.compile( - r'(\d{4})%s\s+(\d{2})%s\s+(\d{2})%s\s+(\d{2}):(\d{2}):(\d{2})' + r"(\d{4})%s\s+(\d{2})%s\s+(\d{2})%s\s+(\d{2}):(\d{2}):(\d{2})" % (_korean_year, _korean_month, _korean_day) ) _korean_nate_date_re = re.compile( - r'(\d{4})-(\d{2})-(\d{2})\s+(%s|%s)\s+(\d{,2}):(\d{,2}):(\d{,2})' - % (_korean_am, _korean_pm)) + r"(\d{4})-(\d{2})-(\d{2})\s+(%s|%s)\s+(\d{,2}):(\d{,2}):(\d{,2})" + % (_korean_am, _korean_pm) +) def _parse_date_onblog(dateString): @@ -51,10 +52,18 @@ def _parse_date_onblog(dateString): m = _korean_onblog_date_re.match(dateString) if not m: return - w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \ - {'year': m.group(1), 'month': m.group(2), 'day': m.group(3), - 'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6), - 'zonediff': '+09:00'} + w3dtfdate = ( + "%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s" + % { + "year": m.group(1), + "month": m.group(2), + "day": m.group(3), + "hour": m.group(4), + "minute": m.group(5), + "second": m.group(6), + "zonediff": "+09:00", + } + ) return _parse_date_w3dtf(w3dtfdate) @@ -69,15 +78,17 @@ def _parse_date_nate(dateString): hour += 12 hour = str(hour) if len(hour) == 1: - hour = '0' + hour - w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \ - { - 'year': m.group(1), - 'month': m.group(2), - 'day': m.group(3), - 'hour': hour, - 'minute': m.group(6), - 'second': m.group(7), - 'zonediff': '+09:00', - } + hour = "0" + hour + w3dtfdate = ( + "%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s" + % { + "year": m.group(1), + "month": m.group(2), + "day": m.group(3), + "hour": hour, + "minute": m.group(6), + "second": m.group(7), + "zonediff": "+09:00", + } + ) return _parse_date_w3dtf(w3dtfdate) diff --git a/lib/feedparser/datetimes/perforce.py b/lib/feedparser/datetimes/perforce.py index d62d722f..8d75eb5b 100644 --- a/lib/feedparser/datetimes/perforce.py +++ b/lib/feedparser/datetimes/perforce.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -33,14 +33,31 @@ import time def _parse_date_perforce(date_string): """parse a date in yyyy/mm/dd hh:mm:ss TTT format""" # Fri, 2006/09/15 08:19:53 EDT - _my_date_pattern = re.compile(r'(\w{,3}), (\d{,4})/(\d{,2})/(\d{2}) (\d{,2}):(\d{2}):(\d{2}) (\w{,3})') + _my_date_pattern = re.compile( + r"(\w{,3}), (\d{,4})/(\d{,2})/(\d{2}) (\d{,2}):(\d{2}):(\d{2}) (\w{,3})" + ) m = _my_date_pattern.search(date_string) if m is None: return None dow, year, month, day, hour, minute, second, tz = m.groups() - months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - new_date_string = "%s, %s %s %s %s:%s:%s %s" % (dow, day, months[int(month) - 1], year, hour, minute, second, tz) + months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] + new_date_string = ( + f"{dow}, {day} {months[int(month) - 1]} {year} {hour}:{minute}:{second} {tz}" + ) tm = email.utils.parsedate_tz(new_date_string) if tm: return time.gmtime(email.utils.mktime_tz(tm)) diff --git a/lib/feedparser/datetimes/rfc822.py b/lib/feedparser/datetimes/rfc822.py index 871e18fd..5f300f1d 100644 --- a/lib/feedparser/datetimes/rfc822.py +++ b/lib/feedparser/datetimes/rfc822.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -28,20 +28,45 @@ import datetime timezone_names = { - 'ut': 0, 'gmt': 0, 'z': 0, - 'adt': -3, 'ast': -4, 'at': -4, - 'edt': -4, 'est': -5, 'et': -5, - 'cdt': -5, 'cst': -6, 'ct': -6, - 'mdt': -6, 'mst': -7, 'mt': -7, - 'pdt': -7, 'pst': -8, 'pt': -8, - 'a': -1, 'n': 1, - 'm': -12, 'y': 12, - 'met': 1, 'mest': 2, + "ut": 0, + "gmt": 0, + "z": 0, + "adt": -3, + "ast": -4, + "at": -4, + "edt": -4, + "est": -5, + "et": -5, + "cdt": -5, + "cst": -6, + "ct": -6, + "mdt": -6, + "mst": -7, + "mt": -7, + "pdt": -7, + "pst": -8, + "pt": -8, + "a": -1, + "n": 1, + "m": -12, + "y": 12, + "met": 1, + "mest": 2, } -day_names = {'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'} +day_names = {"mon", "tue", "wed", "thu", "fri", "sat", "sun"} months = { - 'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, - 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12, + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, } @@ -63,7 +88,7 @@ def _parse_date_rfc822(date): parts = date.lower().split() if len(parts) < 5: # Assume that the time and timezone are missing - parts.extend(('00:00:00', '0000')) + parts.extend(("00:00:00", "0000")) # Remove the day name if parts[0][:3] in day_names: parts = parts[1:] @@ -101,26 +126,26 @@ def _parse_date_rfc822(date): year += (1900, 2000)[year < 90] # Handle the time (default to 00:00:00). - time_parts = parts[3].split(':') - time_parts.extend(('0',) * (3 - len(time_parts))) + time_parts = parts[3].split(":") + time_parts.extend(("0",) * (3 - len(time_parts))) try: - (hour, minute, second) = [int(i) for i in time_parts] + (hour, minute, second) = (int(i) for i in time_parts) except ValueError: return None # Handle the timezone information, if any (default to +0000). # Strip 'Etc/' from the timezone. - if parts[4].startswith('etc/'): + if parts[4].startswith("etc/"): parts[4] = parts[4][4:] # Normalize timezones that start with 'gmt': # GMT-05:00 => -0500 # GMT => GMT - if parts[4].startswith('gmt'): - parts[4] = ''.join(parts[4][3:].split(':')) or 'gmt' + if parts[4].startswith("gmt"): + parts[4] = "".join(parts[4][3:].split(":")) or "gmt" # Handle timezones like '-0500', '+0500', and 'EST' - if parts[4] and parts[4][0] in ('-', '+'): + if parts[4] and parts[4][0] in ("-", "+"): try: - if ':' in parts[4]: + if ":" in parts[4]: timezone_hours = int(parts[4][1:3]) timezone_minutes = int(parts[4][4:]) else: @@ -128,7 +153,7 @@ def _parse_date_rfc822(date): timezone_minutes = int(parts[4][3:]) except ValueError: return None - if parts[4].startswith('-'): + if parts[4].startswith("-"): timezone_hours *= -1 timezone_minutes *= -1 else: diff --git a/lib/feedparser/datetimes/w3dtf.py b/lib/feedparser/datetimes/w3dtf.py index 6fb2c545..977005ca 100644 --- a/lib/feedparser/datetimes/w3dtf.py +++ b/lib/feedparser/datetimes/w3dtf.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -28,14 +28,28 @@ import datetime timezonenames = { - 'ut': 0, 'gmt': 0, 'z': 0, - 'adt': -3, 'ast': -4, 'at': -4, - 'edt': -4, 'est': -5, 'et': -5, - 'cdt': -5, 'cst': -6, 'ct': -6, - 'mdt': -6, 'mst': -7, 'mt': -7, - 'pdt': -7, 'pst': -8, 'pt': -8, - 'a': -1, 'n': 1, - 'm': -12, 'y': 12, + "ut": 0, + "gmt": 0, + "z": 0, + "adt": -3, + "ast": -4, + "at": -4, + "edt": -4, + "est": -5, + "et": -5, + "cdt": -5, + "cst": -6, + "ct": -6, + "mdt": -6, + "mst": -7, + "mt": -7, + "pdt": -7, + "pst": -8, + "pt": -8, + "a": -1, + "n": 1, + "m": -12, + "y": 12, } # W3 date and time format parser # http://www.w3.org/TR/NOTE-datetime @@ -47,57 +61,57 @@ timezonenames = { def _parse_date_w3dtf(datestr): if not datestr.strip(): return None - parts = datestr.lower().split('t') + parts = datestr.lower().split("t") if len(parts) == 1: # This may be a date only, or may be an MSSQL-style date parts = parts[0].split() if len(parts) == 1: # Treat this as a date only - parts.append('00:00:00z') + parts.append("00:00:00z") elif len(parts) > 2: return None - date = parts[0].split('-', 2) + date = parts[0].split("-", 2) if not date or len(date[0]) != 4: return None # Ensure that `date` has 3 elements. Using '1' sets the default # month to January and the default day to the 1st of the month. - date.extend(['1'] * (3 - len(date))) + date.extend(["1"] * (3 - len(date))) try: - year, month, day = [int(i) for i in date] + year, month, day = (int(i) for i in date) except ValueError: # `date` may have more than 3 elements or may contain # non-integer strings. return None - if parts[1].endswith('z'): + if parts[1].endswith("z"): parts[1] = parts[1][:-1] - parts.append('z') + parts.append("z") # Append the numeric timezone offset, if any, to parts. # If this is an MSSQL-style date then parts[2] already contains # the timezone information, so `append()` will not affect it. # Add 1 to each value so that if `find()` returns -1 it will be # treated as False. - loc = parts[1].find('-') + 1 or parts[1].find('+') + 1 or len(parts[1]) + 1 + loc = parts[1].find("-") + 1 or parts[1].find("+") + 1 or len(parts[1]) + 1 loc = loc - 1 parts.append(parts[1][loc:]) parts[1] = parts[1][:loc] - time = parts[1].split(':', 2) + time = parts[1].split(":", 2) # Ensure that time has 3 elements. Using '0' means that the # minutes and seconds, if missing, will default to 0. - time.extend(['0'] * (3 - len(time))) - if parts[2][:1] in ('-', '+'): + time.extend(["0"] * (3 - len(time))) + if parts[2][:1] in ("-", "+"): try: tzhour = int(parts[2][1:3]) tzmin = int(parts[2][4:]) except ValueError: return None - if parts[2].startswith('-'): + if parts[2].startswith("-"): tzhour = tzhour * -1 tzmin = tzmin * -1 else: tzhour = timezonenames.get(parts[2], 0) tzmin = 0 try: - hour, minute, second = [int(float(i)) for i in time] + hour, minute, second = (int(float(i)) for i in time) except ValueError: return None # Create the datetime object and timezone delta objects diff --git a/lib/feedparser/encodings.py b/lib/feedparser/encodings.py index 73251fc1..a7be68ae 100644 --- a/lib/feedparser/encodings.py +++ b/lib/feedparser/encodings.py @@ -1,5 +1,5 @@ # Character encoding routines -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -26,48 +26,53 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +from __future__ import annotations + import codecs +import io import re -import typing as t +import typing try: try: - import cchardet as chardet # type: ignore[import] + import cchardet as chardet # type: ignore[import] except ImportError: - import chardet # type: ignore[no-redef] + import chardet # type: ignore[no-redef] except ImportError: lazy_chardet_encoding = None else: + def lazy_chardet_encoding(data): - return chardet.detect(data)['encoding'] or '' + return chardet.detect(data)["encoding"] or "" + from .exceptions import ( CharacterEncodingOverride, CharacterEncodingUnknown, + FeedparserError, NonXMLContentType, ) - # Each marker represents some of the characters of the opening XML # processing instruction (' -RE_XML_DECLARATION = re.compile(r'^<\?xml[^>]*?>') +RE_XML_DECLARATION = re.compile(r"^<\?xml[^>]*?>") # Capture the value of the XML processing instruction's encoding attribute. # Example: -RE_XML_PI_ENCODING = re.compile(br'^<\?.*encoding=[\'"](.*?)[\'"].*\?>') +RE_XML_PI_ENCODING = re.compile(rb'^<\?.*encoding=[\'"](.*?)[\'"].*\?>') -def parse_content_type(line: str) -> t.Tuple[str, str]: +def parse_content_type(line: str) -> tuple[str, str]: """Parse an HTTP Content-Type header. The return value will be a tuple of strings: @@ -91,11 +96,10 @@ def parse_content_type(line: str) -> t.Tuple[str, str]: return mime_type, charset_value -def convert_to_utf8(http_headers, data, result): - """Detect and convert the character encoding to UTF-8. - - http_headers is a dictionary - data is a raw string (not Unicode)""" +def convert_to_utf8( + http_headers: dict[str, str], data: bytes, result: dict[str, typing.Any] +) -> bytes: + """Detect and convert the character encoding to UTF-8.""" # This is so much trickier than it sounds, it's not even funny. # According to RFC 3023 ('XML Media Types'), if the HTTP Content-Type @@ -134,12 +138,10 @@ def convert_to_utf8(http_headers, data, result): # Of course, none of this guarantees that we will be able to parse the # feed in the declared character encoding (assuming it was declared - # correctly, which many are not). iconv_codec can help a lot; - # you should definitely install it if you can. - # http://cjkpython.i18n.org/ + # correctly, which many are not). - bom_encoding = '' - xml_encoding = '' + bom_encoding = "" + xml_encoding = "" # Look at the first few bytes of the document to guess what # its encoding may be. We only need to decode enough of the @@ -149,50 +151,63 @@ def convert_to_utf8(http_headers, data, result): # http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info # Check for BOMs first. if data[:4] == codecs.BOM_UTF32_BE: - bom_encoding = 'utf-32be' + bom_encoding = "utf-32be" data = data[4:] elif data[:4] == codecs.BOM_UTF32_LE: - bom_encoding = 'utf-32le' + bom_encoding = "utf-32le" data = data[4:] elif data[:2] == codecs.BOM_UTF16_BE and data[2:4] != ZERO_BYTES: - bom_encoding = 'utf-16be' + bom_encoding = "utf-16be" data = data[2:] elif data[:2] == codecs.BOM_UTF16_LE and data[2:4] != ZERO_BYTES: - bom_encoding = 'utf-16le' + bom_encoding = "utf-16le" data = data[2:] elif data[:3] == codecs.BOM_UTF8: - bom_encoding = 'utf-8' + bom_encoding = "utf-8" data = data[3:] # Check for the characters '''' - if RE_XML_DECLARATION.search(data): - data = RE_XML_DECLARATION.sub(new_declaration, data) - else: - data = new_declaration + '\n' + data - data = data.encode('utf-8') - break + continue + + known_encoding = True + if not json: + # Update the encoding in the opening XML processing instruction. + new_declaration = """""" + if RE_XML_DECLARATION.search(text): + text = RE_XML_DECLARATION.sub(new_declaration, text) + else: + text = new_declaration + "\n" + text + data = text.encode("utf-8") + break + # if still no luck, give up if not known_encoding: error = CharacterEncodingUnknown( - 'document encoding unknown, I tried ' + - '%s, %s, utf-8, windows-1252, and iso-8859-2 but nothing worked' % - (rfc3023_encoding, xml_encoding)) - rfc3023_encoding = '' + "document encoding unknown, I tried " + + "%s, %s, utf-8, windows-1252, and iso-8859-2 but nothing worked" + % (rfc3023_encoding, xml_encoding) + ) + rfc3023_encoding = "" elif proposed_encoding != rfc3023_encoding: error = CharacterEncodingOverride( - 'document declared as %s, but parsed as %s' % - (rfc3023_encoding, proposed_encoding)) + "document declared as %s, but parsed as %s" + % (rfc3023_encoding, proposed_encoding) + ) rfc3023_encoding = proposed_encoding - result['content-type'] = http_content_type # for selecting the parser - result['encoding'] = rfc3023_encoding + result["content-type"] = http_content_type # for selecting the parser + result["encoding"] = rfc3023_encoding if error: - result['bozo'] = True - result['bozo_exception'] = error + result["bozo"] = True + result["bozo_exception"] = error return data + + +# How much to read from a binary file in order to detect encoding. +# In inital tests, 4k was enough for ~160 mostly-English feeds; +# 64k seems like a safe margin. +CONVERT_FILE_PREFIX_LEN = 2**16 + +# How much to read from a text file, and use as an utf-8 bytes prefix. +# Note that no encoding detection is needed in this case. +CONVERT_FILE_STR_PREFIX_LEN = 2**13 + +CONVERT_FILE_TEST_CHUNK_LEN = 2**16 + + +def convert_file_to_utf8( + http_headers, file, result, optimistic_encoding_detection=True +): + """Like convert_to_utf8(), but for a stream. + + Unlike convert_to_utf8(), do not read the entire file in memory; + instead, return a text stream that decodes it on the fly. + This should consume significantly less memory, + because it avoids (repeatedly) converting the entire file contents + from bytes to str and back. + + To detect the encoding, only a prefix of the file contents is used. + In rare cases, the wrong encoding may be detected for this prefix; + use optimistic_encoding_detection=False to use the entire file contents + (equivalent to a plain convert_to_utf8() call). + + Args: + http_headers (dict): The response headers. + file (IO[bytes] or IO[str]): A read()-able (binary) stream. + result (dict): The result dictionary. + optimistic_encoding_detection (bool): + If true, use only a prefix of the file content to detect encoding. + + Returns: + StreamFactory: a stream factory, with the detected encoding set, if any + + """ + # Currently, this wraps convert_to_utf8(), because the logic is simply + # too complicated to ensure it's re-implemented correctly for a stream. + # That said, it should be possible to change the implementation + # transparently (not sure it's worth it, though). + + # If file is a text stream, we don't need to detect encoding; + # we still need a bytes prefix to run functions on for side effects: + # convert_to_utf8() to sniff / set result['content-type'], and + # replace_doctype() to extract safe_entities. + + if isinstance(file.read(0), str): + prefix = file.read(CONVERT_FILE_STR_PREFIX_LEN).encode("utf-8") + prefix = convert_to_utf8(http_headers, prefix, result) + result["encoding"] = "utf-8" + return StreamFactory(prefix, file, "utf-8") + + if optimistic_encoding_detection: + prefix = convert_file_prefix_to_utf8(http_headers, file, result) + factory = StreamFactory(prefix, file, result.get("encoding")) + + # Before returning factory, ensure the entire file can be decoded; + # if it cannot, fall back to convert_to_utf8(). + # + # Not doing this means feedparser.parse() may raise UnicodeDecodeError + # instead of setting bozo_exception to CharacterEncodingOverride, + # breaking the 6.x API. + + try: + text_file = factory.get_text_file() + except MissingEncoding: + return factory + try: + # read in chunks to limit memory usage + while text_file.read(CONVERT_FILE_TEST_CHUNK_LEN): + pass + except UnicodeDecodeError: + # fall back to convert_to_utf8() + file = factory.get_binary_file() + else: + return factory + + # this shouldn't increase memory usage if file is BytesIO, + # since BytesIO does copy-on-write; https://bugs.python.org/issue22003 + data = convert_to_utf8(http_headers, file.read(), result) + + # note that data *is* the prefix + return StreamFactory(data, io.BytesIO(b""), result.get("encoding")) + + +def convert_file_prefix_to_utf8( + http_headers, + file: typing.IO[bytes], + result, + *, + prefix_len: int = CONVERT_FILE_PREFIX_LEN, + read_to_ascii_len: int = 2**8, +) -> bytes: + """Like convert_to_utf8(), but only use the prefix of a binary file. + + Set result like convert_to_utf8() would. + + Return the updated prefix, as bytes. + + """ + # This is complicated by convert_to_utf8() detecting the wrong encoding + # if we have only part of the bytes that make a code-point: + # + # '😀'.encode('utf-8') -> utf-8 + # '😀'.encode('utf-8')[:-1] -> windows-1252 + bozo + + prefix = file.read(prefix_len - 1) + + # reading up to after an ASCII byte increases + # the likelihood of being on a code point boundary + prefix += read_to_after_ascii_byte(file, read_to_ascii_len) + + # call convert_to_utf8() up to 4 times, + # to make sure we eventually land on a code point boundary + candidates = [] + for attempt in range(4): + byte = file.read(1) + + # we're at the end of the file, and the loop already ran once + if not byte and attempt != 0: + break + + prefix += byte + + fake_result: typing.Any = {} + converted_prefix = convert_to_utf8(http_headers, prefix, fake_result) + + # an encoding was detected successfully, keep it + if not fake_result.get("bozo"): + break + + candidates.append((file.tell(), converted_prefix, fake_result)) + + # no encoding was detected successfully, pick the "best" one + else: + + def key(candidate): + *_, result = candidate + + exc = result.get("bozo_exception") + exc_score = 0 + if isinstance(exc, NonXMLContentType): + exc_score = 20 + elif isinstance(exc, CharacterEncodingOverride): + exc_score = 10 + + return ( + exc_score, + # prefer utf- encodings to anything else + result.get("encoding").startswith("utf-"), + ) + + candidates.sort(key=key) + offset, converted_prefix, fake_result = candidates[-1] + + file.seek(offset) + + result.update(fake_result) + return converted_prefix + + +def read_to_after_ascii_byte(file: typing.IO[bytes], max_len: int) -> bytes: + offset = file.tell() + buffer = b"" + + for _ in range(max_len): + byte = file.read(1) + + # end of file, nothing to do + if not byte: + break + + buffer += byte + + # we stop after a ASCII character + if byte < b"\x80": + break + + # couldn't find an ASCII character, reset the file to the original offset + else: + file.seek(offset) + return b"" + + return buffer + + +class MissingEncoding(io.UnsupportedOperation): + pass + + +class StreamFactory: + + """Decode on the fly a binary stream that *may* have a known encoding. + + If the underlying stream is seekable, it is possible to call + the get_{text,binary}_file() methods more than once. + + """ + + def __init__(self, prefix: bytes, file, encoding=None): + self.prefix = prefix + self.file = ResetFileWrapper(file) + self.encoding = encoding + self.should_reset = False + + def get_text_file(self, fallback_encoding=None, errors="strict"): + encoding = self.encoding or fallback_encoding + if encoding is None: + raise MissingEncoding("cannot create text stream without encoding") + + if isinstance(self.file.read(0), str): + file = PrefixFileWrapper(self.prefix.decode(encoding), self.file) + else: + file = PrefixFileWrapper( + self.prefix.decode("utf-8", errors), + codecs.getreader(encoding)(self.file, errors), + ) + + self.reset() + return file + + def get_binary_file(self): + if isinstance(self.file.read(0), str): + raise io.UnsupportedOperation( + "underlying stream is text, not binary" + ) from None + + file = PrefixFileWrapper(self.prefix, self.file) + + self.reset() + return file + + def get_file(self): + try: + return self.get_text_file() + except MissingEncoding: + return self.get_binary_file() + + def reset(self): + if self.should_reset: + self.file.reset() + self.should_reset = True + + +class ResetFileWrapper: + """Given a seekable file, allow reading its content again + (from the current position) by calling reset(). + + """ + + def __init__(self, file): + self.file = file + try: + self.file_initial_offset = file.tell() + except OSError: + self.file_initial_offset = None + + def read(self, size=-1): + return self.file.read(size) + + def reset(self): + # raises io.UnsupportedOperation if the underlying stream is not seekable + self.file.seek(self.file_initial_offset) + + +class PrefixFileWrapper: + """Stitch a (possibly modified) prefix and a file into a new file object. + + >>> file = io.StringIO('abcdef') + >>> file.read(2) + 'ab' + >>> wrapped = PrefixFileWrapper(file.read(2).upper(), file) + >>> wrapped.read() + 'CDef' + + """ + + def __init__(self, prefix, file): + self.prefix = prefix + self.file = file + self.offset = 0 + + def read(self, size=-1): + buffer = self.file.read(0) + + if self.offset < len(self.prefix): + if size < 0: + chunk = self.prefix + else: + chunk = self.prefix[self.offset : self.offset + size] + size -= len(chunk) + buffer += chunk + self.offset += len(chunk) + + while True: + chunk = self.file.read(size) + if not chunk: + break + buffer += chunk + self.offset += len(chunk) + + if size <= 0: + break + + size -= len(chunk) + + return buffer + + def close(self): + # do not touch the underlying stream + pass diff --git a/lib/feedparser/exceptions.py b/lib/feedparser/exceptions.py index 0ddb0024..49ca2858 100644 --- a/lib/feedparser/exceptions.py +++ b/lib/feedparser/exceptions.py @@ -1,5 +1,5 @@ # Exceptions used throughout feedparser -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -27,11 +27,11 @@ # POSSIBILITY OF SUCH DAMAGE. __all__ = [ - 'FeedparserError', - 'CharacterEncodingOverride', - 'CharacterEncodingUnknown', - 'NonXMLContentType', - 'UndeclaredNamespace', + "FeedparserError", + "CharacterEncodingOverride", + "CharacterEncodingUnknown", + "NonXMLContentType", + "UndeclaredNamespace", ] diff --git a/lib/feedparser/html.py b/lib/feedparser/html.py index 48ddb924..bbb90389 100644 --- a/lib/feedparser/html.py +++ b/lib/feedparser/html.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -28,36 +28,49 @@ import html.entities import re -import sgmllib3k as sgmllib +# These items must all be imported into this module due to .__code__ replacements. +from .sgml import ( # noqa: F401 + attrfind, + charref, + endbracket, + entityref, + incomplete, + interesting, + sgmllib, + shorttag, + shorttagopen, + starttagopen, + tagfind, +) _cp1252 = { - 128: '\u20ac', # euro sign - 130: '\u201a', # single low-9 quotation mark - 131: '\u0192', # latin small letter f with hook - 132: '\u201e', # double low-9 quotation mark - 133: '\u2026', # horizontal ellipsis - 134: '\u2020', # dagger - 135: '\u2021', # double dagger - 136: '\u02c6', # modifier letter circumflex accent - 137: '\u2030', # per mille sign - 138: '\u0160', # latin capital letter s with caron - 139: '\u2039', # single left-pointing angle quotation mark - 140: '\u0152', # latin capital ligature oe - 142: '\u017d', # latin capital letter z with caron - 145: '\u2018', # left single quotation mark - 146: '\u2019', # right single quotation mark - 147: '\u201c', # left double quotation mark - 148: '\u201d', # right double quotation mark - 149: '\u2022', # bullet - 150: '\u2013', # en dash - 151: '\u2014', # em dash - 152: '\u02dc', # small tilde - 153: '\u2122', # trade mark sign - 154: '\u0161', # latin small letter s with caron - 155: '\u203a', # single right-pointing angle quotation mark - 156: '\u0153', # latin small ligature oe - 158: '\u017e', # latin small letter z with caron - 159: '\u0178', # latin capital letter y with diaeresis + 128: "\u20ac", # euro sign + 130: "\u201a", # single low-9 quotation mark + 131: "\u0192", # latin small letter f with hook + 132: "\u201e", # double low-9 quotation mark + 133: "\u2026", # horizontal ellipsis + 134: "\u2020", # dagger + 135: "\u2021", # double dagger + 136: "\u02c6", # modifier letter circumflex accent + 137: "\u2030", # per mille sign + 138: "\u0160", # latin capital letter s with caron + 139: "\u2039", # single left-pointing angle quotation mark + 140: "\u0152", # latin capital ligature oe + 142: "\u017d", # latin capital letter z with caron + 145: "\u2018", # left single quotation mark + 146: "\u2019", # right single quotation mark + 147: "\u201c", # left double quotation mark + 148: "\u201d", # right double quotation mark + 149: "\u2022", # bullet + 150: "\u2013", # en dash + 151: "\u2014", # em dash + 152: "\u02dc", # small tilde + 153: "\u2122", # trade mark sign + 154: "\u0161", # latin small letter s with caron + 155: "\u203a", # single right-pointing angle quotation mark + 156: "\u0153", # latin small ligature oe + 158: "\u017e", # latin small letter z with caron + 159: "\u0178", # latin capital letter y with diaeresis } @@ -65,28 +78,28 @@ class BaseHTMLProcessor(sgmllib.SGMLParser): special = re.compile("""[<>'"]""") bare_ampersand = re.compile(r"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") elements_no_end_tag = { - 'area', - 'base', - 'basefont', - 'br', - 'col', - 'command', - 'embed', - 'frame', - 'hr', - 'img', - 'input', - 'isindex', - 'keygen', - 'link', - 'meta', - 'param', - 'source', - 'track', - 'wbr', + "area", + "base", + "basefont", + "br", + "col", + "command", + "embed", + "frame", + "hr", + "img", + "input", + "isindex", + "keygen", + "link", + "meta", + "param", + "source", + "track", + "wbr", } - def __init__(self, encoding=None, _type='application/xhtml+xml'): + def __init__(self, encoding=None, _type="application/xhtml+xml"): if encoding: self.encoding = encoding self._type = _type @@ -105,9 +118,9 @@ class BaseHTMLProcessor(sgmllib.SGMLParser): tag = match.group(1) if tag in self.elements_no_end_tag: - return '<' + tag + ' />' + return "<" + tag + " />" else: - return '<' + tag + '>' + return "<" + tag + ">" # By declaring these methods and overriding their compiled code # with the code from sgmllib, the original code will execute in @@ -128,8 +141,8 @@ class BaseHTMLProcessor(sgmllib.SGMLParser): def parse_starttag(self, i): j = self.__parse_starttag(i) - if self._type == 'application/xhtml+xml': - if j > 2 and self.rawdata[j-2:j] == '/>': + if self._type == "application/xhtml+xml": + if j > 2 and self.rawdata[j - 2 : j] == "/>": self.unknown_endtag(self.lasttag) return j @@ -139,10 +152,10 @@ class BaseHTMLProcessor(sgmllib.SGMLParser): :rtype: None """ - data = re.sub(r'\s]+?)\s*/>', self._shorttag_replace, data) - data = data.replace(''', "'") - data = data.replace('"', '"') + data = re.sub(r"\s]+?)\s*/>", self._shorttag_replace, data) + data = data.replace("'", "'") + data = data.replace(""", '"') super().feed(data) super().close() @@ -160,8 +173,7 @@ class BaseHTMLProcessor(sgmllib.SGMLParser): # *attrs* into a dictionary, then convert it back to a list. attrs_d = {k.lower(): v for k, v in attrs} attrs = [ - (k, k in ('rel', 'type') and v.lower() or v) - for k, v in attrs_d.items() + (k, k in ("rel", "type") and v.lower() or v) for k, v in attrs_d.items() ] attrs.sort() return attrs @@ -177,22 +189,19 @@ class BaseHTMLProcessor(sgmllib.SGMLParser): # attrs is a list of (attr, value) tuples # e.g. for
, tag='pre', attrs=[('class', 'screen')]
         uattrs = []
-        strattrs = ''
+        strattrs = ""
         if attrs:
             for key, value in attrs:
-                value = value.replace('>', '>')
-                value = value.replace('<', '<')
-                value = value.replace('"', '"')
+                value = value.replace(">", ">")
+                value = value.replace("<", "<")
+                value = value.replace('"', """)
                 value = self.bare_ampersand.sub("&", value)
                 uattrs.append((key, value))
-            strattrs = ''.join(
-                ' %s="%s"' % (key, value)
-                for key, value in uattrs
-            )
+            strattrs = "".join(f' {key}="{value}"' for key, value in uattrs)
         if tag in self.elements_no_end_tag:
-            self.pieces.append('<%s%s />' % (tag, strattrs))
+            self.pieces.append(f"<{tag}{strattrs} />")
         else:
-            self.pieces.append('<%s%s>' % (tag, strattrs))
+            self.pieces.append(f"<{tag}{strattrs}>")
 
     def unknown_endtag(self, tag):
         """
@@ -214,15 +223,15 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
         # Called for each character reference, e.g. ' ' will extract '160'
         # Reconstruct the original character reference.
         ref = ref.lower()
-        if ref.startswith('x'):
+        if ref.startswith("x"):
             value = int(ref[1:], 16)
         else:
             value = int(ref)
 
         if value in _cp1252:
-            self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:])
+            self.pieces.append("&#%s;" % hex(ord(_cp1252[value]))[1:])
         else:
-            self.pieces.append('&#%s;' % ref)
+            self.pieces.append("&#%s;" % ref)
 
     def handle_entityref(self, ref):
         """
@@ -232,10 +241,10 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
 
         # Called for each entity reference, e.g. '©' will extract 'copy'
         # Reconstruct the original entity reference.
-        if ref in html.entities.name2codepoint or ref == 'apos':
-            self.pieces.append('&%s;' % ref)
+        if ref in html.entities.name2codepoint or ref == "apos":
+            self.pieces.append("&%s;" % ref)
         else:
-            self.pieces.append('&%s' % ref)
+            self.pieces.append("&%s" % ref)
 
     def handle_data(self, text):
         """
@@ -256,7 +265,7 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
 
         # Called for HTML comments, e.g. 
         # Reconstruct the original comment.
-        self.pieces.append('' % text)
+        self.pieces.append("" % text)
 
     def handle_pi(self, text):
         """
@@ -266,7 +275,7 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
 
         # Called for each processing instruction, e.g. 
         # Reconstruct original processing instruction.
-        self.pieces.append('' % text)
+        self.pieces.append("" % text)
 
     def handle_decl(self, text):
         """
@@ -278,9 +287,9 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
         # 
         # Reconstruct original DOCTYPE
-        self.pieces.append('' % text)
+        self.pieces.append("" % text)
 
-    _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
+    _new_declname_match = re.compile(r"[a-zA-Z][-_.a-zA-Z0-9:]*\s*").match
 
     def _scan_name(self, i, declstartpos):
         """
@@ -311,7 +320,7 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
         :rtype: str
         """
 
-        return '&#%s;' % name
+        return "&#%s;" % name
 
     def convert_entityref(self, name):
         """
@@ -319,7 +328,7 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
         :rtype: str
         """
 
-        return '&%s;' % name
+        return "&%s;" % name
 
     def output(self):
         """Return processed HTML as a single string.
@@ -327,7 +336,7 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
         :rtype: str
         """
 
-        return ''.join(self.pieces)
+        return "".join(self.pieces)
 
     def parse_declaration(self, i):
         """
@@ -339,5 +348,5 @@ class BaseHTMLProcessor(sgmllib.SGMLParser):
             return sgmllib.SGMLParser.parse_declaration(self, i)
         except (AssertionError, sgmllib.SGMLParseError):
             # Escape the doctype declaration and continue parsing.
-            self.handle_data('<')
-            return i+1
+            self.handle_data("<")
+            return i + 1
diff --git a/lib/feedparser/http.py b/lib/feedparser/http.py
index a7fee361..d94ade2e 100644
--- a/lib/feedparser/http.py
+++ b/lib/feedparser/http.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2022 Kurt McKee 
+# Copyright 2010-2023 Kurt McKee 
 # Copyright 2002-2008 Mark Pilgrim
 # All rights reserved.
 #
@@ -25,203 +25,54 @@
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 
-import base64
-import datetime
-import gzip
-import io
-import re
-import struct
-import urllib.parse
-import urllib.request
-import zlib
+from __future__ import annotations
+
+import typing
+
+import requests
 
 from .datetimes import _parse_date
-from .urls import convert_to_idn
+
+# HTTP "Accept" header to send to servers when downloading feeds.
+ACCEPT_HEADER: str = (
+    "application/atom+xml"
+    ",application/rdf+xml"
+    ",application/rss+xml"
+    ",application/x-netcdf"
+    ",application/xml"
+    ";q=0.9,text/xml"
+    ";q=0.2,*/*"
+    ";q=0.1"
+)
 
 
-# HTTP "Accept" header to send to servers when downloading feeds.  If you don't
-# want to send an Accept header, set this to None.
-ACCEPT_HEADER = "application/atom+xml,application/rdf+xml,application/rss+xml,application/x-netcdf,application/xml;q=0.9,text/xml;q=0.2,*/*;q=0.1"
+def get(url: str, result: dict[str, typing.Any]) -> bytes:
+    from . import USER_AGENT
 
+    agent = USER_AGENT
 
-class URLHandler(urllib.request.HTTPDigestAuthHandler, urllib.request.HTTPRedirectHandler, urllib.request.HTTPDefaultErrorHandler):
-    def http_error_default(self, req, fp, code, msg, headers):
-        # The default implementation just raises HTTPError.
-        # Forget that.
-        fp.status = code
-        return fp
+    try:
+        response = requests.get(
+            url,
+            headers={"User-Agent": agent, "Accept": ACCEPT_HEADER},
+            timeout=10,
+        )
+    except requests.RequestException as exception:
+        result["bozo"] = True
+        result["bozo_exception"] = exception
+        return b""
 
-    def http_error_301(self, req, fp, code, msg, hdrs):
-        result = urllib.request.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, hdrs)
-        if not result:
-            return fp
-        result.status = code
-        result.newurl = result.geturl()
-        return result
-
-    # The default implementations in urllib.request.HTTPRedirectHandler
-    # are identical, so hardcoding a http_error_301 call above
-    # won't affect anything
-    http_error_300 = http_error_301
-    http_error_302 = http_error_301
-    http_error_303 = http_error_301
-    http_error_307 = http_error_301
-
-    def http_error_401(self, req, fp, code, msg, headers):
-        # Check if
-        # - server requires digest auth, AND
-        # - we tried (unsuccessfully) with basic auth, AND
-        # If all conditions hold, parse authentication information
-        # out of the Authorization header we sent the first time
-        # (for the username and password) and the WWW-Authenticate
-        # header the server sent back (for the realm) and retry
-        # the request with the appropriate digest auth headers instead.
-        # This evil genius hack has been brought to you by Aaron Swartz.
-        host = urllib.parse.urlparse(req.get_full_url())[1]
-        if 'Authorization' not in req.headers or 'WWW-Authenticate' not in headers:
-            return self.http_error_default(req, fp, code, msg, headers)
-        auth = base64.decodebytes(req.headers['Authorization'].split(' ')[1].encode()).decode()
-        user, passw = auth.split(':')
-        realm = re.findall('realm="([^"]*)"', headers['WWW-Authenticate'])[0]
-        self.add_password(realm, host, user, passw)
-        retry = self.http_error_auth_reqed('www-authenticate', host, req, headers)
-        self.reset_retry_count()
-        return retry
-
-
-def _build_urllib2_request(url, agent, accept_header, etag, modified, referrer, auth, request_headers):
-    request = urllib.request.Request(url)
-    request.add_header('User-Agent', agent)
-    if etag:
-        request.add_header('If-None-Match', etag)
-    if isinstance(modified, str):
-        modified = _parse_date(modified)
-    elif isinstance(modified, datetime.datetime):
-        modified = modified.utctimetuple()
-    if modified:
-        # format into an RFC 1123-compliant timestamp. We can't use
-        # time.strftime() since the %a and %b directives can be affected
-        # by the current locale, but RFC 2616 states that dates must be
-        # in English.
-        short_weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
-        request.add_header('If-Modified-Since', '%s, %02d %s %04d %02d:%02d:%02d GMT' % (short_weekdays[modified[6]], modified[2], months[modified[1] - 1], modified[0], modified[3], modified[4], modified[5]))
-    if referrer:
-        request.add_header('Referer', referrer)
-    request.add_header('Accept-encoding', 'gzip, deflate')
-    if auth:
-        request.add_header('Authorization', 'Basic %s' % auth)
-    if accept_header:
-        request.add_header('Accept', accept_header)
-    # use this for whatever -- cookies, special headers, etc
-    # [('Cookie','Something'),('x-special-header','Another Value')]
-    for header_name, header_value in request_headers.items():
-        request.add_header(header_name, header_value)
-    request.add_header('A-IM', 'feed')  # RFC 3229 support
-    return request
-
-
-def get(url, etag=None, modified=None, agent=None, referrer=None, handlers=None, request_headers=None, result=None):
-    if handlers is None:
-        handlers = []
-    elif not isinstance(handlers, list):
-        handlers = [handlers]
-    if request_headers is None:
-        request_headers = {}
-
-    # Deal with the feed URI scheme
-    if url.startswith('feed:http'):
-        url = url[5:]
-    elif url.startswith('feed:'):
-        url = 'http:' + url[5:]
-    if not agent:
-        from . import USER_AGENT
-        agent = USER_AGENT
-    # Test for inline user:password credentials for HTTP basic auth
-    auth = None
-    if not url.startswith('ftp:'):
-        url_pieces = urllib.parse.urlparse(url)
-        if url_pieces.username:
-            new_pieces = list(url_pieces)
-            new_pieces[1] = url_pieces.hostname
-            if url_pieces.port:
-                new_pieces[1] = f'{url_pieces.hostname}:{url_pieces.port}'
-            url = urllib.parse.urlunparse(new_pieces)
-            auth = base64.standard_b64encode(f'{url_pieces.username}:{url_pieces.password}'.encode()).decode()
-
-    # iri support
-    if not isinstance(url, bytes):
-        url = convert_to_idn(url)
-
-    # Prevent UnicodeEncodeErrors caused by Unicode characters in the path.
-    bits = []
-    for c in url:
-        try:
-            c.encode('ascii')
-        except UnicodeEncodeError:
-            bits.append(urllib.parse.quote(c))
-        else:
-            bits.append(c)
-    url = ''.join(bits)
-
-    # try to open with urllib2 (to use optional headers)
-    request = _build_urllib2_request(url, agent, ACCEPT_HEADER, etag, modified, referrer, auth, request_headers)
-    opener = urllib.request.build_opener(*tuple(handlers + [URLHandler()]))
-    opener.addheaders = []  # RMK - must clear so we only send our custom User-Agent
-    f = opener.open(request)
-    data = f.read()
-    f.close()
-
-    # lowercase all of the HTTP headers for comparisons per RFC 2616
-    result['headers'] = {k.lower(): v for k, v in f.headers.items()}
-
-    # if feed is gzip-compressed, decompress it
-    if data and 'gzip' in result['headers'].get('content-encoding', ''):
-        try:
-            data = gzip.GzipFile(fileobj=io.BytesIO(data)).read()
-        except (EOFError, IOError, struct.error) as e:
-            # IOError can occur if the gzip header is bad.
-            # struct.error can occur if the data is damaged.
-            result['bozo'] = True
-            result['bozo_exception'] = e
-            if isinstance(e, struct.error):
-                # A gzip header was found but the data is corrupt.
-                # Ideally, we should re-request the feed without the
-                # 'Accept-encoding: gzip' header, but we don't.
-                data = None
-    elif data and 'deflate' in result['headers'].get('content-encoding', ''):
-        try:
-            data = zlib.decompress(data)
-        except zlib.error:
-            try:
-                # The data may have no headers and no checksum.
-                data = zlib.decompress(data, -15)
-            except zlib.error as e:
-                result['bozo'] = True
-                result['bozo_exception'] = e
+    # Lowercase the HTTP header keys for comparisons per RFC 2616.
+    result["headers"] = {k.lower(): v for k, v in response.headers.items()}
 
     # save HTTP headers
-    if 'etag' in result['headers']:
-        etag = result['headers'].get('etag', '')
-        if isinstance(etag, bytes):
-            etag = etag.decode('utf-8', 'ignore')
-        if etag:
-            result['etag'] = etag
-    if 'last-modified' in result['headers']:
-        modified = result['headers'].get('last-modified', '')
+    if "etag" in result["headers"]:
+        result["etag"] = result["headers"]["etag"]
+    if "last-modified" in result["headers"]:
+        modified = result["headers"]["last-modified"]
         if modified:
-            result['modified'] = modified
-            result['modified_parsed'] = _parse_date(modified)
-    if isinstance(f.url, bytes):
-        result['href'] = f.url.decode('utf-8', 'ignore')
-    else:
-        result['href'] = f.url
-    result['status'] = getattr(f, 'status', None) or 200
-
-    # Stop processing if the server sent HTTP 304 Not Modified.
-    if getattr(f, 'code', 0) == 304:
-        result['version'] = ''
-        result['debug_message'] = 'The feed has not changed since you last checked, ' + \
-            'so the server sent no data.  This is a feature, not a bug!'
-
-    return data
+            result["modified"] = modified
+            result["modified_parsed"] = _parse_date(modified)
+    result["href"] = response.url
+    result["status"] = response.status_code
+    return response.content
diff --git a/lib/feedparser/mixin.py b/lib/feedparser/mixin.py
index 8309e723..4c6d4e9a 100644
--- a/lib/feedparser/mixin.py
+++ b/lib/feedparser/mixin.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2022 Kurt McKee 
+# Copyright 2010-2023 Kurt McKee 
 # Copyright 2002-2008 Mark Pilgrim
 # All rights reserved.
 #
@@ -30,137 +30,144 @@ import binascii
 import copy
 import html.entities
 import re
-from typing import Dict
 import xml.sax.saxutils
+from typing import Dict
 
 from .html import _cp1252
 from .namespaces import _base, cc, dc, georss, itunes, mediarss, psc
-from .sanitizer import sanitize_html, HTMLSanitizer
-from .util import FeedParserDict
+from .sanitizer import HTMLSanitizer, sanitize_html
 from .urls import _urljoin, make_safe_absolute_uri, resolve_relative_uris
+from .util import FeedParserDict
+
+email_pattern = re.compile(
+    r"(([a-zA-Z0-9_.+-]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)"
+    r"|(([a-zA-Z0-9-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(]?))"
+    r"(\?subject=\S+)?"
+)
 
 
 class XMLParserMixin(
-        _base.Namespace,
-        cc.Namespace,
-        dc.Namespace,
-        georss.Namespace,
-        itunes.Namespace,
-        mediarss.Namespace,
-        psc.Namespace,
+    _base.Namespace,
+    cc.Namespace,
+    dc.Namespace,
+    georss.Namespace,
+    itunes.Namespace,
+    mediarss.Namespace,
+    psc.Namespace,
 ):
     namespaces = {
-        '': '',
-        'http://backend.userland.com/rss': '',
-        'http://blogs.law.harvard.edu/tech/rss': '',
-        'http://purl.org/rss/1.0/': '',
-        'http://my.netscape.com/rdf/simple/0.9/': '',
-        'http://example.com/newformat#': '',
-        'http://example.com/necho': '',
-        'http://purl.org/echo/': '',
-        'uri/of/echo/namespace#': '',
-        'http://purl.org/pie/': '',
-        'http://purl.org/atom/ns#': '',
-        'http://www.w3.org/2005/Atom': '',
-        'http://purl.org/rss/1.0/modules/rss091#': '',
-
-        'http://webns.net/mvcb/':                                'admin',
-        'http://purl.org/rss/1.0/modules/aggregation/':          'ag',
-        'http://purl.org/rss/1.0/modules/annotate/':             'annotate',
-        'http://media.tangent.org/rss/1.0/':                     'audio',
-        'http://backend.userland.com/blogChannelModule':         'blogChannel',
-        'http://creativecommons.org/ns#license':                 'cc',
-        'http://web.resource.org/cc/':                           'cc',
-        'http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html': 'creativeCommons',
-        'http://backend.userland.com/creativeCommonsRssModule':  'creativeCommons',
-        'http://purl.org/rss/1.0/modules/company':               'co',
-        'http://purl.org/rss/1.0/modules/content/':              'content',
-        'http://my.theinfo.org/changed/1.0/rss/':                'cp',
-        'http://purl.org/dc/elements/1.1/':                      'dc',
-        'http://purl.org/dc/terms/':                             'dcterms',
-        'http://purl.org/rss/1.0/modules/email/':                'email',
-        'http://purl.org/rss/1.0/modules/event/':                'ev',
-        'http://rssnamespace.org/feedburner/ext/1.0':            'feedburner',
-        'http://freshmeat.net/rss/fm/':                          'fm',
-        'http://xmlns.com/foaf/0.1/':                            'foaf',
-        'http://www.w3.org/2003/01/geo/wgs84_pos#':              'geo',
-        'http://www.georss.org/georss':                          'georss',
-        'http://www.opengis.net/gml':                            'gml',
-        'http://postneo.com/icbm/':                              'icbm',
-        'http://purl.org/rss/1.0/modules/image/':                'image',
-        'http://www.itunes.com/DTDs/PodCast-1.0.dtd':            'itunes',
-        'http://example.com/DTDs/PodCast-1.0.dtd':               'itunes',
-        'http://purl.org/rss/1.0/modules/link/':                 'l',
-        'http://search.yahoo.com/mrss':                          'media',
+        "": "",
+        "http://backend.userland.com/rss": "",
+        "http://blogs.law.harvard.edu/tech/rss": "",
+        "http://purl.org/rss/1.0/": "",
+        "http://my.netscape.com/rdf/simple/0.9/": "",
+        "http://example.com/newformat#": "",
+        "http://example.com/necho": "",
+        "http://purl.org/echo/": "",
+        "uri/of/echo/namespace#": "",
+        "http://purl.org/pie/": "",
+        "http://purl.org/atom/ns#": "",
+        "http://www.w3.org/2005/Atom": "",
+        "http://purl.org/rss/1.0/modules/rss091#": "",
+        "http://webns.net/mvcb/": "admin",
+        "http://purl.org/rss/1.0/modules/aggregation/": "ag",
+        "http://purl.org/rss/1.0/modules/annotate/": "annotate",
+        "http://media.tangent.org/rss/1.0/": "audio",
+        "http://backend.userland.com/blogChannelModule": "blogChannel",
+        "http://creativecommons.org/ns#license": "cc",
+        "http://web.resource.org/cc/": "cc",
+        "http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html": (
+            "creativeCommons"
+        ),
+        "http://backend.userland.com/creativeCommonsRssModule": "creativeCommons",
+        "http://purl.org/rss/1.0/modules/company": "co",
+        "http://purl.org/rss/1.0/modules/content/": "content",
+        "http://my.theinfo.org/changed/1.0/rss/": "cp",
+        "http://purl.org/dc/elements/1.1/": "dc",
+        "http://purl.org/dc/terms/": "dcterms",
+        "http://purl.org/rss/1.0/modules/email/": "email",
+        "http://purl.org/rss/1.0/modules/event/": "ev",
+        "http://rssnamespace.org/feedburner/ext/1.0": "feedburner",
+        "http://freshmeat.net/rss/fm/": "fm",
+        "http://xmlns.com/foaf/0.1/": "foaf",
+        "http://www.w3.org/2003/01/geo/wgs84_pos#": "geo",
+        "http://www.georss.org/georss": "georss",
+        "http://www.opengis.net/gml": "gml",
+        "http://postneo.com/icbm/": "icbm",
+        "http://purl.org/rss/1.0/modules/image/": "image",
+        "http://www.itunes.com/DTDs/PodCast-1.0.dtd": "itunes",
+        "http://example.com/DTDs/PodCast-1.0.dtd": "itunes",
+        "http://purl.org/rss/1.0/modules/link/": "l",
+        "http://search.yahoo.com/mrss": "media",
         # Version 1.1.2 of the Media RSS spec added the trailing slash on the namespace
-        'http://search.yahoo.com/mrss/':                         'media',
-        'http://madskills.com/public/xml/rss/module/pingback/':  'pingback',
-        'http://prismstandard.org/namespaces/1.2/basic/':        'prism',
-        'http://www.w3.org/1999/02/22-rdf-syntax-ns#':           'rdf',
-        'http://www.w3.org/2000/01/rdf-schema#':                 'rdfs',
-        'http://purl.org/rss/1.0/modules/reference/':            'ref',
-        'http://purl.org/rss/1.0/modules/richequiv/':            'reqv',
-        'http://purl.org/rss/1.0/modules/search/':               'search',
-        'http://purl.org/rss/1.0/modules/slash/':                'slash',
-        'http://schemas.xmlsoap.org/soap/envelope/':             'soap',
-        'http://purl.org/rss/1.0/modules/servicestatus/':        'ss',
-        'http://hacks.benhammersley.com/rss/streaming/':         'str',
-        'http://purl.org/rss/1.0/modules/subscription/':         'sub',
-        'http://purl.org/rss/1.0/modules/syndication/':          'sy',
-        'http://schemas.pocketsoap.com/rss/myDescModule/':       'szf',
-        'http://purl.org/rss/1.0/modules/taxonomy/':             'taxo',
-        'http://purl.org/rss/1.0/modules/threading/':            'thr',
-        'http://purl.org/rss/1.0/modules/textinput/':            'ti',
-        'http://madskills.com/public/xml/rss/module/trackback/': 'trackback',
-        'http://wellformedweb.org/commentAPI/':                  'wfw',
-        'http://purl.org/rss/1.0/modules/wiki/':                 'wiki',
-        'http://www.w3.org/1999/xhtml':                          'xhtml',
-        'http://www.w3.org/1999/xlink':                          'xlink',
-        'http://www.w3.org/XML/1998/namespace':                  'xml',
-        'http://podlove.org/simple-chapters':                    'psc',
+        "http://search.yahoo.com/mrss/": "media",
+        "http://madskills.com/public/xml/rss/module/pingback/": "pingback",
+        "http://prismstandard.org/namespaces/1.2/basic/": "prism",
+        "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf",
+        "http://www.w3.org/2000/01/rdf-schema#": "rdfs",
+        "http://purl.org/rss/1.0/modules/reference/": "ref",
+        "http://purl.org/rss/1.0/modules/richequiv/": "reqv",
+        "http://purl.org/rss/1.0/modules/search/": "search",
+        "http://purl.org/rss/1.0/modules/slash/": "slash",
+        "http://schemas.xmlsoap.org/soap/envelope/": "soap",
+        "http://purl.org/rss/1.0/modules/servicestatus/": "ss",
+        "http://hacks.benhammersley.com/rss/streaming/": "str",
+        "http://purl.org/rss/1.0/modules/subscription/": "sub",
+        "http://purl.org/rss/1.0/modules/syndication/": "sy",
+        "http://schemas.pocketsoap.com/rss/myDescModule/": "szf",
+        "http://purl.org/rss/1.0/modules/taxonomy/": "taxo",
+        "http://purl.org/rss/1.0/modules/threading/": "thr",
+        "http://purl.org/rss/1.0/modules/textinput/": "ti",
+        "http://madskills.com/public/xml/rss/module/trackback/": "trackback",
+        "http://wellformedweb.org/commentAPI/": "wfw",
+        "http://purl.org/rss/1.0/modules/wiki/": "wiki",
+        "http://www.w3.org/1999/xhtml": "xhtml",
+        "http://www.w3.org/1999/xlink": "xlink",
+        "http://www.w3.org/XML/1998/namespace": "xml",
+        "http://podlove.org/simple-chapters": "psc",
     }
     _matchnamespaces: Dict[str, str] = {}
 
     can_be_relative_uri = {
-        'comments',
-        'docs',
-        'href',
-        'icon',
-        'id',
-        'link',
-        'logo',
-        'url',
-        'wfw_comment',
-        'wfw_commentrss',
+        "comments",
+        "docs",
+        "href",
+        "icon",
+        "id",
+        "link",
+        "logo",
+        "url",
+        "wfw_comment",
+        "wfw_commentrss",
     }
 
     can_contain_relative_uris = {
-        'content',
-        'copyright',
-        'description',
-        'info',
-        'rights',
-        'subtitle',
-        'summary',
-        'tagline',
-        'title',
+        "content",
+        "copyright",
+        "description",
+        "info",
+        "rights",
+        "subtitle",
+        "summary",
+        "tagline",
+        "title",
     }
 
     can_contain_dangerous_markup = {
-        'content',
-        'copyright',
-        'description',
-        'info',
-        'rights',
-        'subtitle',
-        'summary',
-        'tagline',
-        'title',
+        "content",
+        "copyright",
+        "description",
+        "info",
+        "rights",
+        "subtitle",
+        "summary",
+        "tagline",
+        "title",
     }
 
     html_types = {
-        'application/xhtml+xml',
-        'text/html',
+        "application/xhtml+xml",
+        "text/html",
     }
 
     def __init__(self):
@@ -169,7 +176,7 @@ class XMLParserMixin(
                 self._matchnamespaces[k.lower()] = v
         self.feeddata = FeedParserDict()  # feed-level data
         self.entries = []  # list of entry-level data
-        self.version = ''  # feed type/version, see SUPPORTED_VERSIONS
+        self.version = ""  # feed type/version, see SUPPORTED_VERSIONS
         self.namespaces_in_use = {}  # dictionary of namespaces defined by the feed
         self.resolve_relative_uris = False
         self.sanitize_html = False
@@ -198,7 +205,7 @@ class XMLParserMixin(
         self.depth = 0
         self.hasContent = 0
         if self.lang:
-            self.feeddata['language'] = self.lang.replace('_', '-')
+            self.feeddata["language"] = self.lang.replace("_", "-")
 
         # A map of the following form:
         #     {
@@ -208,7 +215,7 @@ class XMLParserMixin(
         #         },
         #     }
         self.property_depth_map = {}
-        super(XMLParserMixin, self).__init__()
+        super().__init__()
 
     def _normalize_attributes(self, kv):
         raise NotImplementedError
@@ -222,72 +229,80 @@ class XMLParserMixin(
 
         # track xml:base and xml:lang
         attrs_d = dict(attrs)
-        baseuri = attrs_d.get('xml:base', attrs_d.get('base')) or self.baseuri
+        baseuri = attrs_d.get("xml:base", attrs_d.get("base")) or self.baseuri
         if isinstance(baseuri, bytes):
-            baseuri = baseuri.decode(self.encoding, 'ignore')
+            baseuri = baseuri.decode(self.encoding, "ignore")
         # ensure that self.baseuri is always an absolute URI that
         # uses a whitelisted URI scheme (e.g. not `javscript:`)
         if self.baseuri:
             self.baseuri = make_safe_absolute_uri(self.baseuri, baseuri) or self.baseuri
         else:
             self.baseuri = _urljoin(self.baseuri, baseuri)
-        lang = attrs_d.get('xml:lang', attrs_d.get('lang'))
-        if lang == '':
+        lang = attrs_d.get("xml:lang", attrs_d.get("lang"))
+        if lang == "":
             # xml:lang could be explicitly set to '', we need to capture that
             lang = None
         elif lang is None:
             # if no xml:lang is specified, use parent lang
             lang = self.lang
         if lang:
-            if tag in ('feed', 'rss', 'rdf:RDF'):
-                self.feeddata['language'] = lang.replace('_', '-')
+            if tag in ("feed", "rss", "rdf:RDF"):
+                self.feeddata["language"] = lang.replace("_", "-")
         self.lang = lang
         self.basestack.append(self.baseuri)
         self.langstack.append(lang)
 
         # track namespaces
         for prefix, uri in attrs:
-            if prefix.startswith('xmlns:'):
+            if prefix.startswith("xmlns:"):
                 self.track_namespace(prefix[6:], uri)
-            elif prefix == 'xmlns':
+            elif prefix == "xmlns":
                 self.track_namespace(None, uri)
 
         # track inline content
-        if self.incontent and not self.contentparams.get('type', 'xml').endswith('xml'):
-            if tag in ('xhtml:div', 'div'):
+        if self.incontent and not self.contentparams.get("type", "xml").endswith("xml"):
+            if tag in ("xhtml:div", "div"):
                 return  # typepad does this 10/2007
             # element declared itself as escaped markup, but it isn't really
-            self.contentparams['type'] = 'application/xhtml+xml'
-        if self.incontent and self.contentparams.get('type') == 'application/xhtml+xml':
-            if tag.find(':') != -1:
-                prefix, tag = tag.split(':', 1)
-                namespace = self.namespaces_in_use.get(prefix, '')
-                if tag == 'math' and namespace == 'http://www.w3.org/1998/Math/MathML':
-                    attrs.append(('xmlns', namespace))
-                if tag == 'svg' and namespace == 'http://www.w3.org/2000/svg':
-                    attrs.append(('xmlns', namespace))
-            if tag == 'svg':
+            self.contentparams["type"] = "application/xhtml+xml"
+        if self.incontent and self.contentparams.get("type") == "application/xhtml+xml":
+            if tag.find(":") != -1:
+                prefix, tag = tag.split(":", 1)
+                namespace = self.namespaces_in_use.get(prefix, "")
+                if tag == "math" and namespace == "http://www.w3.org/1998/Math/MathML":
+                    attrs.append(("xmlns", namespace))
+                if tag == "svg" and namespace == "http://www.w3.org/2000/svg":
+                    attrs.append(("xmlns", namespace))
+            if tag == "svg":
                 self.svgOK += 1
-            return self.handle_data('<%s%s>' % (tag, self.strattrs(attrs)), escape=0)
+            return self.handle_data(f"<{tag}{self.strattrs(attrs)}>", escape=0)
 
         # match namespaces
-        if tag.find(':') != -1:
-            prefix, suffix = tag.split(':', 1)
+        if tag.find(":") != -1:
+            prefix, suffix = tag.split(":", 1)
         else:
-            prefix, suffix = '', tag
+            prefix, suffix = "", tag
         prefix = self.namespacemap.get(prefix, prefix)
         if prefix:
-            prefix = prefix + '_'
+            prefix = prefix + "_"
 
         # Special hack for better tracking of empty textinput/image elements in
         # illformed feeds.
-        if (not prefix) and tag not in ('title', 'link', 'description', 'name'):
+        if (not prefix) and tag not in ("title", "link", "description", "name"):
             self.intextinput = 0
-        if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'):
+        if (not prefix) and tag not in (
+            "title",
+            "link",
+            "description",
+            "url",
+            "href",
+            "width",
+            "height",
+        ):
             self.inimage = 0
 
         # call special handler (if defined) or default handler
-        methodname = '_start_' + prefix + suffix
+        methodname = "_start_" + prefix + suffix
         try:
             method = getattr(self, methodname)
             return method(attrs_d)
@@ -305,18 +320,18 @@ class XMLParserMixin(
 
     def unknown_endtag(self, tag):
         # match namespaces
-        if tag.find(':') != -1:
-            prefix, suffix = tag.split(':', 1)
+        if tag.find(":") != -1:
+            prefix, suffix = tag.split(":", 1)
         else:
-            prefix, suffix = '', tag
+            prefix, suffix = "", tag
         prefix = self.namespacemap.get(prefix, prefix)
         if prefix:
-            prefix = prefix + '_'
-        if suffix == 'svg' and self.svgOK:
+            prefix = prefix + "_"
+        if suffix == "svg" and self.svgOK:
             self.svgOK -= 1
 
         # call special handler (if defined) or default handler
-        methodname = '_end_' + prefix + suffix
+        methodname = "_end_" + prefix + suffix
         try:
             if self.svgOK:
                 raise AttributeError()
@@ -326,14 +341,14 @@ class XMLParserMixin(
             self.pop(prefix + suffix)
 
         # track inline content
-        if self.incontent and not self.contentparams.get('type', 'xml').endswith('xml'):
+        if self.incontent and not self.contentparams.get("type", "xml").endswith("xml"):
             # element declared itself as escaped markup, but it isn't really
-            if tag in ('xhtml:div', 'div'):
+            if tag in ("xhtml:div", "div"):
                 return  # typepad does this 10/2007
-            self.contentparams['type'] = 'application/xhtml+xml'
-        if self.incontent and self.contentparams.get('type') == 'application/xhtml+xml':
-            tag = tag.split(':')[-1]
-            self.handle_data('' % tag, escape=0)
+            self.contentparams["type"] = "application/xhtml+xml"
+        if self.incontent and self.contentparams.get("type") == "application/xhtml+xml":
+            tag = tag.split(":")[-1]
+            self.handle_data("" % tag, escape=0)
 
         # track xml:base and xml:lang going out of scope
         if self.basestack:
@@ -352,33 +367,33 @@ class XMLParserMixin(
         if not self.elementstack:
             return
         ref = ref.lower()
-        if ref in ('34', '38', '39', '60', '62', 'x22', 'x26', 'x27', 'x3c', 'x3e'):
-            text = '&#%s;' % ref
+        if ref in ("34", "38", "39", "60", "62", "x22", "x26", "x27", "x3c", "x3e"):
+            text = "&#%s;" % ref
         else:
-            if ref[0] == 'x':
+            if ref[0] == "x":
                 c = int(ref[1:], 16)
             else:
                 c = int(ref)
-            text = chr(c).encode('utf-8')
+            text = chr(c).encode("utf-8")
         self.elementstack[-1][2].append(text)
 
     def handle_entityref(self, ref):
         # Called for each entity reference, e.g. for '©', ref is 'copy'
         if not self.elementstack:
             return
-        if ref in ('lt', 'gt', 'quot', 'amp', 'apos'):
-            text = '&%s;' % ref
+        if ref in ("lt", "gt", "quot", "amp", "apos"):
+            text = "&%s;" % ref
         elif ref in self.entities:
             text = self.entities[ref]
-            if text.startswith('&#') and text.endswith(';'):
+            if text.startswith("&#") and text.endswith(";"):
                 return self.handle_entityref(text)
         else:
             try:
                 html.entities.name2codepoint[ref]
             except KeyError:
-                text = '&%s;' % ref
+                text = "&%s;" % ref
             else:
-                text = chr(html.entities.name2codepoint[ref]).encode('utf-8')
+                text = chr(html.entities.name2codepoint[ref]).encode("utf-8")
         self.elementstack[-1][2].append(text)
 
     def handle_data(self, text, escape=1):
@@ -386,7 +401,7 @@ class XMLParserMixin(
         # not containing any character or entity references
         if not self.elementstack:
             return
-        if escape and self.contentparams.get('type') == 'application/xhtml+xml':
+        if escape and self.contentparams.get("type") == "application/xhtml+xml":
             text = xml.sax.saxutils.escape(text)
         self.elementstack[-1][2].append(text)
 
@@ -403,18 +418,18 @@ class XMLParserMixin(
 
     def parse_declaration(self, i):
         # Override internal declaration handler to handle CDATA blocks.
-        if self.rawdata[i:i+9] == '', i)
+        if self.rawdata[i : i + 9] == "", i)
             if k == -1:
                 # CDATA block began but didn't finish
                 k = len(self.rawdata)
                 return k
-            self.handle_data(xml.sax.saxutils.escape(self.rawdata[i+9:k]), 0)
-            return k+3
+            self.handle_data(xml.sax.saxutils.escape(self.rawdata[i + 9 : k]), 0)
+            return k + 3
         else:
-            k = self.rawdata.find('>', i)
+            k = self.rawdata.find(">", i)
             if k >= 0:
-                return k+1
+                return k + 1
             else:
                 # We have an incomplete CDATA block.
                 return k
@@ -422,35 +437,35 @@ class XMLParserMixin(
     @staticmethod
     def map_content_type(content_type):
         content_type = content_type.lower()
-        if content_type == 'text' or content_type == 'plain':
-            content_type = 'text/plain'
-        elif content_type == 'html':
-            content_type = 'text/html'
-        elif content_type == 'xhtml':
-            content_type = 'application/xhtml+xml'
+        if content_type == "text" or content_type == "plain":
+            content_type = "text/plain"
+        elif content_type == "html":
+            content_type = "text/html"
+        elif content_type == "xhtml":
+            content_type = "application/xhtml+xml"
         return content_type
 
     def track_namespace(self, prefix, uri):
         loweruri = uri.lower()
         if not self.version:
-            if (prefix, loweruri) == (None, 'http://my.netscape.com/rdf/simple/0.9/'):
-                self.version = 'rss090'
-            elif loweruri == 'http://purl.org/rss/1.0/':
-                self.version = 'rss10'
-            elif loweruri == 'http://www.w3.org/2005/atom':
-                self.version = 'atom10'
-        if loweruri.find('backend.userland.com/rss') != -1:
+            if (prefix, loweruri) == (None, "http://my.netscape.com/rdf/simple/0.9/"):
+                self.version = "rss090"
+            elif loweruri == "http://purl.org/rss/1.0/":
+                self.version = "rss10"
+            elif loweruri == "http://www.w3.org/2005/atom":
+                self.version = "atom10"
+        if loweruri.find("backend.userland.com/rss") != -1:
             # match any backend.userland.com namespace
-            uri = 'http://backend.userland.com/rss'
+            uri = "http://backend.userland.com/rss"
             loweruri = uri
         if loweruri in self._matchnamespaces:
             self.namespacemap[prefix] = self._matchnamespaces[loweruri]
             self.namespaces_in_use[self._matchnamespaces[loweruri]] = uri
         else:
-            self.namespaces_in_use[prefix or ''] = uri
+            self.namespaces_in_use[prefix or ""] = uri
 
     def resolve_uri(self, uri):
-        return _urljoin(self.baseuri or '', uri)
+        return _urljoin(self.baseuri or "", uri)
 
     @staticmethod
     def decode_entities(element, data):
@@ -458,8 +473,8 @@ class XMLParserMixin(
 
     @staticmethod
     def strattrs(attrs):
-        return ''.join(
-            ' %s="%s"' % (t[0], xml.sax.saxutils.escape(t[1], {'"': '"'}))
+        return "".join(
+            ' {}="{}"'.format(t[0], xml.sax.saxutils.escape(t[1], {'"': """}))
             for t in attrs
         )
 
@@ -475,11 +490,14 @@ class XMLParserMixin(
         element, expecting_text, pieces = self.elementstack.pop()
 
         # Ensure each piece is a str for Python 3
-        for (i, v) in enumerate(pieces):
+        for i, v in enumerate(pieces):
             if isinstance(v, bytes):
-                pieces[i] = v.decode('utf-8')
+                pieces[i] = v.decode("utf-8")
 
-        if self.version == 'atom10' and self.contentparams.get('type', 'text') == 'application/xhtml+xml':
+        if (
+            self.version == "atom10"
+            and self.contentparams.get("type", "text") == "application/xhtml+xml"
+        ):
             # remove enclosing child element, but only if it is a 
and # only if all the remaining content is nested underneath it. # This means that the divs would be retained in the following: @@ -488,76 +506,95 @@ class XMLParserMixin( del pieces[-1] while pieces and len(pieces) > 1 and not pieces[0].strip(): del pieces[0] - if pieces and (pieces[0] == '
' or pieces[0].startswith('
': + if ( + pieces + and (pieces[0] == "
" or pieces[0].startswith("
" + ): depth = 0 for piece in pieces[:-1]: - if piece.startswith(''): + elif piece.startswith("<") and not piece.endswith("/>"): depth += 1 else: pieces = pieces[1:-1] - output = ''.join(pieces) + output = "".join(pieces) if strip_whitespace: output = output.strip() if not expecting_text: return output # decode base64 content - if base64 and self.contentparams.get('base64', 0): + if base64 and self.contentparams.get("base64", 0): try: - output = base64.decodebytes(output.encode('utf8')).decode('utf8') + output = base64.decodebytes(output.encode("utf8")).decode("utf8") except (binascii.Error, binascii.Incomplete, UnicodeDecodeError): pass # resolve relative URIs if (element in self.can_be_relative_uri) and output: # do not resolve guid elements with isPermalink="false" - if not element == 'id' or self.guidislink: + if not element == "id" or self.guidislink: output = self.resolve_uri(output) # decode entities within embedded markup - if not self.contentparams.get('base64', 0): + if not self.contentparams.get("base64", 0): output = self.decode_entities(element, output) # some feed formats require consumers to guess # whether the content is html or plain text - if not self.version.startswith('atom') and self.contentparams.get('type') == 'text/plain': + if ( + not self.version.startswith("atom") + and self.contentparams.get("type") == "text/plain" + ): if self.looks_like_html(output): - self.contentparams['type'] = 'text/html' + self.contentparams["type"] = "text/html" # remove temporary cruft from contentparams try: - del self.contentparams['mode'] + del self.contentparams["mode"] except KeyError: pass try: - del self.contentparams['base64'] + del self.contentparams["base64"] except KeyError: pass - is_htmlish = self.map_content_type(self.contentparams.get('type', 'text/html')) in self.html_types + is_htmlish = ( + self.map_content_type(self.contentparams.get("type", "text/html")) + in self.html_types + ) # resolve relative URIs within embedded markup if is_htmlish and self.resolve_relative_uris: if element in self.can_contain_relative_uris: - output = resolve_relative_uris(output, self.baseuri, self.encoding, self.contentparams.get('type', 'text/html')) + output = resolve_relative_uris( + output, + self.baseuri, + self.encoding, + self.contentparams.get("type", "text/html"), + ) # sanitize embedded markup if is_htmlish and self.sanitize_html: if element in self.can_contain_dangerous_markup: - output = sanitize_html(output, self.encoding, self.contentparams.get('type', 'text/html')) + output = sanitize_html( + output, self.encoding, self.contentparams.get("type", "text/html") + ) if self.encoding and isinstance(output, bytes): - output = output.decode(self.encoding, 'ignore') + output = output.decode(self.encoding, "ignore") # address common error where people take data that is already # utf-8, presume that it is iso-8859-1, and re-encode it. - if self.encoding in ('utf-8', 'utf-8_INVALID_PYTHON_3') and not isinstance(output, bytes): + if self.encoding in ("utf-8", "utf-8_INVALID_PYTHON_3") and not isinstance( + output, bytes + ): try: - output = output.encode('iso-8859-1').decode('utf-8') + output = output.encode("iso-8859-1").decode("utf-8") except (UnicodeEncodeError, UnicodeDecodeError): pass @@ -567,65 +604,74 @@ class XMLParserMixin( # categories/tags/keywords/whatever are handled in _end_category or # _end_tags or _end_itunes_keywords - if element in ('category', 'tags', 'itunes_keywords'): + if element in ("category", "tags", "itunes_keywords"): return output - if element == 'title' and -1 < self.title_depth <= self.depth: + if element == "title" and -1 < self.title_depth <= self.depth: return output # store output in appropriate place(s) if self.inentry and not self.insource: - if element == 'content': + if element == "content": self.entries[-1].setdefault(element, []) contentparams = copy.deepcopy(self.contentparams) - contentparams['value'] = output + contentparams["value"] = output self.entries[-1][element].append(contentparams) - elif element == 'link': + elif element == "link": if not self.inimage: # query variables in urls in link elements are improperly # converted from `?a=1&b=2` to `?a=1&b;=2` as if they're # unhandled character references. fix this special case. - output = output.replace('&', '&') + output = output.replace("&", "&") output = re.sub("&([A-Za-z0-9_]+);", r"&\g<1>", output) self.entries[-1][element] = output if output: - self.entries[-1]['links'][-1]['href'] = output + self.entries[-1]["links"][-1]["href"] = output else: - if element == 'description': - element = 'summary' - old_value_depth = self.property_depth_map.setdefault(self.entries[-1], {}).get(element) + if element == "description": + element = "summary" + old_value_depth = self.property_depth_map.setdefault( + self.entries[-1], {} + ).get(element) if old_value_depth is None or self.depth <= old_value_depth: self.property_depth_map[self.entries[-1]][element] = self.depth self.entries[-1][element] = output if self.incontent: contentparams = copy.deepcopy(self.contentparams) - contentparams['value'] = output - self.entries[-1][element + '_detail'] = contentparams - elif self.infeed or self.insource: # and (not self.intextinput) and (not self.inimage): + contentparams["value"] = output + self.entries[-1][element + "_detail"] = contentparams + elif ( + self.infeed or self.insource + ): # and (not self.intextinput) and (not self.inimage): context = self._get_context() - if element == 'description': - element = 'subtitle' + if element == "description": + element = "subtitle" context[element] = output - if element == 'link': + if element == "link": # fix query variables; see above for the explanation output = re.sub("&([A-Za-z0-9_]+);", r"&\g<1>", output) context[element] = output - context['links'][-1]['href'] = output + context["links"][-1]["href"] = output elif self.incontent: contentparams = copy.deepcopy(self.contentparams) - contentparams['value'] = output - context[element + '_detail'] = contentparams + contentparams["value"] = output + context[element + "_detail"] = contentparams return output def push_content(self, tag, attrs_d, default_content_type, expecting_text): self.incontent += 1 if self.lang: - self.lang = self.lang.replace('_', '-') - self.contentparams = FeedParserDict({ - 'type': self.map_content_type(attrs_d.get('type', default_content_type)), - 'language': self.lang, - 'base': self.baseuri}) - self.contentparams['base64'] = self._is_base64(attrs_d, self.contentparams) + self.lang = self.lang.replace("_", "-") + self.contentparams = FeedParserDict( + { + "type": self.map_content_type( + attrs_d.get("type", default_content_type) + ), + "language": self.lang, + "base": self.baseuri, + } + ) + self.contentparams["base64"] = self._is_base64(attrs_d, self.contentparams) self.push(tag, expecting_text) def pop_content(self, tag): @@ -646,55 +692,61 @@ class XMLParserMixin( """ # must have a close tag or an entity reference to qualify - if not (re.search(r'', s) or re.search(r'&#?\w+;', s)): + if not (re.search(r"", s) or re.search(r"&#?\w+;", s)): return False # all tags must be in a restricted subset of valid HTML tags - if any((t for t in re.findall(r'', '') - author = author.replace('<>', '') + author = author.replace(email, "") + author = author.replace("()", "") + author = author.replace("<>", "") + author = author.replace("<>", "") author = author.strip() - if author and (author[0] == '('): + if author and (author[0] == "("): author = author[1:] - if author and (author[-1] == ')'): + if author and (author[-1] == ")"): author = author[:-1] author = author.strip() if author or email: - context.setdefault('%s_detail' % key, detail) + context.setdefault("%s_detail" % key, detail) if author: - detail['name'] = author + detail["name"] = author if email: - detail['email'] = email + detail["email"] = email def _add_tag(self, term, scheme, label): context = self._get_context() - tags = context.setdefault('tags', []) + tags = context.setdefault("tags", []) if (not term) and (not scheme) and (not label): return value = FeedParserDict(term=term, scheme=scheme, label=label) @@ -781,8 +833,8 @@ class XMLParserMixin( # This is a completely-made up element. Its semantics are determined # only by a single feed that precipitated bug report 392 on Google Code. # In short, this is junk code. - self.push('tags', 1) + self.push("tags", 1) def _end_tags(self): - for term in self.pop('tags').split(','): + for term in self.pop("tags").split(","): self._add_tag(term.strip(), None, None) diff --git a/lib/feedparser/namespaces/_base.py b/lib/feedparser/namespaces/_base.py index 6478a76c..1fc3ee30 100644 --- a/lib/feedparser/namespaces/_base.py +++ b/lib/feedparser/namespaces/_base.py @@ -1,5 +1,5 @@ # Support for the Atom, RSS, RDF, and CDF feed formats -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -33,7 +33,7 @@ from ..urls import make_safe_absolute_uri from ..util import FeedParserDict -class Namespace(object): +class Namespace: """Support for the Atom, RSS, RDF, and CDF feed formats. The feed formats all share common elements, some of which have conflicting @@ -42,452 +42,490 @@ class Namespace(object): """ supported_namespaces = { - '': '', - 'http://backend.userland.com/rss': '', - 'http://blogs.law.harvard.edu/tech/rss': '', - 'http://purl.org/rss/1.0/': '', - 'http://my.netscape.com/rdf/simple/0.9/': '', - 'http://example.com/newformat#': '', - 'http://example.com/necho': '', - 'http://purl.org/echo/': '', - 'uri/of/echo/namespace#': '', - 'http://purl.org/pie/': '', - 'http://purl.org/atom/ns#': '', - 'http://www.w3.org/2005/Atom': '', - 'http://purl.org/rss/1.0/modules/rss091#': '', + "": "", + "http://backend.userland.com/rss": "", + "http://blogs.law.harvard.edu/tech/rss": "", + "http://purl.org/rss/1.0/": "", + "http://my.netscape.com/rdf/simple/0.9/": "", + "http://example.com/newformat#": "", + "http://example.com/necho": "", + "http://purl.org/echo/": "", + "uri/of/echo/namespace#": "", + "http://purl.org/pie/": "", + "http://purl.org/atom/ns#": "", + "http://www.w3.org/2005/Atom": "", + "http://purl.org/rss/1.0/modules/rss091#": "", } def _start_rss(self, attrs_d): versionmap = { - '0.91': 'rss091u', - '0.92': 'rss092', - '0.93': 'rss093', - '0.94': 'rss094', + "0.91": "rss091u", + "0.92": "rss092", + "0.93": "rss093", + "0.94": "rss094", } # If we're here then this is an RSS feed. # If we don't have a version or have a version that starts with something # other than RSS then there's been a mistake. Correct it. - if not self.version or not self.version.startswith('rss'): - attr_version = attrs_d.get('version', '') + if not self.version or not self.version.startswith("rss"): + attr_version = attrs_d.get("version", "") version = versionmap.get(attr_version) if version: self.version = version - elif attr_version.startswith('2.'): - self.version = 'rss20' + elif attr_version.startswith("2."): + self.version = "rss20" else: - self.version = 'rss' + self.version = "rss" def _start_channel(self, attrs_d): self.infeed = 1 self._cdf_common(attrs_d) def _cdf_common(self, attrs_d): - if 'lastmod' in attrs_d: + if "lastmod" in attrs_d: self._start_modified({}) - self.elementstack[-1][-1] = attrs_d['lastmod'] + self.elementstack[-1][-1] = attrs_d["lastmod"] self._end_modified() - if 'href' in attrs_d: + if "href" in attrs_d: self._start_link({}) - self.elementstack[-1][-1] = attrs_d['href'] + self.elementstack[-1][-1] = attrs_d["href"] self._end_link() def _start_feed(self, attrs_d): self.infeed = 1 - versionmap = {'0.1': 'atom01', - '0.2': 'atom02', - '0.3': 'atom03'} + versionmap = {"0.1": "atom01", "0.2": "atom02", "0.3": "atom03"} if not self.version: - attr_version = attrs_d.get('version') + attr_version = attrs_d.get("version") version = versionmap.get(attr_version) if version: self.version = version else: - self.version = 'atom' + self.version = "atom" def _end_channel(self): self.infeed = 0 + _end_feed = _end_channel def _start_image(self, attrs_d): context = self._get_context() if not self.inentry: - context.setdefault('image', FeedParserDict()) + context.setdefault("image", FeedParserDict()) self.inimage = 1 self.title_depth = -1 - self.push('image', 0) + self.push("image", 0) def _end_image(self): - self.pop('image') + self.pop("image") self.inimage = 0 def _start_textinput(self, attrs_d): context = self._get_context() - context.setdefault('textinput', FeedParserDict()) + context.setdefault("textinput", FeedParserDict()) self.intextinput = 1 self.title_depth = -1 - self.push('textinput', 0) + self.push("textinput", 0) + _start_textInput = _start_textinput def _end_textinput(self): - self.pop('textinput') + self.pop("textinput") self.intextinput = 0 + _end_textInput = _end_textinput def _start_author(self, attrs_d): self.inauthor = 1 - self.push('author', 1) + self.push("author", 1) # Append a new FeedParserDict when expecting an author context = self._get_context() - context.setdefault('authors', []) - context['authors'].append(FeedParserDict()) + context.setdefault("authors", []) + context["authors"].append(FeedParserDict()) + _start_managingeditor = _start_author def _end_author(self): - self.pop('author') + self.pop("author") self.inauthor = 0 self._sync_author_detail() + _end_managingeditor = _end_author def _start_contributor(self, attrs_d): self.incontributor = 1 context = self._get_context() - context.setdefault('contributors', []) - context['contributors'].append(FeedParserDict()) - self.push('contributor', 0) + context.setdefault("contributors", []) + context["contributors"].append(FeedParserDict()) + self.push("contributor", 0) def _end_contributor(self): - self.pop('contributor') + self.pop("contributor") self.incontributor = 0 def _start_name(self, attrs_d): - self.push('name', 0) + self.push("name", 0) def _end_name(self): - value = self.pop('name') + value = self.pop("name") if self.inpublisher: - self._save_author('name', value, 'publisher') + self._save_author("name", value, "publisher") elif self.inauthor: - self._save_author('name', value) + self._save_author("name", value) elif self.incontributor: - self._save_contributor('name', value) + self._save_contributor("name", value) elif self.intextinput: context = self._get_context() - context['name'] = value + context["name"] = value def _start_width(self, attrs_d): - self.push('width', 0) + self.push("width", 0) def _end_width(self): - value = self.pop('width') + value = self.pop("width") try: value = int(value) except ValueError: value = 0 if self.inimage: context = self._get_context() - context['width'] = value + context["width"] = value def _start_height(self, attrs_d): - self.push('height', 0) + self.push("height", 0) def _end_height(self): - value = self.pop('height') + value = self.pop("height") try: value = int(value) except ValueError: value = 0 if self.inimage: context = self._get_context() - context['height'] = value + context["height"] = value def _start_url(self, attrs_d): - self.push('href', 1) + self.push("href", 1) + _start_homepage = _start_url _start_uri = _start_url def _end_url(self): - value = self.pop('href') + value = self.pop("href") if self.inauthor: - self._save_author('href', value) + self._save_author("href", value) elif self.incontributor: - self._save_contributor('href', value) + self._save_contributor("href", value) + _end_homepage = _end_url _end_uri = _end_url def _start_email(self, attrs_d): - self.push('email', 0) + self.push("email", 0) def _end_email(self): - value = self.pop('email') + value = self.pop("email") if self.inpublisher: - self._save_author('email', value, 'publisher') + self._save_author("email", value, "publisher") elif self.inauthor: - self._save_author('email', value) + self._save_author("email", value) elif self.incontributor: - self._save_contributor('email', value) + self._save_contributor("email", value) def _start_subtitle(self, attrs_d): - self.push_content('subtitle', attrs_d, 'text/plain', 1) + self.push_content("subtitle", attrs_d, "text/plain", 1) + _start_tagline = _start_subtitle def _end_subtitle(self): - self.pop_content('subtitle') + self.pop_content("subtitle") + _end_tagline = _end_subtitle def _start_rights(self, attrs_d): - self.push_content('rights', attrs_d, 'text/plain', 1) + self.push_content("rights", attrs_d, "text/plain", 1) + _start_copyright = _start_rights def _end_rights(self): - self.pop_content('rights') + self.pop_content("rights") + _end_copyright = _end_rights def _start_item(self, attrs_d): self.entries.append(FeedParserDict()) - self.push('item', 0) + self.push("item", 0) self.inentry = 1 self.guidislink = 0 self.title_depth = -1 - id = self._get_attribute(attrs_d, 'rdf:about') + id = self._get_attribute(attrs_d, "rdf:about") if id: context = self._get_context() - context['id'] = id + context["id"] = id self._cdf_common(attrs_d) + _start_entry = _start_item def _end_item(self): - self.pop('item') + self.pop("item") self.inentry = 0 self.hasContent = 0 + _end_entry = _end_item def _start_language(self, attrs_d): - self.push('language', 1) + self.push("language", 1) def _end_language(self): - self.lang = self.pop('language') + self.lang = self.pop("language") def _start_webmaster(self, attrs_d): - self.push('publisher', 1) + self.push("publisher", 1) def _end_webmaster(self): - self.pop('publisher') - self._sync_author_detail('publisher') + self.pop("publisher") + self._sync_author_detail("publisher") def _start_published(self, attrs_d): - self.push('published', 1) + self.push("published", 1) + _start_issued = _start_published _start_pubdate = _start_published def _end_published(self): - value = self.pop('published') - self._save('published_parsed', _parse_date(value), overwrite=True) + value = self.pop("published") + self._save("published_parsed", _parse_date(value), overwrite=True) + _end_issued = _end_published _end_pubdate = _end_published def _start_updated(self, attrs_d): - self.push('updated', 1) + self.push("updated", 1) + _start_modified = _start_updated _start_lastbuilddate = _start_updated def _end_updated(self): - value = self.pop('updated') + value = self.pop("updated") parsed_value = _parse_date(value) - self._save('updated_parsed', parsed_value, overwrite=True) + self._save("updated_parsed", parsed_value, overwrite=True) + _end_modified = _end_updated _end_lastbuilddate = _end_updated def _start_created(self, attrs_d): - self.push('created', 1) + self.push("created", 1) def _end_created(self): - value = self.pop('created') - self._save('created_parsed', _parse_date(value), overwrite=True) + value = self.pop("created") + self._save("created_parsed", _parse_date(value), overwrite=True) def _start_expirationdate(self, attrs_d): - self.push('expired', 1) + self.push("expired", 1) def _end_expirationdate(self): - self._save('expired_parsed', _parse_date(self.pop('expired')), overwrite=True) + self._save("expired_parsed", _parse_date(self.pop("expired")), overwrite=True) def _start_category(self, attrs_d): - term = attrs_d.get('term') - scheme = attrs_d.get('scheme', attrs_d.get('domain')) - label = attrs_d.get('label') + term = attrs_d.get("term") + scheme = attrs_d.get("scheme", attrs_d.get("domain")) + label = attrs_d.get("label") self._add_tag(term, scheme, label) - self.push('category', 1) + self.push("category", 1) + _start_keywords = _start_category def _end_category(self): - value = self.pop('category') + value = self.pop("category") if not value: return context = self._get_context() - tags = context['tags'] - if value and len(tags) and not tags[-1]['term']: - tags[-1]['term'] = value + tags = context["tags"] + if value and len(tags) and not tags[-1]["term"]: + tags[-1]["term"] = value else: self._add_tag(value, None, None) + _end_keywords = _end_category def _start_cloud(self, attrs_d): - self._get_context()['cloud'] = FeedParserDict(attrs_d) + self._get_context()["cloud"] = FeedParserDict(attrs_d) def _start_link(self, attrs_d): - attrs_d.setdefault('rel', 'alternate') - if attrs_d['rel'] == 'self': - attrs_d.setdefault('type', 'application/atom+xml') + attrs_d.setdefault("rel", "alternate") + if attrs_d["rel"] == "self": + attrs_d.setdefault("type", "application/atom+xml") else: - attrs_d.setdefault('type', 'text/html') + attrs_d.setdefault("type", "text/html") context = self._get_context() attrs_d = self._enforce_href(attrs_d) - if 'href' in attrs_d: - attrs_d['href'] = self.resolve_uri(attrs_d['href']) + if "href" in attrs_d: + attrs_d["href"] = self.resolve_uri(attrs_d["href"]) expecting_text = self.infeed or self.inentry or self.insource - context.setdefault('links', []) + context.setdefault("links", []) if not (self.inentry and self.inimage): - context['links'].append(FeedParserDict(attrs_d)) - if 'href' in attrs_d: + context["links"].append(FeedParserDict(attrs_d)) + if "href" in attrs_d: if ( - attrs_d.get('rel') == 'alternate' - and self.map_content_type(attrs_d.get('type')) in self.html_types + attrs_d.get("rel") == "alternate" + and self.map_content_type(attrs_d.get("type")) in self.html_types ): - context['link'] = attrs_d['href'] + context["link"] = attrs_d["href"] else: - self.push('link', expecting_text) + self.push("link", expecting_text) def _end_link(self): - self.pop('link') + self.pop("link") def _start_guid(self, attrs_d): - self.guidislink = (attrs_d.get('ispermalink', 'true') == 'true') - self.push('id', 1) + self.guidislink = attrs_d.get("ispermalink", "true") == "true" + self.push("id", 1) + _start_id = _start_guid def _end_guid(self): - value = self.pop('id') - self._save('guidislink', self.guidislink and 'link' not in self._get_context()) + value = self.pop("id") + self._save("guidislink", self.guidislink and "link" not in self._get_context()) if self.guidislink: # guid acts as link, but only if 'ispermalink' is not present or is 'true', # and only if the item doesn't already have a link element - self._save('link', value) + self._save("link", value) + _end_id = _end_guid def _start_title(self, attrs_d): if self.svgOK: - return self.unknown_starttag('title', list(attrs_d.items())) - self.push_content('title', attrs_d, 'text/plain', self.infeed or self.inentry or self.insource) + return self.unknown_starttag("title", list(attrs_d.items())) + self.push_content( + "title", attrs_d, "text/plain", self.infeed or self.inentry or self.insource + ) def _end_title(self): if self.svgOK: return - value = self.pop_content('title') + value = self.pop_content("title") if not value: return self.title_depth = self.depth def _start_description(self, attrs_d): context = self._get_context() - if 'summary' in context and not self.hasContent: - self._summaryKey = 'content' + if "summary" in context and not self.hasContent: + self._summaryKey = "content" self._start_content(attrs_d) else: - self.push_content('description', attrs_d, 'text/html', self.infeed or self.inentry or self.insource) + self.push_content( + "description", + attrs_d, + "text/html", + self.infeed or self.inentry or self.insource, + ) def _start_abstract(self, attrs_d): - self.push_content('description', attrs_d, 'text/plain', self.infeed or self.inentry or self.insource) + self.push_content( + "description", + attrs_d, + "text/plain", + self.infeed or self.inentry or self.insource, + ) def _end_description(self): - if self._summaryKey == 'content': + if self._summaryKey == "content": self._end_content() else: - self.pop_content('description') + self.pop_content("description") self._summaryKey = None + _end_abstract = _end_description def _start_info(self, attrs_d): - self.push_content('info', attrs_d, 'text/plain', 1) + self.push_content("info", attrs_d, "text/plain", 1) + _start_feedburner_browserfriendly = _start_info def _end_info(self): - self.pop_content('info') + self.pop_content("info") + _end_feedburner_browserfriendly = _end_info def _start_generator(self, attrs_d): if attrs_d: attrs_d = self._enforce_href(attrs_d) - if 'href' in attrs_d: - attrs_d['href'] = self.resolve_uri(attrs_d['href']) - self._get_context()['generator_detail'] = FeedParserDict(attrs_d) - self.push('generator', 1) + if "href" in attrs_d: + attrs_d["href"] = self.resolve_uri(attrs_d["href"]) + self._get_context()["generator_detail"] = FeedParserDict(attrs_d) + self.push("generator", 1) def _end_generator(self): - value = self.pop('generator') + value = self.pop("generator") context = self._get_context() - if 'generator_detail' in context: - context['generator_detail']['name'] = value + if "generator_detail" in context: + context["generator_detail"]["name"] = value def _start_summary(self, attrs_d): context = self._get_context() - if 'summary' in context and not self.hasContent: - self._summaryKey = 'content' + if "summary" in context and not self.hasContent: + self._summaryKey = "content" self._start_content(attrs_d) else: - self._summaryKey = 'summary' - self.push_content(self._summaryKey, attrs_d, 'text/plain', 1) + self._summaryKey = "summary" + self.push_content(self._summaryKey, attrs_d, "text/plain", 1) def _end_summary(self): - if self._summaryKey == 'content': + if self._summaryKey == "content": self._end_content() else: - self.pop_content(self._summaryKey or 'summary') + self.pop_content(self._summaryKey or "summary") self._summaryKey = None def _start_enclosure(self, attrs_d): attrs_d = self._enforce_href(attrs_d) context = self._get_context() - attrs_d['rel'] = 'enclosure' - context.setdefault('links', []).append(FeedParserDict(attrs_d)) + attrs_d["rel"] = "enclosure" + context.setdefault("links", []).append(FeedParserDict(attrs_d)) def _start_source(self, attrs_d): - if 'url' in attrs_d: + if "url" in attrs_d: # This means that we're processing a source element from an RSS 2.0 feed - self.sourcedata['href'] = attrs_d['url'] - self.push('source', 1) + self.sourcedata["href"] = attrs_d["url"] + self.push("source", 1) self.insource = 1 self.title_depth = -1 def _end_source(self): self.insource = 0 - value = self.pop('source') + value = self.pop("source") if value: - self.sourcedata['title'] = value - self._get_context()['source'] = copy.deepcopy(self.sourcedata) + self.sourcedata["title"] = value + self._get_context()["source"] = copy.deepcopy(self.sourcedata) self.sourcedata.clear() def _start_content(self, attrs_d): self.hasContent = 1 - self.push_content('content', attrs_d, 'text/plain', 1) - src = attrs_d.get('src') + self.push_content("content", attrs_d, "text/plain", 1) + src = attrs_d.get("src") if src: - self.contentparams['src'] = src - self.push('content', 1) + self.contentparams["src"] = src + self.push("content", 1) def _start_body(self, attrs_d): - self.push_content('content', attrs_d, 'application/xhtml+xml', 1) + self.push_content("content", attrs_d, "application/xhtml+xml", 1) + _start_xhtml_body = _start_body def _start_content_encoded(self, attrs_d): self.hasContent = 1 - self.push_content('content', attrs_d, 'text/html', 1) + self.push_content("content", attrs_d, "text/html", 1) + _start_fullitem = _start_content_encoded def _end_content(self): - copyToSummary = self.map_content_type(self.contentparams.get('type')) in ({'text/plain'} | self.html_types) - value = self.pop_content('content') + copyToSummary = self.map_content_type(self.contentparams.get("type")) in ( + {"text/plain"} | self.html_types + ) + value = self.pop_content("content") if copyToSummary: - self._save('summary', value) + self._save("summary", value) _end_body = _end_content _end_xhtml_body = _end_content @@ -495,12 +533,12 @@ class Namespace(object): _end_fullitem = _end_content def _start_newlocation(self, attrs_d): - self.push('newlocation', 1) + self.push("newlocation", 1) def _end_newlocation(self): - url = self.pop('newlocation') + url = self.pop("newlocation") context = self._get_context() # don't set newlocation if the context isn't right if context is not self.feeddata: return - context['newlocation'] = make_safe_absolute_uri(self.baseuri, url.strip()) + context["newlocation"] = make_safe_absolute_uri(self.baseuri, url.strip()) diff --git a/lib/feedparser/namespaces/admin.py b/lib/feedparser/namespaces/admin.py index 74218348..47fa1d5a 100644 --- a/lib/feedparser/namespaces/admin.py +++ b/lib/feedparser/namespaces/admin.py @@ -1,5 +1,5 @@ # Support for the administrative elements extension -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -29,25 +29,25 @@ from ..util import FeedParserDict -class Namespace(object): +class Namespace: # RDF Site Summary 1.0 Modules: Administrative # http://web.resource.org/rss/1.0/modules/admin/ supported_namespaces = { - 'http://webns.net/mvcb/': 'admin', + "http://webns.net/mvcb/": "admin", } def _start_admin_generatoragent(self, attrs_d): - self.push('generator', 1) - value = self._get_attribute(attrs_d, 'rdf:resource') + self.push("generator", 1) + value = self._get_attribute(attrs_d, "rdf:resource") if value: self.elementstack[-1][2].append(value) - self.pop('generator') - self._get_context()['generator_detail'] = FeedParserDict({'href': value}) + self.pop("generator") + self._get_context()["generator_detail"] = FeedParserDict({"href": value}) def _start_admin_errorreportsto(self, attrs_d): - self.push('errorreportsto', 1) - value = self._get_attribute(attrs_d, 'rdf:resource') + self.push("errorreportsto", 1) + value = self._get_attribute(attrs_d, "rdf:resource") if value: self.elementstack[-1][2].append(value) - self.pop('errorreportsto') + self.pop("errorreportsto") diff --git a/lib/feedparser/namespaces/cc.py b/lib/feedparser/namespaces/cc.py index 6735c5fe..bbe3bed2 100644 --- a/lib/feedparser/namespaces/cc.py +++ b/lib/feedparser/namespaces/cc.py @@ -1,5 +1,5 @@ # Support for the Creative Commons licensing extensions -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -29,41 +29,42 @@ from ..util import FeedParserDict -class Namespace(object): +class Namespace: supported_namespaces = { # RDF-based namespace - 'http://creativecommons.org/ns#license': 'cc', - + "http://creativecommons.org/ns#license": "cc", # Old RDF-based namespace - 'http://web.resource.org/cc/': 'cc', - + "http://web.resource.org/cc/": "cc", # RSS-based namespace - 'http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html': 'creativecommons', - + "http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html": ( + "creativecommons" + ), # Old RSS-based namespace - 'http://backend.userland.com/creativeCommonsRssModule': 'creativecommons', + "http://backend.userland.com/creativeCommonsRssModule": "creativecommons", } def _start_cc_license(self, attrs_d): context = self._get_context() - value = self._get_attribute(attrs_d, 'rdf:resource') + value = self._get_attribute(attrs_d, "rdf:resource") attrs_d = FeedParserDict() - attrs_d['rel'] = 'license' + attrs_d["rel"] = "license" if value: - attrs_d['href'] = value - context.setdefault('links', []).append(attrs_d) + attrs_d["href"] = value + context.setdefault("links", []).append(attrs_d) def _start_creativecommons_license(self, attrs_d): - self.push('license', 1) + self.push("license", 1) + _start_creativeCommons_license = _start_creativecommons_license def _end_creativecommons_license(self): - value = self.pop('license') + value = self.pop("license") context = self._get_context() attrs_d = FeedParserDict() - attrs_d['rel'] = 'license' + attrs_d["rel"] = "license" if value: - attrs_d['href'] = value - context.setdefault('links', []).append(attrs_d) - del context['license'] + attrs_d["href"] = value + context.setdefault("links", []).append(attrs_d) + del context["license"] + _end_creativeCommons_license = _end_creativecommons_license diff --git a/lib/feedparser/namespaces/dc.py b/lib/feedparser/namespaces/dc.py index a89221d2..b0275325 100644 --- a/lib/feedparser/namespaces/dc.py +++ b/lib/feedparser/namespaces/dc.py @@ -1,5 +1,5 @@ # Support for the Dublin Core metadata extensions -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -30,10 +30,10 @@ from ..datetimes import _parse_date from ..util import FeedParserDict -class Namespace(object): +class Namespace: supported_namespaces = { - 'http://purl.org/dc/elements/1.1/': 'dc', - 'http://purl.org/dc/terms/': 'dcterms', + "http://purl.org/dc/elements/1.1/": "dc", + "http://purl.org/dc/terms/": "dcterms", } def _end_dc_author(self): @@ -109,25 +109,29 @@ class Namespace(object): self._start_updated(attrs_d) def _start_dcterms_valid(self, attrs_d): - self.push('validity', 1) + self.push("validity", 1) def _end_dcterms_valid(self): - for validity_detail in self.pop('validity').split(';'): - if '=' in validity_detail: - key, value = validity_detail.split('=', 1) - if key == 'start': - self._save('validity_start', value, overwrite=True) - self._save('validity_start_parsed', _parse_date(value), overwrite=True) - elif key == 'end': - self._save('validity_end', value, overwrite=True) - self._save('validity_end_parsed', _parse_date(value), overwrite=True) + for validity_detail in self.pop("validity").split(";"): + if "=" in validity_detail: + key, value = validity_detail.split("=", 1) + if key == "start": + self._save("validity_start", value, overwrite=True) + self._save( + "validity_start_parsed", _parse_date(value), overwrite=True + ) + elif key == "end": + self._save("validity_end", value, overwrite=True) + self._save( + "validity_end_parsed", _parse_date(value), overwrite=True + ) def _start_dc_contributor(self, attrs_d): self.incontributor = 1 context = self._get_context() - context.setdefault('contributors', []) - context['contributors'].append(FeedParserDict()) - self.push('name', 0) + context.setdefault("contributors", []) + context["contributors"].append(FeedParserDict()) + self.push("name", 0) def _end_dc_contributor(self): self._end_name() diff --git a/lib/feedparser/namespaces/georss.py b/lib/feedparser/namespaces/georss.py index 786a926f..93a508e8 100644 --- a/lib/feedparser/namespaces/georss.py +++ b/lib/feedparser/namespaces/georss.py @@ -1,5 +1,5 @@ # Support for the GeoRSS format -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -26,27 +26,24 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -# Required for Python 3.6 compatibility. -from __future__ import generator_stop - from ..util import FeedParserDict -class Namespace(object): +class Namespace: supported_namespaces = { - 'http://www.w3.org/2003/01/geo/wgs84_pos#': 'geo', - 'http://www.georss.org/georss': 'georss', - 'http://www.opengis.net/gml': 'gml', + "http://www.w3.org/2003/01/geo/wgs84_pos#": "geo", + "http://www.georss.org/georss": "georss", + "http://www.opengis.net/gml": "gml", } def __init__(self): self.ingeometry = 0 - super(Namespace, self).__init__() + super().__init__() def _start_georssgeom(self, attrs_d): - self.push('geometry', 0) + self.push("geometry", 0) context = self._get_context() - context['where'] = FeedParserDict() + context["where"] = FeedParserDict() _start_georss_point = _start_georssgeom _start_georss_line = _start_georssgeom @@ -55,76 +52,77 @@ class Namespace(object): def _save_where(self, geometry): context = self._get_context() - context['where'].update(geometry) + context["where"].update(geometry) def _end_georss_point(self): - geometry = _parse_georss_point(self.pop('geometry')) + geometry = _parse_georss_point(self.pop("geometry")) if geometry: self._save_where(geometry) def _end_georss_line(self): - geometry = _parse_georss_line(self.pop('geometry')) + geometry = _parse_georss_line(self.pop("geometry")) if geometry: self._save_where(geometry) def _end_georss_polygon(self): - this = self.pop('geometry') + this = self.pop("geometry") geometry = _parse_georss_polygon(this) if geometry: self._save_where(geometry) def _end_georss_box(self): - geometry = _parse_georss_box(self.pop('geometry')) + geometry = _parse_georss_box(self.pop("geometry")) if geometry: self._save_where(geometry) def _start_where(self, attrs_d): - self.push('where', 0) + self.push("where", 0) context = self._get_context() - context['where'] = FeedParserDict() + context["where"] = FeedParserDict() + _start_georss_where = _start_where def _parse_srs_attrs(self, attrs_d): - srs_name = attrs_d.get('srsname') + srs_name = attrs_d.get("srsname") try: - srs_dimension = int(attrs_d.get('srsdimension', '2')) + srs_dimension = int(attrs_d.get("srsdimension", "2")) except ValueError: srs_dimension = 2 context = self._get_context() - if 'where' not in context: - context['where'] = {} - context['where']['srsName'] = srs_name - context['where']['srsDimension'] = srs_dimension + if "where" not in context: + context["where"] = {} + context["where"]["srsName"] = srs_name + context["where"]["srsDimension"] = srs_dimension def _start_gml_point(self, attrs_d): self._parse_srs_attrs(attrs_d) self.ingeometry = 1 - self.push('geometry', 0) + self.push("geometry", 0) def _start_gml_linestring(self, attrs_d): self._parse_srs_attrs(attrs_d) - self.ingeometry = 'linestring' - self.push('geometry', 0) + self.ingeometry = "linestring" + self.push("geometry", 0) def _start_gml_polygon(self, attrs_d): self._parse_srs_attrs(attrs_d) - self.push('geometry', 0) + self.push("geometry", 0) def _start_gml_exterior(self, attrs_d): - self.push('geometry', 0) + self.push("geometry", 0) def _start_gml_linearring(self, attrs_d): - self.ingeometry = 'polygon' - self.push('geometry', 0) + self.ingeometry = "polygon" + self.push("geometry", 0) def _start_gml_pos(self, attrs_d): - self.push('pos', 0) + self.push("pos", 0) def _end_gml_pos(self): - this = self.pop('pos') + this = self.pop("pos") context = self._get_context() - srs_name = context['where'].get('srsName') - srs_dimension = context['where'].get('srsDimension', 2) + srs_name = context["where"].get("srsName") + srs_dimension = context["where"].get("srsDimension", 2) swap = True if srs_name and "EPSG" in srs_name: epsg = int(srs_name.split(":")[-1]) @@ -134,25 +132,25 @@ class Namespace(object): self._save_where(geometry) def _start_gml_poslist(self, attrs_d): - self.push('pos', 0) + self.push("pos", 0) def _end_gml_poslist(self): - this = self.pop('pos') + this = self.pop("pos") context = self._get_context() - srs_name = context['where'].get('srsName') - srs_dimension = context['where'].get('srsDimension', 2) + srs_name = context["where"].get("srsName") + srs_dimension = context["where"].get("srsDimension", 2) swap = True if srs_name and "EPSG" in srs_name: epsg = int(srs_name.split(":")[-1]) swap = bool(epsg in _geogCS) - geometry = _parse_poslist( - this, self.ingeometry, swap=swap, dims=srs_dimension) + geometry = _parse_poslist(this, self.ingeometry, swap=swap, dims=srs_dimension) if geometry: self._save_where(geometry) def _end_geom(self): self.ingeometry = 0 - self.pop('geometry') + self.pop("geometry") + _end_gml_point = _end_geom _end_gml_linestring = _end_geom _end_gml_linearring = _end_geom @@ -160,19 +158,21 @@ class Namespace(object): _end_gml_polygon = _end_geom def _end_where(self): - self.pop('where') + self.pop("where") + _end_georss_where = _end_where # GeoRSS geometry parsers. Each return a dict with 'type' and 'coordinates' # items, or None in the case of a parsing error. + def _parse_poslist(value, geom_type, swap=True, dims=2): - if geom_type == 'linestring': + if geom_type == "linestring": return _parse_georss_line(value, swap, dims) - elif geom_type == 'polygon': + elif geom_type == "polygon": ring = _parse_georss_line(value, swap, dims) - return {'type': 'Polygon', 'coordinates': (ring['coordinates'],)} + return {"type": "Polygon", "coordinates": (ring["coordinates"],)} else: return None @@ -180,10 +180,10 @@ def _parse_poslist(value, geom_type, swap=True, dims=2): def _gen_georss_coords(value, swap=True, dims=2): # A generator of (lon, lat) pairs from a string of encoded GeoRSS # coordinates. Converts to floats and swaps order. - latlons = (float(ll) for ll in value.replace(',', ' ').split()) + latlons = (float(ll) for ll in value.replace(",", " ").split()) while True: try: - t = [next(latlons), next(latlons)][::swap and -1 or 1] + t = [next(latlons), next(latlons)][:: swap and -1 or 1] if dims == 3: t.append(next(latlons)) yield tuple(t) @@ -196,7 +196,7 @@ def _parse_georss_point(value, swap=True, dims=2): # whitespace. We'll also handle comma separators. try: coords = list(_gen_georss_coords(value, swap, dims)) - return {'type': 'Point', 'coordinates': coords[0]} + return {"type": "Point", "coordinates": coords[0]} except (IndexError, ValueError): return None @@ -207,7 +207,7 @@ def _parse_georss_line(value, swap=True, dims=2): # whitespace. There must be at least two pairs. try: coords = list(_gen_georss_coords(value, swap, dims)) - return {'type': 'LineString', 'coordinates': coords} + return {"type": "LineString", "coordinates": coords} except (IndexError, ValueError): return None @@ -223,7 +223,7 @@ def _parse_georss_polygon(value, swap=True, dims=2): return None if len(ring) < 4: return None - return {'type': 'Polygon', 'coordinates': (ring,)} + return {"type": "Polygon", "coordinates": (ring,)} def _parse_georss_box(value, swap=True, dims=2): @@ -233,7 +233,7 @@ def _parse_georss_box(value, swap=True, dims=2): # first pair is the lower corner, the second is the upper corner. try: coords = list(_gen_georss_coords(value, swap, dims)) - return {'type': 'Box', 'coordinates': tuple(coords)} + return {"type": "Box", "coordinates": tuple(coords)} except (IndexError, ValueError): return None @@ -241,38 +241,443 @@ def _parse_georss_box(value, swap=True, dims=2): # The list of EPSG codes for geographic (latitude/longitude) coordinate # systems to support decoding of GeoRSS GML profiles. _geogCS = [ - 3819, 3821, 3824, 3889, 3906, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, - 4009, 4010, 4011, 4012, 4013, 4014, 4015, 4016, 4018, 4019, 4020, 4021, 4022, - 4023, 4024, 4025, 4027, 4028, 4029, 4030, 4031, 4032, 4033, 4034, 4035, 4036, - 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4052, 4053, 4054, 4055, 4075, 4081, - 4120, 4121, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132, - 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4142, 4143, 4144, 4145, - 4146, 4147, 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158, - 4159, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, - 4172, 4173, 4174, 4175, 4176, 4178, 4179, 4180, 4181, 4182, 4183, 4184, 4185, - 4188, 4189, 4190, 4191, 4192, 4193, 4194, 4195, 4196, 4197, 4198, 4199, 4200, - 4201, 4202, 4203, 4204, 4205, 4206, 4207, 4208, 4209, 4210, 4211, 4212, 4213, - 4214, 4215, 4216, 4218, 4219, 4220, 4221, 4222, 4223, 4224, 4225, 4226, 4227, - 4228, 4229, 4230, 4231, 4232, 4233, 4234, 4235, 4236, 4237, 4238, 4239, 4240, - 4241, 4242, 4243, 4244, 4245, 4246, 4247, 4248, 4249, 4250, 4251, 4252, 4253, - 4254, 4255, 4256, 4257, 4258, 4259, 4260, 4261, 4262, 4263, 4264, 4265, 4266, - 4267, 4268, 4269, 4270, 4271, 4272, 4273, 4274, 4275, 4276, 4277, 4278, 4279, - 4280, 4281, 4282, 4283, 4284, 4285, 4286, 4287, 4288, 4289, 4291, 4292, 4293, - 4294, 4295, 4296, 4297, 4298, 4299, 4300, 4301, 4302, 4303, 4304, 4306, 4307, - 4308, 4309, 4310, 4311, 4312, 4313, 4314, 4315, 4316, 4317, 4318, 4319, 4322, - 4324, 4326, 4463, 4470, 4475, 4483, 4490, 4555, 4558, 4600, 4601, 4602, 4603, - 4604, 4605, 4606, 4607, 4608, 4609, 4610, 4611, 4612, 4613, 4614, 4615, 4616, - 4617, 4618, 4619, 4620, 4621, 4622, 4623, 4624, 4625, 4626, 4627, 4628, 4629, - 4630, 4631, 4632, 4633, 4634, 4635, 4636, 4637, 4638, 4639, 4640, 4641, 4642, - 4643, 4644, 4645, 4646, 4657, 4658, 4659, 4660, 4661, 4662, 4663, 4664, 4665, - 4666, 4667, 4668, 4669, 4670, 4671, 4672, 4673, 4674, 4675, 4676, 4677, 4678, - 4679, 4680, 4681, 4682, 4683, 4684, 4685, 4686, 4687, 4688, 4689, 4690, 4691, - 4692, 4693, 4694, 4695, 4696, 4697, 4698, 4699, 4700, 4701, 4702, 4703, 4704, - 4705, 4706, 4707, 4708, 4709, 4710, 4711, 4712, 4713, 4714, 4715, 4716, 4717, - 4718, 4719, 4720, 4721, 4722, 4723, 4724, 4725, 4726, 4727, 4728, 4729, 4730, - 4731, 4732, 4733, 4734, 4735, 4736, 4737, 4738, 4739, 4740, 4741, 4742, 4743, - 4744, 4745, 4746, 4747, 4748, 4749, 4750, 4751, 4752, 4753, 4754, 4755, 4756, - 4757, 4758, 4759, 4760, 4761, 4762, 4763, 4764, 4765, 4801, 4802, 4803, 4804, - 4805, 4806, 4807, 4808, 4809, 4810, 4811, 4813, 4814, 4815, 4816, 4817, 4818, - 4819, 4820, 4821, 4823, 4824, 4901, 4902, 4903, 4904, 4979, + 3819, + 3821, + 3824, + 3889, + 3906, + 4001, + 4002, + 4003, + 4004, + 4005, + 4006, + 4007, + 4008, + 4009, + 4010, + 4011, + 4012, + 4013, + 4014, + 4015, + 4016, + 4018, + 4019, + 4020, + 4021, + 4022, + 4023, + 4024, + 4025, + 4027, + 4028, + 4029, + 4030, + 4031, + 4032, + 4033, + 4034, + 4035, + 4036, + 4041, + 4042, + 4043, + 4044, + 4045, + 4046, + 4047, + 4052, + 4053, + 4054, + 4055, + 4075, + 4081, + 4120, + 4121, + 4122, + 4123, + 4124, + 4125, + 4126, + 4127, + 4128, + 4129, + 4130, + 4131, + 4132, + 4133, + 4134, + 4135, + 4136, + 4137, + 4138, + 4139, + 4140, + 4141, + 4142, + 4143, + 4144, + 4145, + 4146, + 4147, + 4148, + 4149, + 4150, + 4151, + 4152, + 4153, + 4154, + 4155, + 4156, + 4157, + 4158, + 4159, + 4160, + 4161, + 4162, + 4163, + 4164, + 4165, + 4166, + 4167, + 4168, + 4169, + 4170, + 4171, + 4172, + 4173, + 4174, + 4175, + 4176, + 4178, + 4179, + 4180, + 4181, + 4182, + 4183, + 4184, + 4185, + 4188, + 4189, + 4190, + 4191, + 4192, + 4193, + 4194, + 4195, + 4196, + 4197, + 4198, + 4199, + 4200, + 4201, + 4202, + 4203, + 4204, + 4205, + 4206, + 4207, + 4208, + 4209, + 4210, + 4211, + 4212, + 4213, + 4214, + 4215, + 4216, + 4218, + 4219, + 4220, + 4221, + 4222, + 4223, + 4224, + 4225, + 4226, + 4227, + 4228, + 4229, + 4230, + 4231, + 4232, + 4233, + 4234, + 4235, + 4236, + 4237, + 4238, + 4239, + 4240, + 4241, + 4242, + 4243, + 4244, + 4245, + 4246, + 4247, + 4248, + 4249, + 4250, + 4251, + 4252, + 4253, + 4254, + 4255, + 4256, + 4257, + 4258, + 4259, + 4260, + 4261, + 4262, + 4263, + 4264, + 4265, + 4266, + 4267, + 4268, + 4269, + 4270, + 4271, + 4272, + 4273, + 4274, + 4275, + 4276, + 4277, + 4278, + 4279, + 4280, + 4281, + 4282, + 4283, + 4284, + 4285, + 4286, + 4287, + 4288, + 4289, + 4291, + 4292, + 4293, + 4294, + 4295, + 4296, + 4297, + 4298, + 4299, + 4300, + 4301, + 4302, + 4303, + 4304, + 4306, + 4307, + 4308, + 4309, + 4310, + 4311, + 4312, + 4313, + 4314, + 4315, + 4316, + 4317, + 4318, + 4319, + 4322, + 4324, + 4326, + 4463, + 4470, + 4475, + 4483, + 4490, + 4555, + 4558, + 4600, + 4601, + 4602, + 4603, + 4604, + 4605, + 4606, + 4607, + 4608, + 4609, + 4610, + 4611, + 4612, + 4613, + 4614, + 4615, + 4616, + 4617, + 4618, + 4619, + 4620, + 4621, + 4622, + 4623, + 4624, + 4625, + 4626, + 4627, + 4628, + 4629, + 4630, + 4631, + 4632, + 4633, + 4634, + 4635, + 4636, + 4637, + 4638, + 4639, + 4640, + 4641, + 4642, + 4643, + 4644, + 4645, + 4646, + 4657, + 4658, + 4659, + 4660, + 4661, + 4662, + 4663, + 4664, + 4665, + 4666, + 4667, + 4668, + 4669, + 4670, + 4671, + 4672, + 4673, + 4674, + 4675, + 4676, + 4677, + 4678, + 4679, + 4680, + 4681, + 4682, + 4683, + 4684, + 4685, + 4686, + 4687, + 4688, + 4689, + 4690, + 4691, + 4692, + 4693, + 4694, + 4695, + 4696, + 4697, + 4698, + 4699, + 4700, + 4701, + 4702, + 4703, + 4704, + 4705, + 4706, + 4707, + 4708, + 4709, + 4710, + 4711, + 4712, + 4713, + 4714, + 4715, + 4716, + 4717, + 4718, + 4719, + 4720, + 4721, + 4722, + 4723, + 4724, + 4725, + 4726, + 4727, + 4728, + 4729, + 4730, + 4731, + 4732, + 4733, + 4734, + 4735, + 4736, + 4737, + 4738, + 4739, + 4740, + 4741, + 4742, + 4743, + 4744, + 4745, + 4746, + 4747, + 4748, + 4749, + 4750, + 4751, + 4752, + 4753, + 4754, + 4755, + 4756, + 4757, + 4758, + 4759, + 4760, + 4761, + 4762, + 4763, + 4764, + 4765, + 4801, + 4802, + 4803, + 4804, + 4805, + 4806, + 4807, + 4808, + 4809, + 4810, + 4811, + 4813, + 4814, + 4815, + 4816, + 4817, + 4818, + 4819, + 4820, + 4821, + 4823, + 4824, + 4901, + 4902, + 4903, + 4904, + 4979, ] diff --git a/lib/feedparser/namespaces/itunes.py b/lib/feedparser/namespaces/itunes.py index a50a0ea8..952f37a9 100644 --- a/lib/feedparser/namespaces/itunes.py +++ b/lib/feedparser/namespaces/itunes.py @@ -1,5 +1,5 @@ # Support for the iTunes format -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -29,13 +29,12 @@ from ..util import FeedParserDict -class Namespace(object): +class Namespace: supported_namespaces = { # Canonical namespace - 'http://www.itunes.com/DTDs/PodCast-1.0.dtd': 'itunes', - + "http://www.itunes.com/DTDs/PodCast-1.0.dtd": "itunes", # Extra namespace - 'http://example.com/DTDs/PodCast-1.0.dtd': 'itunes', + "http://example.com/DTDs/PodCast-1.0.dtd": "itunes", } def _start_itunes_author(self, attrs_d): @@ -73,37 +72,42 @@ class Namespace(object): def _start_itunes_owner(self, attrs_d): self.inpublisher = 1 - self.push('publisher', 0) + self.push("publisher", 0) def _end_itunes_owner(self): - self.pop('publisher') + self.pop("publisher") self.inpublisher = 0 - self._sync_author_detail('publisher') + self._sync_author_detail("publisher") def _end_itunes_keywords(self): - for term in self.pop('itunes_keywords').split(','): + for term in self.pop("itunes_keywords").split(","): if term.strip(): - self._add_tag(term.strip(), 'http://www.itunes.com/', None) + self._add_tag(term.strip(), "http://www.itunes.com/", None) def _start_itunes_category(self, attrs_d): - self._add_tag(attrs_d.get('text'), 'http://www.itunes.com/', None) - self.push('category', 1) + self._add_tag(attrs_d.get("text"), "http://www.itunes.com/", None) + self.push("category", 1) def _start_itunes_image(self, attrs_d): - self.push('itunes_image', 0) - if attrs_d.get('href'): - self._get_context()['image'] = FeedParserDict({'href': attrs_d.get('href')}) - elif attrs_d.get('url'): - self._get_context()['image'] = FeedParserDict({'href': attrs_d.get('url')}) + self.push("itunes_image", 0) + if attrs_d.get("href"): + self._get_context()["image"] = FeedParserDict({"href": attrs_d.get("href")}) + elif attrs_d.get("url"): + self._get_context()["image"] = FeedParserDict({"href": attrs_d.get("url")}) + _start_itunes_link = _start_itunes_image def _end_itunes_block(self): - value = self.pop('itunes_block', 0) - self._get_context()['itunes_block'] = (value == 'yes' or value == 'Yes') and 1 or 0 + value = self.pop("itunes_block", 0) + self._get_context()["itunes_block"] = ( + (value == "yes" or value == "Yes") and 1 or 0 + ) def _end_itunes_explicit(self): - value = self.pop('itunes_explicit', 0) + value = self.pop("itunes_explicit", 0) # Convert 'yes' -> True, 'clean' to False, and any other value to None # False and None both evaluate as False, so the difference can be ignored # by applications that only need to know if the content is explicit. - self._get_context()['itunes_explicit'] = (None, False, True)[(value == 'yes' and 2) or value == 'clean' or 0] + self._get_context()["itunes_explicit"] = (None, False, True)[ + (value == "yes" and 2) or value == "clean" or 0 + ] diff --git a/lib/feedparser/namespaces/mediarss.py b/lib/feedparser/namespaces/mediarss.py index 2298ad2f..5ec4b67b 100644 --- a/lib/feedparser/namespaces/mediarss.py +++ b/lib/feedparser/namespaces/mediarss.py @@ -1,5 +1,5 @@ # Support for the Media RSS format -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -29,24 +29,23 @@ from ..util import FeedParserDict -class Namespace(object): +class Namespace: supported_namespaces = { # Canonical namespace - 'http://search.yahoo.com/mrss/': 'media', - + "http://search.yahoo.com/mrss/": "media", # Old namespace (no trailing slash) - 'http://search.yahoo.com/mrss': 'media', + "http://search.yahoo.com/mrss": "media", } def _start_media_category(self, attrs_d): - attrs_d.setdefault('scheme', 'http://search.yahoo.com/mrss/category_schema') + attrs_d.setdefault("scheme", "http://search.yahoo.com/mrss/category_schema") self._start_category(attrs_d) def _end_media_category(self): self._end_category() def _end_media_keywords(self): - for term in self.pop('media_keywords').split(','): + for term in self.pop("media_keywords").split(","): if term.strip(): self._add_tag(term.strip(), None, None) @@ -64,26 +63,26 @@ class Namespace(object): def _start_media_rating(self, attrs_d): context = self._get_context() - context.setdefault('media_rating', attrs_d) - self.push('rating', 1) + context.setdefault("media_rating", attrs_d) + self.push("rating", 1) def _end_media_rating(self): - rating = self.pop('rating') + rating = self.pop("rating") if rating is not None and rating.strip(): context = self._get_context() - context['media_rating']['content'] = rating + context["media_rating"]["content"] = rating def _start_media_credit(self, attrs_d): context = self._get_context() - context.setdefault('media_credit', []) - context['media_credit'].append(attrs_d) - self.push('credit', 1) + context.setdefault("media_credit", []) + context["media_credit"].append(attrs_d) + self.push("credit", 1) def _end_media_credit(self): - credit = self.pop('credit') + credit = self.pop("credit") if credit is not None and credit.strip(): context = self._get_context() - context['media_credit'][-1]['content'] = credit + context["media_credit"][-1]["content"] = credit def _start_media_description(self, attrs_d): self._start_description(attrs_d) @@ -93,49 +92,51 @@ class Namespace(object): def _start_media_restriction(self, attrs_d): context = self._get_context() - context.setdefault('media_restriction', attrs_d) - self.push('restriction', 1) + context.setdefault("media_restriction", attrs_d) + self.push("restriction", 1) def _end_media_restriction(self): - restriction = self.pop('restriction') + restriction = self.pop("restriction") if restriction is not None and restriction.strip(): context = self._get_context() - context['media_restriction']['content'] = [cc.strip().lower() for cc in restriction.split(' ')] + context["media_restriction"]["content"] = [ + cc.strip().lower() for cc in restriction.split(" ") + ] def _start_media_license(self, attrs_d): context = self._get_context() - context.setdefault('media_license', attrs_d) - self.push('license', 1) + context.setdefault("media_license", attrs_d) + self.push("license", 1) def _end_media_license(self): - license_ = self.pop('license') + license_ = self.pop("license") if license_ is not None and license_.strip(): context = self._get_context() - context['media_license']['content'] = license_ + context["media_license"]["content"] = license_ def _start_media_content(self, attrs_d): context = self._get_context() - context.setdefault('media_content', []) - context['media_content'].append(attrs_d) + context.setdefault("media_content", []) + context["media_content"].append(attrs_d) def _start_media_thumbnail(self, attrs_d): context = self._get_context() - context.setdefault('media_thumbnail', []) - self.push('url', 1) # new - context['media_thumbnail'].append(attrs_d) + context.setdefault("media_thumbnail", []) + self.push("url", 1) # new + context["media_thumbnail"].append(attrs_d) def _end_media_thumbnail(self): - url = self.pop('url') + url = self.pop("url") context = self._get_context() if url is not None and url.strip(): - if 'url' not in context['media_thumbnail'][-1]: - context['media_thumbnail'][-1]['url'] = url + if "url" not in context["media_thumbnail"][-1]: + context["media_thumbnail"][-1]["url"] = url def _start_media_player(self, attrs_d): - self.push('media_player', 0) - self._get_context()['media_player'] = FeedParserDict(attrs_d) + self.push("media_player", 0) + self._get_context()["media_player"] = FeedParserDict(attrs_d) def _end_media_player(self): - value = self.pop('media_player') + value = self.pop("media_player") context = self._get_context() - context['media_player']['content'] = value + context["media_player"]["content"] = value diff --git a/lib/feedparser/namespaces/psc.py b/lib/feedparser/namespaces/psc.py index a440bd68..4a43d6df 100644 --- a/lib/feedparser/namespaces/psc.py +++ b/lib/feedparser/namespaces/psc.py @@ -1,5 +1,5 @@ # Support for the Podlove Simple Chapters format -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -32,36 +32,36 @@ import re from .. import util -class Namespace(object): +class Namespace: supported_namespaces = { - 'http://podlove.org/simple-chapters': 'psc', + "http://podlove.org/simple-chapters": "psc", } def __init__(self): # chapters will only be captured while psc_chapters_flag is True. self.psc_chapters_flag = False - super(Namespace, self).__init__() + super().__init__() def _start_psc_chapters(self, attrs_d): context = self._get_context() - if 'psc_chapters' not in context: + if "psc_chapters" not in context: self.psc_chapters_flag = True - attrs_d['chapters'] = [] - context['psc_chapters'] = util.FeedParserDict(attrs_d) + attrs_d["chapters"] = [] + context["psc_chapters"] = util.FeedParserDict(attrs_d) def _end_psc_chapters(self): self.psc_chapters_flag = False def _start_psc_chapter(self, attrs_d): if self.psc_chapters_flag: - start = self._get_attribute(attrs_d, 'start') - attrs_d['start_parsed'] = _parse_psc_chapter_start(start) + start = self._get_attribute(attrs_d, "start") + attrs_d["start_parsed"] = _parse_psc_chapter_start(start) - context = self._get_context()['psc_chapters'] - context['chapters'].append(util.FeedParserDict(attrs_d)) + context = self._get_context()["psc_chapters"] + context["chapters"].append(util.FeedParserDict(attrs_d)) -format_ = re.compile(r'^((\d{2}):)?(\d{2}):(\d{2})(\.(\d{3}))?$') +format_ = re.compile(r"^((\d{2}):)?(\d{2}):(\d{2})(\.(\d{3}))?$") def _parse_psc_chapter_start(start): @@ -71,4 +71,4 @@ def _parse_psc_chapter_start(start): _, h, m, s, _, ms = m.groups() h, m, s, ms = (int(h or 0), int(m), int(s), int(ms or 0)) - return datetime.timedelta(0, h*60*60 + m*60 + s, ms*1000) + return datetime.timedelta(0, h * 60 * 60 + m * 60 + s, ms * 1000) diff --git a/lib/feedparser/parsers/json.py b/lib/feedparser/parsers/json.py index ae43163c..36f714a5 100644 --- a/lib/feedparser/parsers/json.py +++ b/lib/feedparser/parsers/json.py @@ -34,37 +34,37 @@ from ..util import FeedParserDict class JSONParser: VERSIONS = { - 'https://jsonfeed.org/version/1': 'json1', - 'https://jsonfeed.org/version/1.1': 'json11', + "https://jsonfeed.org/version/1": "json1", + "https://jsonfeed.org/version/1.1": "json11", } FEED_FIELDS = ( - ('title', 'title'), - ('icon', 'image'), - ('home_page_url', 'link'), - ('description', 'description'), + ("title", "title"), + ("icon", "image"), + ("home_page_url", "link"), + ("description", "description"), ) ITEM_FIELDS = ( - ('title', 'title'), - ('id', 'guid'), - ('url', 'link'), - ('summary', 'summary'), - ('external_url', 'source'), + ("title", "title"), + ("id", "guid"), + ("url", "link"), + ("summary", "summary"), + ("external_url", "source"), ) def __init__(self, baseuri=None, baselang=None, encoding=None): - self.baseuri = baseuri or '' + self.baseuri = baseuri or "" self.lang = baselang or None - self.encoding = encoding or 'utf-8' # character encoding + self.encoding = encoding or "utf-8" # character encoding self.version = None self.feeddata = FeedParserDict() self.namespacesInUse = [] self.entries = [] - def feed(self, data): - data = json.loads(data) + def feed(self, file): + data = json.load(file) - v = data.get('version', '') + v = data.get("version", "") try: self.version = self.VERSIONS[v] except KeyError: @@ -73,11 +73,11 @@ class JSONParser: for src, dst in self.FEED_FIELDS: if src in data: self.feeddata[dst] = data[src] - if 'author' in data: - self.parse_author(data['author'], self.feeddata) + if "author" in data: + self.parse_author(data["author"], self.feeddata) # TODO: hubs; expired has no RSS equivalent - self.entries = [self.parse_entry(e) for e in data['items']] + self.entries = [self.parse_entry(e) for e in data["items"]] def parse_entry(self, e): entry = FeedParserDict() @@ -85,49 +85,51 @@ class JSONParser: if src in e: entry[dst] = e[src] - if 'content_text' in e: - entry['content'] = c = FeedParserDict() - c['value'] = e['content_text'] - c['type'] = 'text' - elif 'content_html' in e: - entry['content'] = c = FeedParserDict() - c['value'] = sanitize_html(e['content_html'], self.encoding, 'application/json') - c['type'] = 'html' + if "content_text" in e: + entry["content"] = c = FeedParserDict() + c["value"] = e["content_text"] + c["type"] = "text" + elif "content_html" in e: + entry["content"] = c = FeedParserDict() + c["value"] = sanitize_html( + e["content_html"], self.encoding, "application/json" + ) + c["type"] = "html" - if 'date_published' in e: - entry['published'] = e['date_published'] - entry['published_parsed'] = _parse_date(e['date_published']) - if 'date_updated' in e: - entry['updated'] = e['date_modified'] - entry['updated_parsed'] = _parse_date(e['date_modified']) + if "date_published" in e: + entry["published"] = e["date_published"] + entry["published_parsed"] = _parse_date(e["date_published"]) + if "date_updated" in e: + entry["updated"] = e["date_modified"] + entry["updated_parsed"] = _parse_date(e["date_modified"]) - if 'tags' in e: - entry['category'] = e['tags'] + if "tags" in e: + entry["category"] = e["tags"] - if 'author' in e: - self.parse_author(e['author'], entry) + if "author" in e: + self.parse_author(e["author"], entry) - if 'attachments' in e: - entry['enclosures'] = [self.parse_attachment(a) for a in e['attachments']] + if "attachments" in e: + entry["enclosures"] = [self.parse_attachment(a) for a in e["attachments"]] return entry @staticmethod def parse_author(parent, dest): - dest['author_detail'] = detail = FeedParserDict() - if 'name' in parent: - dest['author'] = detail['name'] = parent['name'] - if 'url' in parent: - if parent['url'].startswith('mailto:'): - detail['email'] = parent['url'][7:] + dest["author_detail"] = detail = FeedParserDict() + if "name" in parent: + dest["author"] = detail["name"] = parent["name"] + if "url" in parent: + if parent["url"].startswith("mailto:"): + detail["email"] = parent["url"][7:] else: - detail['href'] = parent['url'] + detail["href"] = parent["url"] @staticmethod def parse_attachment(attachment): enc = FeedParserDict() - enc['href'] = attachment['url'] - enc['type'] = attachment['mime_type'] - if 'size_in_bytes' in attachment: - enc['length'] = attachment['size_in_bytes'] + enc["href"] = attachment["url"] + enc["type"] = attachment["mime_type"] + if "size_in_bytes" in attachment: + enc["length"] = attachment["size_in_bytes"] return enc diff --git a/lib/feedparser/parsers/loose.py b/lib/feedparser/parsers/loose.py index 3f22bfb4..57285077 100644 --- a/lib/feedparser/parsers/loose.py +++ b/lib/feedparser/parsers/loose.py @@ -1,5 +1,5 @@ # The loose feed parser that interfaces with an SGML parsing library -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -26,52 +26,50 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. + class LooseXMLParser: contentparams = None def __init__(self, baseuri=None, baselang=None, encoding=None, entities=None): - self.baseuri = baseuri or '' + self.baseuri = baseuri or "" self.lang = baselang or None - self.encoding = encoding or 'utf-8' # character encoding + self.encoding = encoding or "utf-8" # character encoding self.entities = entities or {} super().__init__() @staticmethod def _normalize_attributes(kv): k = kv[0].lower() - v = k in ('rel', 'type') and kv[1].lower() or kv[1] + v = k in ("rel", "type") and kv[1].lower() or kv[1] # the sgml parser doesn't handle entities in attributes, nor # does it pass the attribute values through as unicode, while # strict xml parsers do -- account for this difference - v = v.replace('&', '&') + v = v.replace("&", "&") return k, v def decode_entities(self, element, data): - data = data.replace('<', '<') - data = data.replace('<', '<') - data = data.replace('<', '<') - data = data.replace('>', '>') - data = data.replace('>', '>') - data = data.replace('>', '>') - data = data.replace('&', '&') - data = data.replace('&', '&') - data = data.replace('"', '"') - data = data.replace('"', '"') - data = data.replace(''', ''') - data = data.replace(''', ''') - if not self.contentparams.get('type', 'xml').endswith('xml'): - data = data.replace('<', '<') - data = data.replace('>', '>') - data = data.replace('&', '&') - data = data.replace('"', '"') - data = data.replace(''', "'") - data = data.replace('/', '/') - data = data.replace('/', '/') + data = data.replace("<", "<") + data = data.replace("<", "<") + data = data.replace("<", "<") + data = data.replace(">", ">") + data = data.replace(">", ">") + data = data.replace(">", ">") + data = data.replace("&", "&") + data = data.replace("&", "&") + data = data.replace(""", """) + data = data.replace(""", """) + data = data.replace("'", "'") + data = data.replace("'", "'") + if not self.contentparams.get("type", "xml").endswith("xml"): + data = data.replace("<", "<") + data = data.replace(">", ">") + data = data.replace("&", "&") + data = data.replace(""", '"') + data = data.replace("'", "'") + data = data.replace("/", "/") + data = data.replace("/", "/") return data @staticmethod def strattrs(attrs): - return ''.join( - ' %s="%s"' % (n, v.replace('"', '"')) - for n, v in attrs - ) + return "".join(' {}="{}"'.format(n, v.replace('"', """)) for n, v in attrs) diff --git a/lib/feedparser/parsers/strict.py b/lib/feedparser/parsers/strict.py index 7b0386e5..4f701985 100644 --- a/lib/feedparser/parsers/strict.py +++ b/lib/feedparser/parsers/strict.py @@ -1,5 +1,5 @@ # The strict feed parser that interfaces with an XML parsing library -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -34,15 +34,15 @@ class StrictXMLParser: self.bozo = 0 self.exc = None self.decls = {} - self.baseuri = baseuri or '' + self.baseuri = baseuri or "" self.lang = baselang self.encoding = encoding - super(StrictXMLParser, self).__init__() + super().__init__() @staticmethod def _normalize_attributes(kv): k = kv[0].lower() - v = k in ('rel', 'type') and kv[1].lower() or kv[1] + v = k in ("rel", "type") and kv[1].lower() or kv[1] return k, v def startPrefixMapping(self, prefix, uri): @@ -51,23 +51,29 @@ class StrictXMLParser: # Jython uses '' instead of None; standardize on None prefix = prefix or None self.track_namespace(prefix, uri) - if prefix and uri == 'http://www.w3.org/1999/xlink': - self.decls['xmlns:' + prefix] = uri + if prefix and uri == "http://www.w3.org/1999/xlink": + self.decls["xmlns:" + prefix] = uri def startElementNS(self, name, qname, attrs): namespace, localname = name - lowernamespace = str(namespace or '').lower() - if lowernamespace.find('backend.userland.com/rss') != -1: + lowernamespace = str(namespace or "").lower() + if lowernamespace.find("backend.userland.com/rss") != -1: # match any backend.userland.com namespace - namespace = 'http://backend.userland.com/rss' + namespace = "http://backend.userland.com/rss" lowernamespace = namespace - if qname and qname.find(':') > 0: - givenprefix = qname.split(':')[0] + if qname and qname.find(":") > 0: + givenprefix = qname.split(":")[0] else: givenprefix = None prefix = self._matchnamespaces.get(lowernamespace, givenprefix) - if givenprefix and (prefix is None or (prefix == '' and lowernamespace == '')) and givenprefix not in self.namespaces_in_use: - raise UndeclaredNamespace("'%s' is not associated with a namespace" % givenprefix) + if ( + givenprefix + and (prefix is None or (prefix == "" and lowernamespace == "")) + and givenprefix not in self.namespaces_in_use + ): + raise UndeclaredNamespace( + "'%s' is not associated with a namespace" % givenprefix + ) localname = str(localname).lower() # qname implementation is horribly broken in Python 2.1 (it @@ -78,24 +84,24 @@ class StrictXMLParser: # at all). Thanks to MatejC for helping me test this and # tirelessly telling me that it didn't work yet. attrsD, self.decls = self.decls, {} - if localname == 'math' and namespace == 'http://www.w3.org/1998/Math/MathML': - attrsD['xmlns'] = namespace - if localname == 'svg' and namespace == 'http://www.w3.org/2000/svg': - attrsD['xmlns'] = namespace + if localname == "math" and namespace == "http://www.w3.org/1998/Math/MathML": + attrsD["xmlns"] = namespace + if localname == "svg" and namespace == "http://www.w3.org/2000/svg": + attrsD["xmlns"] = namespace if prefix: - localname = prefix.lower() + ':' + localname + localname = prefix.lower() + ":" + localname elif namespace and not qname: # Expat for name, value in self.namespaces_in_use.items(): if name and value == namespace: - localname = name + ':' + localname + localname = name + ":" + localname break for (namespace, attrlocalname), attrvalue in attrs.items(): - lowernamespace = (namespace or '').lower() - prefix = self._matchnamespaces.get(lowernamespace, '') + lowernamespace = (namespace or "").lower() + prefix = self._matchnamespaces.get(lowernamespace, "") if prefix: - attrlocalname = prefix + ':' + attrlocalname + attrlocalname = prefix + ":" + attrlocalname attrsD[str(attrlocalname).lower()] = attrvalue for qname in attrs.getQNames(): attrsD[str(qname).lower()] = attrs.getValueByQName(qname) @@ -107,18 +113,18 @@ class StrictXMLParser: def endElementNS(self, name, qname): namespace, localname = name - lowernamespace = str(namespace or '').lower() - if qname and qname.find(':') > 0: - givenprefix = qname.split(':')[0] + lowernamespace = str(namespace or "").lower() + if qname and qname.find(":") > 0: + givenprefix = qname.split(":")[0] else: - givenprefix = '' + givenprefix = "" prefix = self._matchnamespaces.get(lowernamespace, givenprefix) if prefix: - localname = prefix + ':' + localname + localname = prefix + ":" + localname elif namespace and not qname: # Expat for name, value in self.namespaces_in_use.items(): if name and value == namespace: - localname = name + ':' + localname + localname = name + ":" + localname break localname = str(localname).lower() self.unknown_endtag(localname) diff --git a/lib/feedparser/py.typed b/lib/feedparser/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/lib/feedparser/sanitizer.py b/lib/feedparser/sanitizer.py index 5b729830..76be5700 100644 --- a/lib/feedparser/sanitizer.py +++ b/lib/feedparser/sanitizer.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -25,6 +25,8 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +from __future__ import annotations + import re from .html import BaseHTMLProcessor @@ -33,705 +35,705 @@ from .urls import make_safe_absolute_uri class HTMLSanitizer(BaseHTMLProcessor): acceptable_elements = { - 'a', - 'abbr', - 'acronym', - 'address', - 'area', - 'article', - 'aside', - 'audio', - 'b', - 'big', - 'blockquote', - 'br', - 'button', - 'canvas', - 'caption', - 'center', - 'cite', - 'code', - 'col', - 'colgroup', - 'command', - 'datagrid', - 'datalist', - 'dd', - 'del', - 'details', - 'dfn', - 'dialog', - 'dir', - 'div', - 'dl', - 'dt', - 'em', - 'event-source', - 'fieldset', - 'figcaption', - 'figure', - 'font', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'header', - 'hr', - 'i', - 'img', - 'input', - 'ins', - 'kbd', - 'keygen', - 'label', - 'legend', - 'li', - 'm', - 'map', - 'menu', - 'meter', - 'multicol', - 'nav', - 'nextid', - 'noscript', - 'ol', - 'optgroup', - 'option', - 'output', - 'p', - 'pre', - 'progress', - 'q', - 's', - 'samp', - 'section', - 'select', - 'small', - 'sound', - 'source', - 'spacer', - 'span', - 'strike', - 'strong', - 'sub', - 'sup', - 'table', - 'tbody', - 'td', - 'textarea', - 'tfoot', - 'th', - 'thead', - 'time', - 'tr', - 'tt', - 'u', - 'ul', - 'var', - 'video', + "a", + "abbr", + "acronym", + "address", + "area", + "article", + "aside", + "audio", + "b", + "big", + "blockquote", + "br", + "button", + "canvas", + "caption", + "center", + "cite", + "code", + "col", + "colgroup", + "command", + "datagrid", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "dir", + "div", + "dl", + "dt", + "em", + "event-source", + "fieldset", + "figcaption", + "figure", + "font", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "header", + "hr", + "i", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "m", + "map", + "menu", + "meter", + "multicol", + "nav", + "nextid", + "noscript", + "ol", + "optgroup", + "option", + "output", + "p", + "pre", + "progress", + "q", + "s", + "samp", + "section", + "select", + "small", + "sound", + "source", + "spacer", + "span", + "strike", + "strong", + "sub", + "sup", + "table", + "tbody", + "td", + "textarea", + "tfoot", + "th", + "thead", + "time", + "tr", + "tt", + "u", + "ul", + "var", + "video", } acceptable_attributes = { - 'abbr', - 'accept', - 'accept-charset', - 'accesskey', - 'action', - 'align', - 'alt', - 'autocomplete', - 'autofocus', - 'axis', - 'background', - 'balance', - 'bgcolor', - 'bgproperties', - 'border', - 'bordercolor', - 'bordercolordark', - 'bordercolorlight', - 'bottompadding', - 'cellpadding', - 'cellspacing', - 'ch', - 'challenge', - 'char', - 'charoff', - 'charset', - 'checked', - 'choff', - 'cite', - 'class', - 'clear', - 'color', - 'cols', - 'colspan', - 'compact', - 'contenteditable', - 'controls', - 'coords', - 'data', - 'datafld', - 'datapagesize', - 'datasrc', - 'datetime', - 'default', - 'delay', - 'dir', - 'disabled', - 'draggable', - 'dynsrc', - 'enctype', - 'end', - 'face', - 'for', - 'form', - 'frame', - 'galleryimg', - 'gutter', - 'headers', - 'height', - 'hidden', - 'hidefocus', - 'high', - 'href', - 'hreflang', - 'hspace', - 'icon', - 'id', - 'inputmode', - 'ismap', - 'keytype', - 'label', - 'lang', - 'leftspacing', - 'list', - 'longdesc', - 'loop', - 'loopcount', - 'loopend', - 'loopstart', - 'low', - 'lowsrc', - 'max', - 'maxlength', - 'media', - 'method', - 'min', - 'multiple', - 'name', - 'nohref', - 'noshade', - 'nowrap', - 'open', - 'optimum', - 'pattern', - 'ping', - 'point-size', - 'poster', - 'pqg', - 'preload', - 'prompt', - 'radiogroup', - 'readonly', - 'rel', - 'repeat-max', - 'repeat-min', - 'replace', - 'required', - 'rev', - 'rightspacing', - 'rows', - 'rowspan', - 'rules', - 'scope', - 'selected', - 'shape', - 'size', - 'span', - 'src', - 'start', - 'step', - 'style', - 'summary', - 'suppress', - 'tabindex', - 'target', - 'template', - 'title', - 'toppadding', - 'type', - 'unselectable', - 'urn', - 'usemap', - 'valign', - 'value', - 'variable', - 'volume', - 'vrml', - 'vspace', - 'width', - 'wrap', - 'xml:lang', + "abbr", + "accept", + "accept-charset", + "accesskey", + "action", + "align", + "alt", + "autocomplete", + "autofocus", + "axis", + "background", + "balance", + "bgcolor", + "bgproperties", + "border", + "bordercolor", + "bordercolordark", + "bordercolorlight", + "bottompadding", + "cellpadding", + "cellspacing", + "ch", + "challenge", + "char", + "charoff", + "charset", + "checked", + "choff", + "cite", + "class", + "clear", + "color", + "cols", + "colspan", + "compact", + "contenteditable", + "controls", + "coords", + "data", + "datafld", + "datapagesize", + "datasrc", + "datetime", + "default", + "delay", + "dir", + "disabled", + "draggable", + "dynsrc", + "enctype", + "end", + "face", + "for", + "form", + "frame", + "galleryimg", + "gutter", + "headers", + "height", + "hidden", + "hidefocus", + "high", + "href", + "hreflang", + "hspace", + "icon", + "id", + "inputmode", + "ismap", + "keytype", + "label", + "lang", + "leftspacing", + "list", + "longdesc", + "loop", + "loopcount", + "loopend", + "loopstart", + "low", + "lowsrc", + "max", + "maxlength", + "media", + "method", + "min", + "multiple", + "name", + "nohref", + "noshade", + "nowrap", + "open", + "optimum", + "pattern", + "ping", + "point-size", + "poster", + "pqg", + "preload", + "prompt", + "radiogroup", + "readonly", + "rel", + "repeat-max", + "repeat-min", + "replace", + "required", + "rev", + "rightspacing", + "rows", + "rowspan", + "rules", + "scope", + "selected", + "shape", + "size", + "span", + "src", + "start", + "step", + "style", + "summary", + "suppress", + "tabindex", + "target", + "template", + "title", + "toppadding", + "type", + "unselectable", + "urn", + "usemap", + "valign", + "value", + "variable", + "volume", + "vrml", + "vspace", + "width", + "wrap", + "xml:lang", } unacceptable_elements_with_end_tag = { - 'applet', - 'script', - 'style', + "applet", + "script", + "style", } acceptable_css_properties = { - 'azimuth', - 'background-color', - 'border-bottom-color', - 'border-collapse', - 'border-color', - 'border-left-color', - 'border-right-color', - 'border-top-color', - 'clear', - 'color', - 'cursor', - 'direction', - 'display', - 'elevation', - 'float', - 'font', - 'font-family', - 'font-size', - 'font-style', - 'font-variant', - 'font-weight', - 'height', - 'letter-spacing', - 'line-height', - 'overflow', - 'pause', - 'pause-after', - 'pause-before', - 'pitch', - 'pitch-range', - 'richness', - 'speak', - 'speak-header', - 'speak-numeral', - 'speak-punctuation', - 'speech-rate', - 'stress', - 'text-align', - 'text-decoration', - 'text-indent', - 'unicode-bidi', - 'vertical-align', - 'voice-family', - 'volume', - 'white-space', - 'width', + "azimuth", + "background-color", + "border-bottom-color", + "border-collapse", + "border-color", + "border-left-color", + "border-right-color", + "border-top-color", + "clear", + "color", + "cursor", + "direction", + "display", + "elevation", + "float", + "font", + "font-family", + "font-size", + "font-style", + "font-variant", + "font-weight", + "height", + "letter-spacing", + "line-height", + "overflow", + "pause", + "pause-after", + "pause-before", + "pitch", + "pitch-range", + "richness", + "speak", + "speak-header", + "speak-numeral", + "speak-punctuation", + "speech-rate", + "stress", + "text-align", + "text-decoration", + "text-indent", + "unicode-bidi", + "vertical-align", + "voice-family", + "volume", + "white-space", + "width", } # survey of common keywords found in feeds acceptable_css_keywords = { - '!important', - 'aqua', - 'auto', - 'black', - 'block', - 'blue', - 'bold', - 'both', - 'bottom', - 'brown', - 'center', - 'collapse', - 'dashed', - 'dotted', - 'fuchsia', - 'gray', - 'green', - 'italic', - 'left', - 'lime', - 'maroon', - 'medium', - 'navy', - 'none', - 'normal', - 'nowrap', - 'olive', - 'pointer', - 'purple', - 'red', - 'right', - 'silver', - 'solid', - 'teal', - 'top', - 'transparent', - 'underline', - 'white', - 'yellow', + "!important", + "aqua", + "auto", + "black", + "block", + "blue", + "bold", + "both", + "bottom", + "brown", + "center", + "collapse", + "dashed", + "dotted", + "fuchsia", + "gray", + "green", + "italic", + "left", + "lime", + "maroon", + "medium", + "navy", + "none", + "normal", + "nowrap", + "olive", + "pointer", + "purple", + "red", + "right", + "silver", + "solid", + "teal", + "top", + "transparent", + "underline", + "white", + "yellow", } valid_css_values = re.compile( - r'^(' - r'#[0-9a-f]+' # Hex values - r'|rgb\(\d+%?,\d*%?,?\d*%?\)?' # RGB values - r'|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?' # Sizes/widths - r')$' + r"^(" + r"#[0-9a-f]+" # Hex values + r"|rgb\(\d+%?,\d*%?,?\d*%?\)?" # RGB values + r"|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?" # Sizes/widths + r")$" ) mathml_elements = { - 'annotation', - 'annotation-xml', - 'maction', - 'maligngroup', - 'malignmark', - 'math', - 'menclose', - 'merror', - 'mfenced', - 'mfrac', - 'mglyph', - 'mi', - 'mlabeledtr', - 'mlongdiv', - 'mmultiscripts', - 'mn', - 'mo', - 'mover', - 'mpadded', - 'mphantom', - 'mprescripts', - 'mroot', - 'mrow', - 'ms', - 'mscarries', - 'mscarry', - 'msgroup', - 'msline', - 'mspace', - 'msqrt', - 'msrow', - 'mstack', - 'mstyle', - 'msub', - 'msubsup', - 'msup', - 'mtable', - 'mtd', - 'mtext', - 'mtr', - 'munder', - 'munderover', - 'none', - 'semantics', + "annotation", + "annotation-xml", + "maction", + "maligngroup", + "malignmark", + "math", + "menclose", + "merror", + "mfenced", + "mfrac", + "mglyph", + "mi", + "mlabeledtr", + "mlongdiv", + "mmultiscripts", + "mn", + "mo", + "mover", + "mpadded", + "mphantom", + "mprescripts", + "mroot", + "mrow", + "ms", + "mscarries", + "mscarry", + "msgroup", + "msline", + "mspace", + "msqrt", + "msrow", + "mstack", + "mstyle", + "msub", + "msubsup", + "msup", + "mtable", + "mtd", + "mtext", + "mtr", + "munder", + "munderover", + "none", + "semantics", } mathml_attributes = { - 'accent', - 'accentunder', - 'actiontype', - 'align', - 'alignmentscope', - 'altimg', - 'altimg-height', - 'altimg-valign', - 'altimg-width', - 'alttext', - 'bevelled', - 'charalign', - 'close', - 'columnalign', - 'columnlines', - 'columnspacing', - 'columnspan', - 'columnwidth', - 'crossout', - 'decimalpoint', - 'denomalign', - 'depth', - 'dir', - 'display', - 'displaystyle', - 'edge', - 'encoding', - 'equalcolumns', - 'equalrows', - 'fence', - 'fontstyle', - 'fontweight', - 'form', - 'frame', - 'framespacing', - 'groupalign', - 'height', - 'href', - 'id', - 'indentalign', - 'indentalignfirst', - 'indentalignlast', - 'indentshift', - 'indentshiftfirst', - 'indentshiftlast', - 'indenttarget', - 'infixlinebreakstyle', - 'largeop', - 'length', - 'linebreak', - 'linebreakmultchar', - 'linebreakstyle', - 'lineleading', - 'linethickness', - 'location', - 'longdivstyle', - 'lquote', - 'lspace', - 'mathbackground', - 'mathcolor', - 'mathsize', - 'mathvariant', - 'maxsize', - 'minlabelspacing', - 'minsize', - 'movablelimits', - 'notation', - 'numalign', - 'open', - 'other', - 'overflow', - 'position', - 'rowalign', - 'rowlines', - 'rowspacing', - 'rowspan', - 'rquote', - 'rspace', - 'scriptlevel', - 'scriptminsize', - 'scriptsizemultiplier', - 'selection', - 'separator', - 'separators', - 'shift', - 'side', - 'src', - 'stackalign', - 'stretchy', - 'subscriptshift', - 'superscriptshift', - 'symmetric', - 'voffset', - 'width', - 'xlink:href', - 'xlink:show', - 'xlink:type', - 'xmlns', - 'xmlns:xlink', + "accent", + "accentunder", + "actiontype", + "align", + "alignmentscope", + "altimg", + "altimg-height", + "altimg-valign", + "altimg-width", + "alttext", + "bevelled", + "charalign", + "close", + "columnalign", + "columnlines", + "columnspacing", + "columnspan", + "columnwidth", + "crossout", + "decimalpoint", + "denomalign", + "depth", + "dir", + "display", + "displaystyle", + "edge", + "encoding", + "equalcolumns", + "equalrows", + "fence", + "fontstyle", + "fontweight", + "form", + "frame", + "framespacing", + "groupalign", + "height", + "href", + "id", + "indentalign", + "indentalignfirst", + "indentalignlast", + "indentshift", + "indentshiftfirst", + "indentshiftlast", + "indenttarget", + "infixlinebreakstyle", + "largeop", + "length", + "linebreak", + "linebreakmultchar", + "linebreakstyle", + "lineleading", + "linethickness", + "location", + "longdivstyle", + "lquote", + "lspace", + "mathbackground", + "mathcolor", + "mathsize", + "mathvariant", + "maxsize", + "minlabelspacing", + "minsize", + "movablelimits", + "notation", + "numalign", + "open", + "other", + "overflow", + "position", + "rowalign", + "rowlines", + "rowspacing", + "rowspan", + "rquote", + "rspace", + "scriptlevel", + "scriptminsize", + "scriptsizemultiplier", + "selection", + "separator", + "separators", + "shift", + "side", + "src", + "stackalign", + "stretchy", + "subscriptshift", + "superscriptshift", + "symmetric", + "voffset", + "width", + "xlink:href", + "xlink:show", + "xlink:type", + "xmlns", + "xmlns:xlink", } # svgtiny - foreignObject + linearGradient + radialGradient + stop svg_elements = { - 'a', - 'animate', - 'animateColor', - 'animateMotion', - 'animateTransform', - 'circle', - 'defs', - 'desc', - 'ellipse', - 'font-face', - 'font-face-name', - 'font-face-src', - 'foreignObject', - 'g', - 'glyph', - 'hkern', - 'line', - 'linearGradient', - 'marker', - 'metadata', - 'missing-glyph', - 'mpath', - 'path', - 'polygon', - 'polyline', - 'radialGradient', - 'rect', - 'set', - 'stop', - 'svg', - 'switch', - 'text', - 'title', - 'tspan', - 'use', + "a", + "animate", + "animateColor", + "animateMotion", + "animateTransform", + "circle", + "defs", + "desc", + "ellipse", + "font-face", + "font-face-name", + "font-face-src", + "foreignObject", + "g", + "glyph", + "hkern", + "line", + "linearGradient", + "marker", + "metadata", + "missing-glyph", + "mpath", + "path", + "polygon", + "polyline", + "radialGradient", + "rect", + "set", + "stop", + "svg", + "switch", + "text", + "title", + "tspan", + "use", } # svgtiny + class + opacity + offset + xmlns + xmlns:xlink svg_attributes = { - 'accent-height', - 'accumulate', - 'additive', - 'alphabetic', - 'arabic-form', - 'ascent', - 'attributeName', - 'attributeType', - 'baseProfile', - 'bbox', - 'begin', - 'by', - 'calcMode', - 'cap-height', - 'class', - 'color', - 'color-rendering', - 'content', - 'cx', - 'cy', - 'd', - 'descent', - 'display', - 'dur', - 'dx', - 'dy', - 'end', - 'fill', - 'fill-opacity', - 'fill-rule', - 'font-family', - 'font-size', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'from', - 'fx', - 'fy', - 'g1', - 'g2', - 'glyph-name', - 'gradientUnits', - 'hanging', - 'height', - 'horiz-adv-x', - 'horiz-origin-x', - 'id', - 'ideographic', - 'k', - 'keyPoints', - 'keySplines', - 'keyTimes', - 'lang', - 'marker-end', - 'marker-mid', - 'marker-start', - 'markerHeight', - 'markerUnits', - 'markerWidth', - 'mathematical', - 'max', - 'min', - 'name', - 'offset', - 'opacity', - 'orient', - 'origin', - 'overline-position', - 'overline-thickness', - 'panose-1', - 'path', - 'pathLength', - 'points', - 'preserveAspectRatio', - 'r', - 'refX', - 'refY', - 'repeatCount', - 'repeatDur', - 'requiredExtensions', - 'requiredFeatures', - 'restart', - 'rotate', - 'rx', - 'ry', - 'slope', - 'stemh', - 'stemv', - 'stop-color', - 'stop-opacity', - 'strikethrough-position', - 'strikethrough-thickness', - 'stroke', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke-width', - 'systemLanguage', - 'target', - 'text-anchor', - 'to', - 'transform', - 'type', - 'u1', - 'u2', - 'underline-position', - 'underline-thickness', - 'unicode', - 'unicode-range', - 'units-per-em', - 'values', - 'version', - 'viewBox', - 'visibility', - 'width', - 'widths', - 'x', - 'x-height', - 'x1', - 'x2', - 'xlink:actuate', - 'xlink:arcrole', - 'xlink:href', - 'xlink:role', - 'xlink:show', - 'xlink:title', - 'xlink:type', - 'xml:base', - 'xml:lang', - 'xml:space', - 'xmlns', - 'xmlns:xlink', - 'y', - 'y1', - 'y2', - 'zoomAndPan', + "accent-height", + "accumulate", + "additive", + "alphabetic", + "arabic-form", + "ascent", + "attributeName", + "attributeType", + "baseProfile", + "bbox", + "begin", + "by", + "calcMode", + "cap-height", + "class", + "color", + "color-rendering", + "content", + "cx", + "cy", + "d", + "descent", + "display", + "dur", + "dx", + "dy", + "end", + "fill", + "fill-opacity", + "fill-rule", + "font-family", + "font-size", + "font-stretch", + "font-style", + "font-variant", + "font-weight", + "from", + "fx", + "fy", + "g1", + "g2", + "glyph-name", + "gradientUnits", + "hanging", + "height", + "horiz-adv-x", + "horiz-origin-x", + "id", + "ideographic", + "k", + "keyPoints", + "keySplines", + "keyTimes", + "lang", + "marker-end", + "marker-mid", + "marker-start", + "markerHeight", + "markerUnits", + "markerWidth", + "mathematical", + "max", + "min", + "name", + "offset", + "opacity", + "orient", + "origin", + "overline-position", + "overline-thickness", + "panose-1", + "path", + "pathLength", + "points", + "preserveAspectRatio", + "r", + "refX", + "refY", + "repeatCount", + "repeatDur", + "requiredExtensions", + "requiredFeatures", + "restart", + "rotate", + "rx", + "ry", + "slope", + "stemh", + "stemv", + "stop-color", + "stop-opacity", + "strikethrough-position", + "strikethrough-thickness", + "stroke", + "stroke-dasharray", + "stroke-dashoffset", + "stroke-linecap", + "stroke-linejoin", + "stroke-miterlimit", + "stroke-opacity", + "stroke-width", + "systemLanguage", + "target", + "text-anchor", + "to", + "transform", + "type", + "u1", + "u2", + "underline-position", + "underline-thickness", + "unicode", + "unicode-range", + "units-per-em", + "values", + "version", + "viewBox", + "visibility", + "width", + "widths", + "x", + "x-height", + "x1", + "x2", + "xlink:actuate", + "xlink:arcrole", + "xlink:href", + "xlink:role", + "xlink:show", + "xlink:title", + "xlink:type", + "xml:base", + "xml:lang", + "xml:space", + "xmlns", + "xmlns:xlink", + "y", + "y1", + "y2", + "zoomAndPan", } svg_attr_map = None svg_elem_map = None acceptable_svg_properties = { - 'fill', - 'fill-opacity', - 'fill-rule', - 'stroke', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-opacity', - 'stroke-width', + "fill", + "fill-opacity", + "fill-rule", + "stroke", + "stroke-linecap", + "stroke-linejoin", + "stroke-opacity", + "stroke-width", } - def __init__(self, encoding=None, _type='application/xhtml+xml'): + def __init__(self, encoding=None, _type="application/xhtml+xml"): super().__init__(encoding, _type) self.unacceptablestack = 0 @@ -752,17 +754,20 @@ class HTMLSanitizer(BaseHTMLProcessor): self.unacceptablestack += 1 # add implicit namespaces to html5 inline svg/mathml - if self._type.endswith('html'): - if not dict(attrs).get('xmlns'): - if tag == 'svg': - attrs.append(('xmlns', 'http://www.w3.org/2000/svg')) - if tag == 'math': - attrs.append(('xmlns', 'http://www.w3.org/1998/Math/MathML')) + if self._type.endswith("html"): + if not dict(attrs).get("xmlns"): + if tag == "svg": + attrs.append(("xmlns", "http://www.w3.org/2000/svg")) + if tag == "math": + attrs.append(("xmlns", "http://www.w3.org/1998/Math/MathML")) # not otherwise acceptable, perhaps it is MathML or SVG? - if tag == 'math' and ('xmlns', 'http://www.w3.org/1998/Math/MathML') in attrs: + if ( + tag == "math" + and ("xmlns", "http://www.w3.org/1998/Math/MathML") in attrs + ): self.mathmlOK += 1 - if tag == 'svg' and ('xmlns', 'http://www.w3.org/2000/svg') in attrs: + if tag == "svg" and ("xmlns", "http://www.w3.org/2000/svg") in attrs: self.svgOK += 1 # chose acceptable attributes based on tag class, else bail @@ -789,20 +794,20 @@ class HTMLSanitizer(BaseHTMLProcessor): # declare xlink namespace, if needed if self.mathmlOK or self.svgOK: - if any((a for a in attrs if a[0].startswith('xlink:'))): - if not ('xmlns:xlink', 'http://www.w3.org/1999/xlink') in attrs: - attrs.append(('xmlns:xlink', 'http://www.w3.org/1999/xlink')) + if any(a for a in attrs if a[0].startswith("xlink:")): + if not ("xmlns:xlink", "http://www.w3.org/1999/xlink") in attrs: + attrs.append(("xmlns:xlink", "http://www.w3.org/1999/xlink")) clean_attrs = [] for key, value in self.normalize_attrs(attrs): - if key == 'style' and 'style' in acceptable_attributes: + if key == "style" and "style" in acceptable_attributes: clean_value = self.sanitize_style(value) if clean_value: clean_attrs.append((key, clean_value)) elif key in acceptable_attributes: key = keymap.get(key, key) # make sure the uri uses an acceptable uri scheme - if key == 'href': + if key == "href": value = make_safe_absolute_uri(value) clean_attrs.append((key, value)) super().unknown_starttag(tag, clean_attrs) @@ -812,11 +817,11 @@ class HTMLSanitizer(BaseHTMLProcessor): if tag in self.unacceptable_elements_with_end_tag: self.unacceptablestack -= 1 if self.mathmlOK and tag in self.mathml_elements: - if tag == 'math' and self.mathmlOK: + if tag == "math" and self.mathmlOK: self.mathmlOK -= 1 elif self.svgOK and tag in self.svg_elements: tag = self.svg_elem_map.get(tag, tag) - if tag == 'svg' and self.svgOK: + if tag == "svg" and self.svgOK: self.svgOK -= 1 else: return @@ -834,35 +839,43 @@ class HTMLSanitizer(BaseHTMLProcessor): def sanitize_style(self, style): # disallow urls - style = re.compile(r'url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style) + style = re.compile(r"url\s*\(\s*[^\s)]+?\s*\)\s*").sub(" ", style) # gauntlet - if not re.match(r"""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): - return '' + if not re.match( + r"""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", + style, + ): + return "" # This replaced a regexp that used re.match and was prone to # pathological back-tracking. - if re.sub(r"\s*[-\w]+\s*:\s*[^:;]*;?", '', style).strip(): - return '' + if re.sub(r"\s*[-\w]+\s*:\s*[^:;]*;?", "", style).strip(): + return "" clean = [] for prop, value in re.findall(r"([-\w]+)\s*:\s*([^:;]*)", style): if not value: continue if prop.lower() in self.acceptable_css_properties: - clean.append(prop + ': ' + value + ';') - elif prop.split('-')[0].lower() in ['background', 'border', 'margin', 'padding']: + clean.append(prop + ": " + value + ";") + elif prop.split("-")[0].lower() in [ + "background", + "border", + "margin", + "padding", + ]: for keyword in value.split(): if ( - keyword not in self.acceptable_css_keywords - and not self.valid_css_values.match(keyword) + keyword not in self.acceptable_css_keywords + and not self.valid_css_values.match(keyword) ): break else: - clean.append(prop + ': ' + value + ';') + clean.append(prop + ": " + value + ";") elif self.svgOK and prop.lower() in self.acceptable_svg_properties: - clean.append(prop + ': ' + value + ';') + clean.append(prop + ": " + value + ";") - return ' '.join(clean) + return " ".join(clean) def parse_comment(self, i, report=1): ret = super().parse_comment(i, report) @@ -870,7 +883,7 @@ class HTMLSanitizer(BaseHTMLProcessor): return ret # if ret == -1, this may be a malicious attempt to circumvent # sanitization, or a page-destroying unclosed comment - match = re.compile(r'--[^>]*>').search(self.rawdata, i+4) + match = re.compile(r"--[^>]*>").search(self.rawdata, i + 4) if match: return match.end() # unclosed comment; deliberately fail to handle_data() @@ -879,20 +892,20 @@ class HTMLSanitizer(BaseHTMLProcessor): def sanitize_html(html_source, encoding, _type): p = HTMLSanitizer(encoding, _type) - html_source = html_source.replace(' -RE_ENTITY_PATTERN = re.compile(br'^\s*]*?)>', re.MULTILINE) +RE_ENTITY_PATTERN = re.compile(rb"^\s*]*?)>", re.MULTILINE) # Match XML DOCTYPE declarations. # Example: -RE_DOCTYPE_PATTERN = re.compile(br'^\s*]*?)>', re.MULTILINE) +RE_DOCTYPE_PATTERN = re.compile(rb"^\s*]*?)>", re.MULTILINE) # Match safe entity declarations. # This will allow hexadecimal character references through, @@ -900,51 +913,62 @@ RE_DOCTYPE_PATTERN = re.compile(br'^\s*]*?)>', re.MULTILINE) # Example: cubed "³" # Example: copyright "(C)" # Forbidden: explode1 "&explode2;&explode2;" -RE_SAFE_ENTITY_PATTERN = re.compile(br'\s+(\w+)\s+"(&#\w+;|[^&"]*)"') +RE_SAFE_ENTITY_PATTERN = re.compile(rb'\s+(\w+)\s+"(&#\w+;|[^&"]*)"') -def replace_doctype(data): - """Strips and replaces the DOCTYPE, returns (rss_version, stripped_data) +def replace_doctype(data: bytes) -> tuple[str | None, bytes, dict[str, str]]: + """Strip and replaces the DOCTYPE. - rss_version may be 'rss091n' or None - stripped_data is the same XML document with a replaced DOCTYPE + One RSS format -- Netscape's RSS 0.91 -- is identified within the XML declaration. + Therefore, this function must identify that version while replacing the DOCTYPE. + + As a convenience to the loose XML parser, entities are pre-computed and returned. + + The tuple that is returned has the following values, in order: + + 1. The version extracted from the XML DOCTYPE. + The value will either be "rss091n" or None. + 2. Binary XML content with a replaced DOCTYPE. + 3. A dictionary of entities and replacements. """ + # Verify this looks like an XML feed. + if not re.match(rb"^\s*<", data): + return None, data, {} + # Divide the document into two groups by finding the location # of the first element that doesn't begin with '\n\n]>' + replacement = ( + b"\n\n]>" + ) data = RE_DOCTYPE_PATTERN.sub(replacement, head) + data # Precompute the safe entities for the loose parser. - safe_entities = { - k.decode('utf-8'): v.decode('utf-8') + entities = { + k.decode("utf-8"): v.decode("utf-8") for k, v in RE_SAFE_ENTITY_PATTERN.findall(replacement) } - return version, data, safe_entities + return version, data, entities diff --git a/lib/feedparser/sgml.py b/lib/feedparser/sgml.py index b5dbdbc3..21735431 100644 --- a/lib/feedparser/sgml.py +++ b/lib/feedparser/sgml.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -27,20 +27,20 @@ import re -import sgmllib # type: ignore[import] +import sgmllib3k as sgmllib __all__ = [ - 'sgmllib', - 'charref', - 'tagfind', - 'attrfind', - 'entityref', - 'incomplete', - 'interesting', - 'shorttag', - 'shorttagopen', - 'starttagopen', - 'endbracket', + "sgmllib", + "charref", + "tagfind", + "attrfind", + "entityref", + "incomplete", + "interesting", + "shorttag", + "shorttagopen", + "starttagopen", + "endbracket", ] # sgmllib defines a number of module-level regular expressions that are @@ -49,20 +49,20 @@ __all__ = [ # names, and the compiled code objects of several sgmllib.SGMLParser # methods are copied into _BaseHTMLProcessor so that they execute in # feedparser's scope instead of sgmllib's scope. -charref = re.compile(r'&#(\d+|[xX][0-9a-fA-F]+);') -tagfind = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*') +charref = re.compile(r"&#(\d+|[xX][0-9a-fA-F]+);") +tagfind = re.compile(r"[a-zA-Z][-_.:a-zA-Z0-9]*") attrfind = re.compile( r"""\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)[$]?(\s*=\s*""" r"""('[^']*'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$()_#=~'"@]*))?""" ) # Unfortunately, these must be copied over to prevent NameError exceptions -entityref = sgmllib.entityref -incomplete = sgmllib.incomplete -interesting = sgmllib.interesting -shorttag = sgmllib.shorttag -shorttagopen = sgmllib.shorttagopen -starttagopen = sgmllib.starttagopen +entityref = sgmllib.SGMLParser.entityref +incomplete = sgmllib.SGMLParser.incomplete +interesting = sgmllib.SGMLParser.interesting +shorttag = sgmllib.SGMLParser.shorttag +shorttagopen = sgmllib.SGMLParser.shorttagopen +starttagopen = sgmllib.SGMLParser.starttagopen class _EndBracketRegEx: @@ -70,12 +70,12 @@ class _EndBracketRegEx: # Overriding the built-in sgmllib.endbracket regex allows the # parser to find angle brackets embedded in element attributes. self.endbracket = re.compile( - r'(' + r"(" r"""[^'"<>]""" r"""|"[^"]*"(?=>|/|\s|\w+=)""" r"""|'[^']*'(?=>|/|\s|\w+=))*(?=[<>])""" r"""|.*?(?=[<>]""" - r')' + r")" ) def search(self, target, index=0): diff --git a/lib/feedparser/urls.py b/lib/feedparser/urls.py index 623f030a..2579443b 100644 --- a/lib/feedparser/urls.py +++ b/lib/feedparser/urls.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -37,103 +37,116 @@ from .html import BaseHTMLProcessor # https://secure.wikimedia.org/wikipedia/en/wiki/URI_scheme # Many more will likely need to be added! ACCEPTABLE_URI_SCHEMES = ( - 'file', 'ftp', 'gopher', 'h323', 'hdl', 'http', 'https', 'imap', 'magnet', - 'mailto', 'mms', 'news', 'nntp', 'prospero', 'rsync', 'rtsp', 'rtspu', - 'sftp', 'shttp', 'sip', 'sips', 'snews', 'svn', 'svn+ssh', 'telnet', - 'wais', + "file", + "ftp", + "gopher", + "h323", + "hdl", + "http", + "https", + "imap", + "magnet", + "mailto", + "mms", + "news", + "nntp", + "prospero", + "rsync", + "rtsp", + "rtspu", + "sftp", + "shttp", + "sip", + "sips", + "snews", + "svn", + "svn+ssh", + "telnet", + "wais", # Additional common-but-unofficial schemes - 'aim', 'callto', 'cvs', 'facetime', 'feed', 'git', 'gtalk', 'irc', 'ircs', - 'irc6', 'itms', 'mms', 'msnim', 'skype', 'ssh', 'smb', 'svn', 'ymsg', + "aim", + "callto", + "cvs", + "facetime", + "feed", + "git", + "gtalk", + "irc", + "ircs", + "irc6", + "itms", + "mms", + "msnim", + "skype", + "ssh", + "smb", + "svn", + "ymsg", ) -_urifixer = re.compile('^([A-Za-z][A-Za-z0-9+-.]*://)(/*)(.*?)') +_urifixer = re.compile("^([A-Za-z][A-Za-z0-9+-.]*://)(/*)(.*?)") def _urljoin(base, uri): - uri = _urifixer.sub(r'\1\3', uri) + uri = _urifixer.sub(r"\1\3", uri) try: uri = urllib.parse.urljoin(base, uri) except ValueError: - uri = '' + uri = "" return uri -def convert_to_idn(url): - """Convert a URL to IDN notation""" - # this function should only be called with a unicode string - # strategy: if the host cannot be encoded in ascii, then - # it'll be necessary to encode it in idn form - parts = list(urllib.parse.urlsplit(url)) - try: - parts[1].encode('ascii') - except UnicodeEncodeError: - # the url needs to be converted to idn notation - host = parts[1].rsplit(':', 1) - newhost = [] - port = '' - if len(host) == 2: - port = host.pop() - for h in host[0].split('.'): - newhost.append(h.encode('idna').decode('utf-8')) - parts[1] = '.'.join(newhost) - if port: - parts[1] += ':' + port - return urllib.parse.urlunsplit(parts) - else: - return url - - def make_safe_absolute_uri(base, rel=None): # bail if ACCEPTABLE_URI_SCHEMES is empty if not ACCEPTABLE_URI_SCHEMES: - return _urljoin(base, rel or '') + return _urljoin(base, rel or "") if not base: - return rel or '' + return rel or "" if not rel: try: scheme = urllib.parse.urlparse(base)[0] except ValueError: - return '' + return "" if not scheme or scheme in ACCEPTABLE_URI_SCHEMES: return base - return '' + return "" uri = _urljoin(base, rel) - if uri.strip().split(':', 1)[0] not in ACCEPTABLE_URI_SCHEMES: - return '' + if uri.strip().split(":", 1)[0] not in ACCEPTABLE_URI_SCHEMES: + return "" return uri class RelativeURIResolver(BaseHTMLProcessor): relative_uris = { - ('a', 'href'), - ('applet', 'codebase'), - ('area', 'href'), - ('audio', 'src'), - ('blockquote', 'cite'), - ('body', 'background'), - ('del', 'cite'), - ('form', 'action'), - ('frame', 'longdesc'), - ('frame', 'src'), - ('iframe', 'longdesc'), - ('iframe', 'src'), - ('head', 'profile'), - ('img', 'longdesc'), - ('img', 'src'), - ('img', 'usemap'), - ('input', 'src'), - ('input', 'usemap'), - ('ins', 'cite'), - ('link', 'href'), - ('object', 'classid'), - ('object', 'codebase'), - ('object', 'data'), - ('object', 'usemap'), - ('q', 'cite'), - ('script', 'src'), - ('source', 'src'), - ('video', 'poster'), - ('video', 'src'), + ("a", "href"), + ("applet", "codebase"), + ("area", "href"), + ("audio", "src"), + ("blockquote", "cite"), + ("body", "background"), + ("del", "cite"), + ("form", "action"), + ("frame", "longdesc"), + ("frame", "src"), + ("iframe", "longdesc"), + ("iframe", "src"), + ("head", "profile"), + ("img", "longdesc"), + ("img", "src"), + ("img", "usemap"), + ("input", "src"), + ("input", "usemap"), + ("ins", "cite"), + ("link", "href"), + ("object", "classid"), + ("object", "codebase"), + ("object", "data"), + ("object", "usemap"), + ("q", "cite"), + ("script", "src"), + ("source", "src"), + ("video", "poster"), + ("video", "src"), } def __init__(self, baseuri, encoding, _type): @@ -145,8 +158,14 @@ class RelativeURIResolver(BaseHTMLProcessor): def unknown_starttag(self, tag, attrs): attrs = self.normalize_attrs(attrs) - attrs = [(key, ((tag, key) in self.relative_uris) and self.resolve_uri(value) or value) for key, value in attrs] - super(RelativeURIResolver, self).unknown_starttag(tag, attrs) + attrs = [ + ( + key, + ((tag, key) in self.relative_uris) and self.resolve_uri(value) or value, + ) + for key, value in attrs + ] + super().unknown_starttag(tag, attrs) def resolve_relative_uris(html_source, base_uri, encoding, type_): diff --git a/lib/feedparser/util.py b/lib/feedparser/util.py index 9e1516cf..bcf0f61e 100644 --- a/lib/feedparser/util.py +++ b/lib/feedparser/util.py @@ -1,4 +1,4 @@ -# Copyright 2010-2022 Kurt McKee +# Copyright 2010-2023 Kurt McKee # Copyright 2002-2008 Mark Pilgrim # All rights reserved. # @@ -30,22 +30,22 @@ import warnings class FeedParserDict(dict): keymap = { - 'channel': 'feed', - 'items': 'entries', - 'guid': 'id', - 'date': 'updated', - 'date_parsed': 'updated_parsed', - 'description': ['summary', 'subtitle'], - 'description_detail': ['summary_detail', 'subtitle_detail'], - 'url': ['href'], - 'modified': 'updated', - 'modified_parsed': 'updated_parsed', - 'issued': 'published', - 'issued_parsed': 'published_parsed', - 'copyright': 'rights', - 'copyright_detail': 'rights_detail', - 'tagline': 'subtitle', - 'tagline_detail': 'subtitle_detail', + "channel": "feed", + "items": "entries", + "guid": "id", + "date": "updated", + "date_parsed": "updated_parsed", + "description": ["summary", "subtitle"], + "description_detail": ["summary_detail", "subtitle_detail"], + "url": ["href"], + "modified": "updated", + "modified_parsed": "updated_parsed", + "issued": "published", + "issued_parsed": "published_parsed", + "copyright": "rights", + "copyright_detail": "rights_detail", + "tagline": "subtitle", + "tagline_detail": "subtitle_detail", } def __getitem__(self, key, _stacklevel=2): @@ -53,28 +53,29 @@ class FeedParserDict(dict): :return: A :class:`FeedParserDict`. """ - if key == 'category': + if key == "category": try: - return dict.__getitem__(self, 'tags')[0]['term'] + return dict.__getitem__(self, "tags")[0]["term"] except IndexError: raise KeyError("object doesn't have key 'category'") - elif key == 'enclosures': + elif key == "enclosures": return [ - FeedParserDict([(name, value) for (name, value) in link.items() if name != 'rel']) - for link in dict.__getitem__(self, 'links') - if link['rel'] == 'enclosure' + FeedParserDict( + [(name, value) for (name, value) in link.items() if name != "rel"] + ) + for link in dict.__getitem__(self, "links") + if link["rel"] == "enclosure" ] - elif key == 'license': - for link in dict.__getitem__(self, 'links'): - if link['rel'] == 'license' and 'href' in link: - return link['href'] - elif key == 'updated': + elif key == "license": + for link in dict.__getitem__(self, "links"): + if link["rel"] == "license" and "href" in link: + return link["href"] + elif key == "updated": # Temporarily help developers out by keeping the old # broken behavior that was reported in issue 310. # This fix was proposed in issue 328. - if ( - not dict.__contains__(self, 'updated') - and dict.__contains__(self, 'published') + if not dict.__contains__(self, "updated") and dict.__contains__( + self, "published" ): warnings.warn( "To avoid breaking existing software while " @@ -85,12 +86,11 @@ class FeedParserDict(dict): DeprecationWarning, stacklevel=_stacklevel, ) - return dict.__getitem__(self, 'published') - return dict.__getitem__(self, 'updated') - elif key == 'updated_parsed': - if ( - not dict.__contains__(self, 'updated_parsed') - and dict.__contains__(self, 'published_parsed') + return dict.__getitem__(self, "published") + return dict.__getitem__(self, "updated") + elif key == "updated_parsed": + if not dict.__contains__(self, "updated_parsed") and dict.__contains__( + self, "published_parsed" ): warnings.warn( "To avoid breaking existing software while " @@ -101,8 +101,8 @@ class FeedParserDict(dict): DeprecationWarning, stacklevel=_stacklevel, ) - return dict.__getitem__(self, 'published_parsed') - return dict.__getitem__(self, 'updated_parsed') + return dict.__getitem__(self, "published_parsed") + return dict.__getitem__(self, "updated_parsed") else: realkey = self.keymap.get(key, key) if isinstance(realkey, list): @@ -114,7 +114,7 @@ class FeedParserDict(dict): return dict.__getitem__(self, key) def __contains__(self, key): - if key in ('updated', 'updated_parsed'): + if key in ("updated", "updated_parsed"): # Temporarily help developers out by keeping the old # broken behavior that was reported in issue 310. # This fix was proposed in issue 328. From 73158c9ab8a6c9cc3dabd705066189f956dd9419 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Thu, 13 Apr 2023 09:41:12 +0100 Subject: [PATCH 10/36] =?UTF-8?q?Update=20Apprise=201.2.1=20(3d07004)=20?= =?UTF-8?q?=E2=86=92=201.3.0=20(6458ab0).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + lib/apprise/Apprise.py | 264 ++++++++---- lib/apprise/AppriseAsset.py | 43 +- lib/apprise/AppriseAttachment.py | 43 +- lib/apprise/AppriseConfig.py | 43 +- lib/apprise/AppriseLocale.py | 43 +- lib/apprise/URLBase.py | 43 +- lib/apprise/__init__.py | 49 ++- lib/apprise/attachment/AttachBase.py | 43 +- lib/apprise/attachment/AttachFile.py | 43 +- lib/apprise/attachment/AttachHTTP.py | 43 +- lib/apprise/attachment/__init__.py | 44 +- lib/apprise/common.py | 44 +- lib/apprise/config/ConfigBase.py | 43 +- lib/apprise/config/ConfigFile.py | 43 +- lib/apprise/config/ConfigHTTP.py | 43 +- lib/apprise/config/ConfigMemory.py | 43 +- lib/apprise/config/__init__.py | 43 +- lib/apprise/conversion.py | 43 +- lib/apprise/decorators/CustomNotifyPlugin.py | 44 +- lib/apprise/decorators/__init__.py | 44 +- lib/apprise/decorators/notify.py | 44 +- lib/apprise/i18n/apprise.pot | 29 +- lib/apprise/logger.py | 43 +- lib/apprise/plugins/NotifyAppriseAPI.py | 43 +- lib/apprise/plugins/NotifyBark.py | 44 +- lib/apprise/plugins/NotifyBase.py | 124 ++++-- lib/apprise/plugins/NotifyBoxcar.py | 43 +- lib/apprise/plugins/NotifyBulkSMS.py | 43 +- lib/apprise/plugins/NotifyClickSend.py | 43 +- lib/apprise/plugins/NotifyD7Networks.py | 43 +- lib/apprise/plugins/NotifyDBus.py | 43 +- lib/apprise/plugins/NotifyDapnet.py | 43 +- lib/apprise/plugins/NotifyDingTalk.py | 43 +- lib/apprise/plugins/NotifyDiscord.py | 43 +- lib/apprise/plugins/NotifyEmail.py | 43 +- lib/apprise/plugins/NotifyEmby.py | 46 +- lib/apprise/plugins/NotifyEnigma2.py | 43 +- lib/apprise/plugins/NotifyFCM/__init__.py | 43 +- lib/apprise/plugins/NotifyFCM/color.py | 43 +- lib/apprise/plugins/NotifyFCM/common.py | 44 +- lib/apprise/plugins/NotifyFCM/oauth.py | 43 +- lib/apprise/plugins/NotifyFCM/priority.py | 43 +- lib/apprise/plugins/NotifyFaast.py | 42 +- lib/apprise/plugins/NotifyFlock.py | 43 +- lib/apprise/plugins/NotifyForm.py | 131 ++++-- lib/apprise/plugins/NotifyGitter.py | 43 +- lib/apprise/plugins/NotifyGnome.py | 43 +- lib/apprise/plugins/NotifyGoogleChat.py | 43 +- lib/apprise/plugins/NotifyGotify.py | 46 +- lib/apprise/plugins/NotifyGrowl.py | 43 +- lib/apprise/plugins/NotifyGuilded.py | 43 +- lib/apprise/plugins/NotifyHomeAssistant.py | 43 +- lib/apprise/plugins/NotifyIFTTT.py | 44 +- lib/apprise/plugins/NotifyJSON.py | 49 ++- lib/apprise/plugins/NotifyJoin.py | 43 +- lib/apprise/plugins/NotifyKavenegar.py | 43 +- lib/apprise/plugins/NotifyKumulos.py | 43 +- lib/apprise/plugins/NotifyLametric.py | 43 +- lib/apprise/plugins/NotifyLine.py | 44 +- lib/apprise/plugins/NotifyMQTT.py | 45 +- lib/apprise/plugins/NotifyMSG91.py | 43 +- lib/apprise/plugins/NotifyMSTeams.py | 43 +- lib/apprise/plugins/NotifyMacOSX.py | 43 +- lib/apprise/plugins/NotifyMailgun.py | 43 +- lib/apprise/plugins/NotifyMastodon.py | 45 +- lib/apprise/plugins/NotifyMatrix.py | 52 ++- lib/apprise/plugins/NotifyMatterMost.py | 43 +- lib/apprise/plugins/NotifyMessageBird.py | 43 +- lib/apprise/plugins/NotifyMisskey.py | 310 ++++++++++++++ lib/apprise/plugins/NotifyNextcloud.py | 42 +- lib/apprise/plugins/NotifyNextcloudTalk.py | 42 +- lib/apprise/plugins/NotifyNotica.py | 42 +- lib/apprise/plugins/NotifyNotifico.py | 43 +- lib/apprise/plugins/NotifyNtfy.py | 219 ++++++++-- lib/apprise/plugins/NotifyOffice365.py | 43 +- lib/apprise/plugins/NotifyOneSignal.py | 43 +- lib/apprise/plugins/NotifyOpsgenie.py | 43 +- lib/apprise/plugins/NotifyPagerDuty.py | 43 +- lib/apprise/plugins/NotifyPagerTree.py | 424 +++++++++++++++++++ lib/apprise/plugins/NotifyParsePlatform.py | 43 +- lib/apprise/plugins/NotifyPopcornNotify.py | 43 +- lib/apprise/plugins/NotifyProwl.py | 43 +- lib/apprise/plugins/NotifyPushBullet.py | 43 +- lib/apprise/plugins/NotifyPushSafer.py | 43 +- lib/apprise/plugins/NotifyPushed.py | 43 +- lib/apprise/plugins/NotifyPushjet.py | 43 +- lib/apprise/plugins/NotifyPushover.py | 45 +- lib/apprise/plugins/NotifyReddit.py | 44 +- lib/apprise/plugins/NotifyRocketChat.py | 43 +- lib/apprise/plugins/NotifyRyver.py | 43 +- lib/apprise/plugins/NotifySES.py | 43 +- lib/apprise/plugins/NotifySMSEagle.py | 43 +- lib/apprise/plugins/NotifySMTP2Go.py | 43 +- lib/apprise/plugins/NotifySNS.py | 43 +- lib/apprise/plugins/NotifySendGrid.py | 44 +- lib/apprise/plugins/NotifyServerChan.py | 43 +- lib/apprise/plugins/NotifySignalAPI.py | 43 +- lib/apprise/plugins/NotifySimplePush.py | 44 +- lib/apprise/plugins/NotifySinch.py | 43 +- lib/apprise/plugins/NotifySlack.py | 43 +- lib/apprise/plugins/NotifySparkPost.py | 43 +- lib/apprise/plugins/NotifySpontit.py | 43 +- lib/apprise/plugins/NotifyStreamlabs.py | 43 +- lib/apprise/plugins/NotifySyslog.py | 44 +- lib/apprise/plugins/NotifyTechulusPush.py | 43 +- lib/apprise/plugins/NotifyTelegram.py | 43 +- lib/apprise/plugins/NotifyTwilio.py | 43 +- lib/apprise/plugins/NotifyTwist.py | 44 +- lib/apprise/plugins/NotifyTwitter.py | 43 +- lib/apprise/plugins/NotifyVoipms.py | 372 ++++++++++++++++ lib/apprise/plugins/NotifyVonage.py | 43 +- lib/apprise/plugins/NotifyWebexTeams.py | 54 ++- lib/apprise/plugins/NotifyWindows.py | 43 +- lib/apprise/plugins/NotifyXBMC.py | 43 +- lib/apprise/plugins/NotifyXML.py | 49 ++- lib/apprise/plugins/NotifyZulip.py | 43 +- lib/apprise/plugins/__init__.py | 43 +- lib/apprise/utils.py | 47 +- 119 files changed, 4475 insertions(+), 2193 deletions(-) create mode 100644 lib/apprise/plugins/NotifyMisskey.py create mode 100644 lib/apprise/plugins/NotifyPagerTree.py create mode 100644 lib/apprise/plugins/NotifyVoipms.py diff --git a/CHANGES.md b/CHANGES.md index 50c5764c..0bcb07a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.29.0 (2023-xx-xx xx:xx:00 UTC) +* Update Apprise 1.2.1 (3d07004) to 1.3.0 (6458ab0) * Update attr 22.2.0 (a9960de) to 22.2.0 (683d056) * Update diskcache 5.4.0 (1cb1425) to 5.6.1 (4d30686) * Update feedparser 6.0.10 (5fcb3ae) to 6.0.10 (6d032b8) diff --git a/lib/apprise/Apprise.py b/lib/apprise/Apprise.py index 39a1ff0a..19dde830 100644 --- a/lib/apprise/Apprise.py +++ b/lib/apprise/Apprise.py @@ -1,29 +1,38 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 asyncio import os +from functools import partial from itertools import chain from . import common from .conversion import convert_between @@ -43,11 +52,6 @@ from .plugins.NotifyBase import NotifyBase from . import plugins from . import __version__ -# Python v3+ support code made importable, so it can remain backwards -# compatible with Python v2 -# TODO: Review after dropping support for Python 2. -from . import py3compat - class Apprise: """ @@ -369,91 +373,118 @@ class Apprise: such as turning a \n into an actual new line, etc. """ - return py3compat.asyncio.tosync( - self.async_notify( + try: + # Process arguments and build synchronous and asynchronous calls + # (this step can throw internal errors). + sync_partials, async_cors = self._create_notify_calls( body, title, notify_type=notify_type, body_format=body_format, tag=tag, match_always=match_always, attach=attach, - interpret_escapes=interpret_escapes, - ), - debug=self.debug - ) + interpret_escapes=interpret_escapes + ) - def async_notify(self, *args, **kwargs): + except TypeError: + # No notifications sent, and there was an internal error. + return False + + if not sync_partials and not async_cors: + # Nothing to send + return None + + sync_result = Apprise._notify_all(*sync_partials) + + if async_cors: + # A single coroutine sends all asynchronous notifications in + # parallel. + all_cor = Apprise._async_notify_all(*async_cors) + + try: + # Python <3.7 automatically starts an event loop if there isn't + # already one for the main thread. + loop = asyncio.get_event_loop() + + except RuntimeError: + # Python >=3.7 raises this exception if there isn't already an + # event loop. So, we can spin up our own. + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.set_debug(self.debug) + + # Run the coroutine and wait for the result. + async_result = loop.run_until_complete(all_cor) + + # Clean up the loop. + loop.close() + asyncio.set_event_loop(None) + + else: + old_debug = loop.get_debug() + loop.set_debug(self.debug) + + # Run the coroutine and wait for the result. + async_result = loop.run_until_complete(all_cor) + + loop.set_debug(old_debug) + + else: + async_result = True + + return sync_result and async_result + + async def async_notify(self, *args, **kwargs): """ Send a notification to all the plugins previously loaded, for - asynchronous callers. This method is an async method that should be - awaited on, even if it is missing the async keyword in its signature. - (This is omitted to preserve syntax compatibility with Python 2.) + asynchronous callers. The arguments are identical to those of Apprise.notify(). """ try: - coroutines = list( - self._notifyall( - Apprise._notifyhandlerasync, *args, **kwargs)) + # Process arguments and build synchronous and asynchronous calls + # (this step can throw internal errors). + sync_partials, async_cors = self._create_notify_calls( + *args, **kwargs) except TypeError: # No notifications sent, and there was an internal error. - return py3compat.asyncio.toasyncwrapvalue(False) - - else: - if len(coroutines) > 0: - # All notifications sent, return False if any failed. - return py3compat.asyncio.notify(coroutines) - - else: - # No notifications sent. - return py3compat.asyncio.toasyncwrapvalue(None) - - @staticmethod - def _notifyhandler(server, **kwargs): - """ - The synchronous notification sender. Returns True if the notification - sent successfully. - """ - - try: - # Send notification - return server.notify(**kwargs) - - except TypeError: - # These our our internally thrown notifications return False - except Exception: - # A catch all so we don't have to abort early - # just because one of our plugins has a bug in it. - logger.exception("Unhandled Notification Exception") - return False + if not sync_partials and not async_cors: + # Nothing to send + return None - @staticmethod - def _notifyhandlerasync(server, **kwargs): - """ - The asynchronous notification sender. Returns a coroutine that yields - True if the notification sent successfully. - """ + sync_result = Apprise._notify_all(*sync_partials) + async_result = await Apprise._async_notify_all(*async_cors) + return sync_result and async_result - if server.asset.async_mode: - return server.async_notify(**kwargs) - - else: - # Send the notification immediately, and wrap the result in a - # coroutine. - status = Apprise._notifyhandler(server, **kwargs) - return py3compat.asyncio.toasyncwrapvalue(status) - - def _notifyall(self, handler, body, title='', - notify_type=common.NotifyType.INFO, body_format=None, - tag=common.MATCH_ALL_TAG, match_always=True, attach=None, - interpret_escapes=None): + def _create_notify_calls(self, *args, **kwargs): """ Creates notifications for all the plugins loaded. - Returns a generator that calls handler for each notification. The first - and only argument supplied to handler is the server, and the keyword - arguments are exactly as they would be passed to server.notify(). + Returns a list of synchronous calls (partial functions with no + arguments required) for plugins with async disabled and a list of + asynchronous calls (coroutines) for plugins with async enabled. + """ + + all_calls = list(self._create_notify_gen(*args, **kwargs)) + + # Split into synchronous partials and asynchronous coroutines. + sync_partials, async_cors = [], [] + for notify in all_calls: + if asyncio.iscoroutine(notify): + async_cors.append(notify) + else: + sync_partials.append(notify) + + return sync_partials, async_cors + + def _create_notify_gen(self, body, title='', + notify_type=common.NotifyType.INFO, + body_format=None, tag=common.MATCH_ALL_TAG, + match_always=True, attach=None, + interpret_escapes=None): + """ + Internal generator function for _create_notify_calls(). """ if len(self) == 0: @@ -546,14 +577,67 @@ class Apprise: logger.error(msg) raise TypeError(msg) - yield handler( - server, + kwargs = dict( body=conversion_body_map[server.notify_format], title=conversion_title_map[server.notify_format], notify_type=notify_type, attach=attach, - body_format=body_format, + body_format=body_format ) + if server.asset.async_mode: + yield server.async_notify(**kwargs) + else: + yield partial(server.notify, **kwargs) + + @staticmethod + def _notify_all(*partials): + """ + Process a list of synchronous notify() calls. + """ + + success = True + + for notify in partials: + try: + # Send notification + result = notify() + success = success and result + + except TypeError: + # These are our internally thrown notifications. + success = False + + except Exception: + # A catch all so we don't have to abort early + # just because one of our plugins has a bug in it. + logger.exception("Unhandled Notification Exception") + success = False + + return success + + @staticmethod + async def _async_notify_all(*cors): + """ + Process a list of asynchronous async_notify() calls. + """ + + # Create log entry + logger.info('Notifying %d service(s) asynchronously.', len(cors)) + + results = await asyncio.gather(*cors, return_exceptions=True) + + if any(isinstance(status, Exception) + and not isinstance(status, TypeError) for status in results): + # A catch all so we don't have to abort early just because + # one of our plugins has a bug in it. + logger.exception("Unhandled Notification Exception") + return False + + if any(isinstance(status, TypeError) for status in results): + # These are our internally thrown notifications. + return False + + return all(results) def details(self, lang=None, show_requirements=False, show_disabled=False): """ diff --git a/lib/apprise/AppriseAsset.py b/lib/apprise/AppriseAsset.py index 80bd0656..34821e27 100644 --- a/lib/apprise/AppriseAsset.py +++ b/lib/apprise/AppriseAsset.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re from uuid import uuid4 diff --git a/lib/apprise/AppriseAttachment.py b/lib/apprise/AppriseAttachment.py index 833edaa0..0a3913ed 100644 --- a/lib/apprise/AppriseAttachment.py +++ b/lib/apprise/AppriseAttachment.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. from . import attachment from . import URLBase diff --git a/lib/apprise/AppriseConfig.py b/lib/apprise/AppriseConfig.py index f92d31d1..8f285777 100644 --- a/lib/apprise/AppriseConfig.py +++ b/lib/apprise/AppriseConfig.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. from . import config from . import ConfigBase diff --git a/lib/apprise/AppriseLocale.py b/lib/apprise/AppriseLocale.py index 0288c045..ce61d0c9 100644 --- a/lib/apprise/AppriseLocale.py +++ b/lib/apprise/AppriseLocale.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 ctypes import locale diff --git a/lib/apprise/URLBase.py b/lib/apprise/URLBase.py index 44be3819..c0036e19 100644 --- a/lib/apprise/URLBase.py +++ b/lib/apprise/URLBase.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re from .logger import logger diff --git a/lib/apprise/__init__.py b/lib/apprise/__init__.py index c00c048e..e67ce953 100644 --- a/lib/apprise/__init__.py +++ b/lib/apprise/__init__.py @@ -1,33 +1,40 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. __title__ = 'Apprise' -__version__ = '1.2.1' +__version__ = '1.3.0' __author__ = 'Chris Caron' -__license__ = 'MIT' -__copywrite__ = 'Copyright (C) 2022 Chris Caron ' +__license__ = 'BSD' +__copywrite__ = 'Copyright (C) 2023 Chris Caron ' __email__ = 'lead2gold@gmail.com' __status__ = 'Production' diff --git a/lib/apprise/attachment/AttachBase.py b/lib/apprise/attachment/AttachBase.py index a4b04544..2b05c849 100644 --- a/lib/apprise/attachment/AttachBase.py +++ b/lib/apprise/attachment/AttachBase.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 os import time diff --git a/lib/apprise/attachment/AttachFile.py b/lib/apprise/attachment/AttachFile.py index f1024a98..f89b915e 100644 --- a/lib/apprise/attachment/AttachFile.py +++ b/lib/apprise/attachment/AttachFile.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import os diff --git a/lib/apprise/attachment/AttachHTTP.py b/lib/apprise/attachment/AttachHTTP.py index d91deb18..d8b46ff2 100644 --- a/lib/apprise/attachment/AttachHTTP.py +++ b/lib/apprise/attachment/AttachHTTP.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import os diff --git a/lib/apprise/attachment/__init__.py b/lib/apprise/attachment/__init__.py index 7f83769a..1b0e1bfe 100644 --- a/lib/apprise/attachment/__init__.py +++ b/lib/apprise/attachment/__init__.py @@ -1,30 +1,36 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re - from os import listdir from os.path import dirname from os.path import abspath diff --git a/lib/apprise/common.py b/lib/apprise/common.py index 288f47f8..2a5a0162 100644 --- a/lib/apprise/common.py +++ b/lib/apprise/common.py @@ -1,28 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # we mirror our base purely for the ability to reset everything; this # is generally only used in testing and should not be used by developers diff --git a/lib/apprise/config/ConfigBase.py b/lib/apprise/config/ConfigBase.py index 08080166..2f3e33b3 100644 --- a/lib/apprise/config/ConfigBase.py +++ b/lib/apprise/config/ConfigBase.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 os import re diff --git a/lib/apprise/config/ConfigFile.py b/lib/apprise/config/ConfigFile.py index ee333250..bfb869a1 100644 --- a/lib/apprise/config/ConfigFile.py +++ b/lib/apprise/config/ConfigFile.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import os diff --git a/lib/apprise/config/ConfigHTTP.py b/lib/apprise/config/ConfigHTTP.py index 3b73cfe6..244b45d3 100644 --- a/lib/apprise/config/ConfigHTTP.py +++ b/lib/apprise/config/ConfigHTTP.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests diff --git a/lib/apprise/config/ConfigMemory.py b/lib/apprise/config/ConfigMemory.py index 2900cce9..ec44e9b4 100644 --- a/lib/apprise/config/ConfigMemory.py +++ b/lib/apprise/config/ConfigMemory.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. from .ConfigBase import ConfigBase from ..AppriseLocale import gettext_lazy as _ diff --git a/lib/apprise/config/__init__.py b/lib/apprise/config/__init__.py index 78311890..7d03a34a 100644 --- a/lib/apprise/config/__init__.py +++ b/lib/apprise/config/__init__.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re from os import listdir diff --git a/lib/apprise/conversion.py b/lib/apprise/conversion.py index da04e921..dc4f1169 100644 --- a/lib/apprise/conversion.py +++ b/lib/apprise/conversion.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re #from markdown import markdown diff --git a/lib/apprise/decorators/CustomNotifyPlugin.py b/lib/apprise/decorators/CustomNotifyPlugin.py index 7c256a84..9c8e7cb1 100644 --- a/lib/apprise/decorators/CustomNotifyPlugin.py +++ b/lib/apprise/decorators/CustomNotifyPlugin.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + from ..plugins.NotifyBase import NotifyBase from ..utils import URL_DETAILS_RE from ..utils import parse_url diff --git a/lib/apprise/decorators/__init__.py b/lib/apprise/decorators/__init__.py index a6ef9662..699fd0da 100644 --- a/lib/apprise/decorators/__init__.py +++ b/lib/apprise/decorators/__init__.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + from .notify import notify diff --git a/lib/apprise/decorators/notify.py b/lib/apprise/decorators/notify.py index 3705e870..36842b41 100644 --- a/lib/apprise/decorators/notify.py +++ b/lib/apprise/decorators/notify.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + from .CustomNotifyPlugin import CustomNotifyPlugin diff --git a/lib/apprise/i18n/apprise.pot b/lib/apprise/i18n/apprise.pot index 6c03a345..677814fe 100644 --- a/lib/apprise/i18n/apprise.pot +++ b/lib/apprise/i18n/apprise.pot @@ -1,14 +1,14 @@ # Translations template for apprise. -# Copyright (C) 2022 Chris Caron +# Copyright (C) 2023 Chris Caron # This file is distributed under the same license as the apprise project. -# FIRST AUTHOR , 2022. +# FIRST AUTHOR , 2023. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: apprise 1.2.1\n" +"Project-Id-Version: apprise 1.3.0\n" "Report-Msgid-Bugs-To: lead2gold@gmail.com\n" -"POT-Creation-Date: 2022-12-28 09:43-0500\n" +"POT-Creation-Date: 2023-02-22 17:31-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -50,6 +50,9 @@ msgstr "" msgid "Account SID" msgstr "" +msgid "Action" +msgstr "" + msgid "Add Tokens" msgstr "" @@ -83,6 +86,9 @@ msgstr "" msgid "Attach" msgstr "" +msgid "Attach File As" +msgstr "" + msgid "Attach Filename" msgstr "" @@ -92,6 +98,9 @@ msgstr "" msgid "Authentication Key" msgstr "" +msgid "Authentication Type" +msgstr "" + msgid "Authorization Token" msgstr "" @@ -305,6 +314,9 @@ msgstr "" msgid "Include Segment" msgstr "" +msgid "Integration ID" +msgstr "" + msgid "Is Ad?" msgstr "" @@ -353,6 +365,9 @@ msgstr "" msgid "Message Type" msgstr "" +msgid "Meta Extras" +msgstr "" + msgid "Modal" msgstr "" @@ -658,6 +673,9 @@ msgstr "" msgid "Text To Speech" msgstr "" +msgid "Third Party ID" +msgstr "" + msgid "Thread ID" msgstr "" @@ -715,6 +733,9 @@ msgstr "" msgid "Use Session" msgstr "" +msgid "User Email" +msgstr "" + msgid "User ID" msgstr "" diff --git a/lib/apprise/logger.py b/lib/apprise/logger.py index fefa08bd..005a3e0d 100644 --- a/lib/apprise/logger.py +++ b/lib/apprise/logger.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 os import logging diff --git a/lib/apprise/plugins/NotifyAppriseAPI.py b/lib/apprise/plugins/NotifyAppriseAPI.py index b4092408..d2f1452a 100644 --- a/lib/apprise/plugins/NotifyAppriseAPI.py +++ b/lib/apprise/plugins/NotifyAppriseAPI.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests diff --git a/lib/apprise/plugins/NotifyBark.py b/lib/apprise/plugins/NotifyBark.py index 48be1bf5..923788de 100644 --- a/lib/apprise/plugins/NotifyBark.py +++ b/lib/apprise/plugins/NotifyBark.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + # # API: https://github.com/Finb/bark-server/blob/master/docs/API_V2.md#python # diff --git a/lib/apprise/plugins/NotifyBase.py b/lib/apprise/plugins/NotifyBase.py index 9b2514de..1b07baa7 100644 --- a/lib/apprise/plugins/NotifyBase.py +++ b/lib/apprise/plugins/NotifyBase.py @@ -1,29 +1,38 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 asyncio import re +from functools import partial from ..URLBase import URLBase from ..common import NotifyType @@ -36,12 +45,7 @@ from ..AppriseLocale import gettext_lazy as _ from ..AppriseAttachment import AppriseAttachment -# Wrap our base with the asyncio wrapper -from ..py3compat.asyncio import AsyncNotifyBase -BASE_OBJECT = AsyncNotifyBase - - -class NotifyBase(BASE_OBJECT): +class NotifyBase(URLBase): """ This is the base class for all notification services """ @@ -267,19 +271,64 @@ class NotifyBase(BASE_OBJECT): color_type=color_type, ) - def notify(self, body, title=None, notify_type=NotifyType.INFO, - overflow=None, attach=None, body_format=None, **kwargs): + def notify(self, *args, **kwargs): """ Performs notification + """ + try: + # Build a list of dictionaries that can be used to call send(). + send_calls = list(self._build_send_calls(*args, **kwargs)) + except TypeError: + # Internal error + return False + + else: + # Loop through each call, one at a time. (Use a list rather than a + # generator to call all the partials, even in case of a failure.) + the_calls = [self.send(**kwargs2) for kwargs2 in send_calls] + return all(the_calls) + + async def async_notify(self, *args, **kwargs): + """ + Performs notification for asynchronous callers + """ + try: + # Build a list of dictionaries that can be used to call send(). + send_calls = list(self._build_send_calls(*args, **kwargs)) + + except TypeError: + # Internal error + return False + + else: + loop = asyncio.get_event_loop() + + # Wrap each call in a coroutine that uses the default executor. + # TODO: In the future, allow plugins to supply a native + # async_send() method. + async def do_send(**kwargs2): + send = partial(self.send, **kwargs2) + result = await loop.run_in_executor(None, send) + return result + + # gather() all calls in parallel. + the_cors = (do_send(**kwargs2) for kwargs2 in send_calls) + return all(await asyncio.gather(*the_cors)) + + def _build_send_calls(self, body, title=None, + notify_type=NotifyType.INFO, overflow=None, + attach=None, body_format=None, **kwargs): + """ + Get a list of dictionaries that can be used to call send() or + (in the future) async_send(). """ if not self.enabled: # Deny notifications issued to services that are disabled - self.logger.warning( - "{} is currently disabled on this system.".format( - self.service_name)) - return False + msg = f"{self.service_name} is currently disabled on this system." + self.logger.warning(msg) + raise TypeError(msg) # Prepare attachments if required if attach is not None and not isinstance(attach, AppriseAttachment): @@ -288,7 +337,7 @@ class NotifyBase(BASE_OBJECT): except TypeError: # bad attachments - return False + raise # Handle situations where the title is None title = '' if not title else title @@ -299,14 +348,11 @@ class NotifyBase(BASE_OBJECT): body_format=body_format): # Send notification - if not self.send(body=chunk['body'], title=chunk['title'], - notify_type=notify_type, attach=attach, - body_format=body_format): - - # Toggle our return status flag - return False - - return True + yield dict( + body=chunk['body'], title=chunk['title'], + notify_type=notify_type, attach=attach, + body_format=body_format + ) def _apply_overflow(self, body, title=None, overflow=None, body_format=None): diff --git a/lib/apprise/plugins/NotifyBoxcar.py b/lib/apprise/plugins/NotifyBoxcar.py index 572ea82e..a613e46e 100644 --- a/lib/apprise/plugins/NotifyBoxcar.py +++ b/lib/apprise/plugins/NotifyBoxcar.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests diff --git a/lib/apprise/plugins/NotifyBulkSMS.py b/lib/apprise/plugins/NotifyBulkSMS.py index eaf2a18b..257c1def 100644 --- a/lib/apprise/plugins/NotifyBulkSMS.py +++ b/lib/apprise/plugins/NotifyBulkSMS.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this service you will need a BulkSMS account # You will need credits (new accounts start with a few) diff --git a/lib/apprise/plugins/NotifyClickSend.py b/lib/apprise/plugins/NotifyClickSend.py index d9c8f652..c93201be 100644 --- a/lib/apprise/plugins/NotifyClickSend.py +++ b/lib/apprise/plugins/NotifyClickSend.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, simply signup with clicksend: # https://www.clicksend.com/ diff --git a/lib/apprise/plugins/NotifyD7Networks.py b/lib/apprise/plugins/NotifyD7Networks.py index 1de64317..7b17f848 100644 --- a/lib/apprise/plugins/NotifyD7Networks.py +++ b/lib/apprise/plugins/NotifyD7Networks.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this service you will need a D7 Networks account from their website # at https://d7networks.com/ diff --git a/lib/apprise/plugins/NotifyDBus.py b/lib/apprise/plugins/NotifyDBus.py index a0839e29..336dfac4 100644 --- a/lib/apprise/plugins/NotifyDBus.py +++ b/lib/apprise/plugins/NotifyDBus.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. from __future__ import absolute_import from __future__ import print_function diff --git a/lib/apprise/plugins/NotifyDapnet.py b/lib/apprise/plugins/NotifyDapnet.py index 3eccc235..bf1ff333 100644 --- a/lib/apprise/plugins/NotifyDapnet.py +++ b/lib/apprise/plugins/NotifyDapnet.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, sign up with Hampager (you need to be a licensed # ham radio operator diff --git a/lib/apprise/plugins/NotifyDingTalk.py b/lib/apprise/plugins/NotifyDingTalk.py index e18123b1..474d4c88 100644 --- a/lib/apprise/plugins/NotifyDingTalk.py +++ b/lib/apprise/plugins/NotifyDingTalk.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import time diff --git a/lib/apprise/plugins/NotifyDiscord.py b/lib/apprise/plugins/NotifyDiscord.py index 83b6ea97..78cd3265 100644 --- a/lib/apprise/plugins/NotifyDiscord.py +++ b/lib/apprise/plugins/NotifyDiscord.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # For this to work correctly you need to create a webhook. To do this just # click on the little gear icon next to the channel you're part of. From diff --git a/lib/apprise/plugins/NotifyEmail.py b/lib/apprise/plugins/NotifyEmail.py index 12518d3d..8698c113 100644 --- a/lib/apprise/plugins/NotifyEmail.py +++ b/lib/apprise/plugins/NotifyEmail.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 dataclasses import re diff --git a/lib/apprise/plugins/NotifyEmby.py b/lib/apprise/plugins/NotifyEmby.py index b617a79b..23d4c611 100644 --- a/lib/apprise/plugins/NotifyEmby.py +++ b/lib/apprise/plugins/NotifyEmby.py @@ -1,30 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# For this plugin to work correct, the Emby server must be set up to allow -# for remote connections. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Emby Docker configuration: https://hub.docker.com/r/emby/embyserver/ # Authentication: https://github.com/MediaBrowser/Emby/wiki/Authentication diff --git a/lib/apprise/plugins/NotifyEnigma2.py b/lib/apprise/plugins/NotifyEnigma2.py index 0739a615..10d58179 100644 --- a/lib/apprise/plugins/NotifyEnigma2.py +++ b/lib/apprise/plugins/NotifyEnigma2.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Sources # - https://dreambox.de/en/ diff --git a/lib/apprise/plugins/NotifyFCM/__init__.py b/lib/apprise/plugins/NotifyFCM/__init__.py index b1cd40e5..098f9ad0 100644 --- a/lib/apprise/plugins/NotifyFCM/__init__.py +++ b/lib/apprise/plugins/NotifyFCM/__init__.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # For this plugin to work correct, the FCM server must be set up to allow # for remote connections. diff --git a/lib/apprise/plugins/NotifyFCM/color.py b/lib/apprise/plugins/NotifyFCM/color.py index 1f096ab0..46d0f2a7 100644 --- a/lib/apprise/plugins/NotifyFCM/color.py +++ b/lib/apprise/plugins/NotifyFCM/color.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # New priorities are defined here: # - https://firebase.google.com/docs/reference/fcm/rest/v1/\ diff --git a/lib/apprise/plugins/NotifyFCM/common.py b/lib/apprise/plugins/NotifyFCM/common.py index 91bc2a0c..0ec10eec 100644 --- a/lib/apprise/plugins/NotifyFCM/common.py +++ b/lib/apprise/plugins/NotifyFCM/common.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + class FCMMode: """ Define the Firebase Cloud Messaging Modes diff --git a/lib/apprise/plugins/NotifyFCM/oauth.py b/lib/apprise/plugins/NotifyFCM/oauth.py index a948d2fe..a76bc698 100644 --- a/lib/apprise/plugins/NotifyFCM/oauth.py +++ b/lib/apprise/plugins/NotifyFCM/oauth.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # # To generate a private key file for your service account: # diff --git a/lib/apprise/plugins/NotifyFCM/priority.py b/lib/apprise/plugins/NotifyFCM/priority.py index b6652f27..81976cb6 100644 --- a/lib/apprise/plugins/NotifyFCM/priority.py +++ b/lib/apprise/plugins/NotifyFCM/priority.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # New priorities are defined here: # - https://firebase.google.com/docs/reference/fcm/rest/v1/\ diff --git a/lib/apprise/plugins/NotifyFaast.py b/lib/apprise/plugins/NotifyFaast.py index 025fdf59..3e55e120 100644 --- a/lib/apprise/plugins/NotifyFaast.py +++ b/lib/apprise/plugins/NotifyFaast.py @@ -1,26 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CON +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 diff --git a/lib/apprise/plugins/NotifyFlock.py b/lib/apprise/plugins/NotifyFlock.py index 1c19a891..4f34b662 100644 --- a/lib/apprise/plugins/NotifyFlock.py +++ b/lib/apprise/plugins/NotifyFlock.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, you need to first access https://dev.flock.com/webhooks # Specifically https://dev.flock.com/webhooks/incoming diff --git a/lib/apprise/plugins/NotifyForm.py b/lib/apprise/plugins/NotifyForm.py index 600713b3..b14ae5ef 100644 --- a/lib/apprise/plugins/NotifyForm.py +++ b/lib/apprise/plugins/NotifyForm.py @@ -1,28 +1,36 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests from .NotifyBase import NotifyBase @@ -38,7 +46,8 @@ METHODS = ( 'GET', 'DELETE', 'PUT', - 'HEAD' + 'HEAD', + 'PATCH' ) @@ -47,6 +56,27 @@ class NotifyForm(NotifyBase): A wrapper for Form Notifications """ + # Support + # - file* + # - file? + # - file*name + # - file?name + # - ?file + # - *file + # - file + # The code will convert the ? or * to the digit increments + __attach_as_re = re.compile( + r'((?P(?P[a-z0-9_-]+)?' + r'(?P[*?+$:.%]+)(?P[a-z0-9_-]+))' + r'|(?P(?P[a-z0-9_-]+)(?P[*?+$:.%]?)))', + re.IGNORECASE) + + # Our count + attach_as_count = '{:02d}' + + # the default attach_as value + attach_as_default = f'file{attach_as_count}' + # The default descriptive name associated with the Notification service_name = 'Form' @@ -111,6 +141,12 @@ class NotifyForm(NotifyBase): 'values': METHODS, 'default': METHODS[0], }, + 'attach-as': { + 'name': _('Attach File As'), + 'type': 'string', + 'default': 'file*', + 'map_to': 'attach_as', + }, }) # Define any kwargs we're using @@ -130,7 +166,7 @@ class NotifyForm(NotifyBase): } def __init__(self, headers=None, method=None, payload=None, params=None, - **kwargs): + attach_as=None, **kwargs): """ Initialize Form Object @@ -152,6 +188,36 @@ class NotifyForm(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + # Custom File Attachment Over-Ride Support + if not isinstance(attach_as, str): + # Default value + self.attach_as = self.attach_as_default + self.attach_multi_support = True + + else: + result = self.__attach_as_re.match(attach_as.strip()) + if not result: + msg = 'The attach-as specified ({}) is invalid.'.format( + attach_as) + self.logger.warning(msg) + raise TypeError(msg) + + self.attach_as = '' + self.attach_multi_support = False + if result.group('match1'): + if result.group('id1a'): + self.attach_as += result.group('id1a') + + self.attach_as += self.attach_as_count + self.attach_multi_support = True + self.attach_as += result.group('id1b') + + else: # result.group('match2'): + self.attach_as += result.group('id2') + if result.group('wc2'): + self.attach_as += self.attach_as_count + self.attach_multi_support = True + self.params = {} if params: # Store our extra headers @@ -192,6 +258,10 @@ class NotifyForm(NotifyBase): params.update( {':{}'.format(k): v for k, v in self.payload_extras.items()}) + if self.attach_as != self.attach_as_default: + # Provide Attach-As extension details + params['attach-as'] = self.attach_as + # Determine Authentication auth = '' if self.user and self.password: @@ -247,7 +317,8 @@ class NotifyForm(NotifyBase): try: files.append(( - 'file{:02d}'.format(no), ( + self.attach_as.format(no) + if self.attach_multi_support else self.attach_as, ( attachment.name, open(attachment.path, 'rb'), attachment.mimetype) @@ -260,11 +331,10 @@ class NotifyForm(NotifyBase): self.logger.debug('I/O Exception: %s' % str(e)) return False - finally: - for file in files: - # Ensure all files are closed - if file[1][1]: - file[1][1].close() + if not self.attach_multi_support and no > 1: + self.logger.warning( + 'Multiple attachments provided while ' + 'form:// Multi-Attachment Support not enabled') # prepare Form Object payload = { @@ -308,6 +378,9 @@ class NotifyForm(NotifyBase): elif self.method == 'PUT': method = requests.put + elif self.method == 'PATCH': + method = requests.patch + elif self.method == 'DELETE': method = requests.delete @@ -396,6 +469,12 @@ class NotifyForm(NotifyBase): results['params'] = {NotifyForm.unquote(x): NotifyForm.unquote(y) for x, y in results['qsd-'].items()} + # Allow Attach-As Support which over-rides the name of the filename + # posted with the form:// + # the default is file01, file02, file03, etc + if 'attach-as' in results['qsd'] and len(results['qsd']['attach-as']): + results['attach_as'] = results['qsd']['attach-as'] + # Set method if not otherwise set if 'method' in results['qsd'] and len(results['qsd']['method']): results['method'] = NotifyForm.unquote(results['qsd']['method']) diff --git a/lib/apprise/plugins/NotifyGitter.py b/lib/apprise/plugins/NotifyGitter.py index f5081ab7..48d14c7c 100644 --- a/lib/apprise/plugins/NotifyGitter.py +++ b/lib/apprise/plugins/NotifyGitter.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Once you visit: https://developer.gitter.im/apps you'll get a personal # access token that will look something like this: diff --git a/lib/apprise/plugins/NotifyGnome.py b/lib/apprise/plugins/NotifyGnome.py index 131b82cc..dc23f736 100644 --- a/lib/apprise/plugins/NotifyGnome.py +++ b/lib/apprise/plugins/NotifyGnome.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. from __future__ import absolute_import from __future__ import print_function diff --git a/lib/apprise/plugins/NotifyGoogleChat.py b/lib/apprise/plugins/NotifyGoogleChat.py index 6a65ba7f..f65b6541 100644 --- a/lib/apprise/plugins/NotifyGoogleChat.py +++ b/lib/apprise/plugins/NotifyGoogleChat.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # For this to work correctly you need to create a webhook. You'll also # need a GSuite account (there are free trials if you don't have one) diff --git a/lib/apprise/plugins/NotifyGotify.py b/lib/apprise/plugins/NotifyGotify.py index 96691514..37922568 100644 --- a/lib/apprise/plugins/NotifyGotify.py +++ b/lib/apprise/plugins/NotifyGotify.py @@ -1,30 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# For this plugin to work correct, the Gotify server must be set up to allow -# for remote connections. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Gotify Docker configuration: https://hub.docker.com/r/gotify/server # Example: https://github.com/gotify/server/blob/\ diff --git a/lib/apprise/plugins/NotifyGrowl.py b/lib/apprise/plugins/NotifyGrowl.py index ca1975f6..9240d62c 100644 --- a/lib/apprise/plugins/NotifyGrowl.py +++ b/lib/apprise/plugins/NotifyGrowl.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. from .NotifyBase import NotifyBase from ..URLBase import PrivacyMode diff --git a/lib/apprise/plugins/NotifyGuilded.py b/lib/apprise/plugins/NotifyGuilded.py index 603bd9fe..8bb9aeea 100644 --- a/lib/apprise/plugins/NotifyGuilded.py +++ b/lib/apprise/plugins/NotifyGuilded.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # For this to work correctly you need to create a webhook. To do this just # click on the little gear icon next to the channel you're part of. From diff --git a/lib/apprise/plugins/NotifyHomeAssistant.py b/lib/apprise/plugins/NotifyHomeAssistant.py index ff4926ea..a403356a 100644 --- a/lib/apprise/plugins/NotifyHomeAssistant.py +++ b/lib/apprise/plugins/NotifyHomeAssistant.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # You must generate a "Long-Lived Access Token". This can be done from your # Home Assistant Profile page. diff --git a/lib/apprise/plugins/NotifyIFTTT.py b/lib/apprise/plugins/NotifyIFTTT.py index 976c22fc..70d51aa6 100644 --- a/lib/apprise/plugins/NotifyIFTTT.py +++ b/lib/apprise/plugins/NotifyIFTTT.py @@ -1,29 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# IFTTT (If-This-Then-That) +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# This code is licensed under the MIT License. +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 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. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 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. + # # For this plugin to work, you need to add the Maker applet to your profile # Simply visit https://ifttt.com/search and search for 'Webhooks' diff --git a/lib/apprise/plugins/NotifyJSON.py b/lib/apprise/plugins/NotifyJSON.py index 2428efef..509c7627 100644 --- a/lib/apprise/plugins/NotifyJSON.py +++ b/lib/apprise/plugins/NotifyJSON.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 import base64 @@ -40,7 +47,8 @@ METHODS = ( 'GET', 'DELETE', 'PUT', - 'HEAD' + 'HEAD', + 'PATCH' ) @@ -308,6 +316,9 @@ class NotifyJSON(NotifyBase): elif self.method == 'PUT': method = requests.put + elif self.method == 'PATCH': + method = requests.patch + elif self.method == 'DELETE': method = requests.delete diff --git a/lib/apprise/plugins/NotifyJoin.py b/lib/apprise/plugins/NotifyJoin.py index 02a9f236..91b2c86b 100644 --- a/lib/apprise/plugins/NotifyJoin.py +++ b/lib/apprise/plugins/NotifyJoin.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Join URL: http://joaoapps.com/join/ # To use this plugin, you need to first access (make sure your browser allows diff --git a/lib/apprise/plugins/NotifyKavenegar.py b/lib/apprise/plugins/NotifyKavenegar.py index f9d17b33..84100b25 100644 --- a/lib/apprise/plugins/NotifyKavenegar.py +++ b/lib/apprise/plugins/NotifyKavenegar.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this service you will need a Kavenegar account from their website # at https://kavenegar.com/ diff --git a/lib/apprise/plugins/NotifyKumulos.py b/lib/apprise/plugins/NotifyKumulos.py index b858bfbd..27e0995c 100644 --- a/lib/apprise/plugins/NotifyKumulos.py +++ b/lib/apprise/plugins/NotifyKumulos.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, you must have a Kumulos account set up. Add a client # and link it with your phone using the phone app (using your Companion App diff --git a/lib/apprise/plugins/NotifyLametric.py b/lib/apprise/plugins/NotifyLametric.py index b2ff0973..1b98b694 100644 --- a/lib/apprise/plugins/NotifyLametric.py +++ b/lib/apprise/plugins/NotifyLametric.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # For LaMetric to work, you need to first setup a custom application on their # website. it can be done as follows: diff --git a/lib/apprise/plugins/NotifyLine.py b/lib/apprise/plugins/NotifyLine.py index cdb17193..65cd0163 100644 --- a/lib/apprise/plugins/NotifyLine.py +++ b/lib/apprise/plugins/NotifyLine.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + # # API Docs: https://developers.line.biz/en/reference/messaging-api/ diff --git a/lib/apprise/plugins/NotifyMQTT.py b/lib/apprise/plugins/NotifyMQTT.py index d6d2b0f0..48094e5f 100644 --- a/lib/apprise/plugins/NotifyMQTT.py +++ b/lib/apprise/plugins/NotifyMQTT.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # PAHO MQTT Documentation: # https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php @@ -317,7 +324,7 @@ class NotifyMQTT(NotifyBase): ciphers=None) # Set our TLS Verify Flag - self.client.tls_insecure_set(self.verify_certificate) + self.client.tls_insecure_set(not self.verify_certificate) # Establish our connection if self.client.connect( diff --git a/lib/apprise/plugins/NotifyMSG91.py b/lib/apprise/plugins/NotifyMSG91.py index 85e9489f..ec94ab9a 100644 --- a/lib/apprise/plugins/NotifyMSG91.py +++ b/lib/apprise/plugins/NotifyMSG91.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Create an account https://msg91.com/ if you don't already have one # diff --git a/lib/apprise/plugins/NotifyMSTeams.py b/lib/apprise/plugins/NotifyMSTeams.py index 98d59fbb..19f9fe34 100644 --- a/lib/apprise/plugins/NotifyMSTeams.py +++ b/lib/apprise/plugins/NotifyMSTeams.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, you need to create a webhook; you can read more about # this here: diff --git a/lib/apprise/plugins/NotifyMacOSX.py b/lib/apprise/plugins/NotifyMacOSX.py index 5c0b1063..59c0620a 100644 --- a/lib/apprise/plugins/NotifyMacOSX.py +++ b/lib/apprise/plugins/NotifyMacOSX.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. from __future__ import absolute_import from __future__ import print_function diff --git a/lib/apprise/plugins/NotifyMailgun.py b/lib/apprise/plugins/NotifyMailgun.py index 524308c7..f6017c82 100644 --- a/lib/apprise/plugins/NotifyMailgun.py +++ b/lib/apprise/plugins/NotifyMailgun.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Signup @ https://www.mailgun.com/ # diff --git a/lib/apprise/plugins/NotifyMastodon.py b/lib/apprise/plugins/NotifyMastodon.py index 90001c1d..cfd7ff48 100644 --- a/lib/apprise/plugins/NotifyMastodon.py +++ b/lib/apprise/plugins/NotifyMastodon.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests @@ -246,7 +253,7 @@ class NotifyMastodon(NotifyBase): self.token = validate_regex(token) if not self.token: - msg = 'An invalid Twitter Consumer Key was specified.' + msg = 'An invalid Mastodon Access Token was specified.' self.logger.warning(msg) raise TypeError(msg) diff --git a/lib/apprise/plugins/NotifyMatrix.py b/lib/apprise/plugins/NotifyMatrix.py index 9f19e128..ca9692aa 100644 --- a/lib/apprise/plugins/NotifyMatrix.py +++ b/lib/apprise/plugins/NotifyMatrix.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Great sources # - https://github.com/matrix-org/matrix-python-sdk @@ -128,7 +135,14 @@ class NotifyMatrix(NotifyBase): image_size = NotifyImageSize.XY_32 # The maximum allowable characters allowed in the body per message - body_maxlen = 1000 + # https://spec.matrix.org/v1.6/client-server-api/#size-limits + # The complete event MUST NOT be larger than 65536 bytes, when formatted + # with the federation event format, including any signatures, and encoded + # as Canonical JSON. + # + # To gracefully allow for some overhead' we'll define a max body length + # of just slighty lower then the limit of the full message itself. + body_maxlen = 65000 # Throttle a wee-bit to avoid thrashing request_rate_per_sec = 0.5 diff --git a/lib/apprise/plugins/NotifyMatterMost.py b/lib/apprise/plugins/NotifyMatterMost.py index 76b6e19d..e62f653c 100644 --- a/lib/apprise/plugins/NotifyMatterMost.py +++ b/lib/apprise/plugins/NotifyMatterMost.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Create an incoming webhook; the website will provide you with something like: # http://localhost:8065/hooks/yobjmukpaw3r3urc5h6i369yima diff --git a/lib/apprise/plugins/NotifyMessageBird.py b/lib/apprise/plugins/NotifyMessageBird.py index 05766334..72c24b6a 100644 --- a/lib/apprise/plugins/NotifyMessageBird.py +++ b/lib/apprise/plugins/NotifyMessageBird.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Create an account https://messagebird.com if you don't already have one # diff --git a/lib/apprise/plugins/NotifyMisskey.py b/lib/apprise/plugins/NotifyMisskey.py new file mode 100644 index 00000000..54c4e628 --- /dev/null +++ b/lib/apprise/plugins/NotifyMisskey.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- +# BSD 3-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron +# +# 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. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. +# 1. visit https://misskey-hub.net/ and see what it's all about if you want. +# Choose a service you want to create an account on from here: +# https://misskey-hub.net/en/instances.html +# +# - For this plugin, I tested using https://misskey.sda1.net and created an +# account. +# +# 2. Generate an API Key: +# - Settings > API > Generate Key +# - Name it whatever you want +# - Assign it 'AT LEAST': +# a. Compose or delete chat messages +# b. Compose or delete notes +# +# +# This plugin also supports taking the URL (as identified above) directly +# as well. + +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import validate_regex +from ..AppriseLocale import gettext_lazy as _ + + +class MisskeyVisibility: + """ + The visibility of any note created + """ + # post will be public + PUBLIC = 'public' + + HOME = 'home' + + FOLLOWERS = 'followers' + + PRIVATE = 'private' + + SPECIFIED = 'specified' + + +# Define the types in a list for validation purposes +MISSKEY_VISIBILITIES = ( + MisskeyVisibility.PUBLIC, + MisskeyVisibility.HOME, + MisskeyVisibility.FOLLOWERS, + MisskeyVisibility.PRIVATE, + MisskeyVisibility.SPECIFIED, +) + + +class NotifyMisskey(NotifyBase): + """ + A wrapper for Misskey Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'Misskey' + + # The services URL + service_url = 'https://misskey-hub.net/' + + # The default protocol + protocol = 'misskey' + + # The default secure protocol + secure_protocol = 'misskeys' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_misskey' + + # The title is not used + title_maxlen = 0 + + # The maximum allowable characters allowed in the body per message + body_maxlen = 512 + + # Define object templates + templates = ( + '{schema}://{project_id}/{msghook}', + ) + + # Define object templates + templates = ( + '{schema}://{token}@{host}', + '{schema}://{token}@{host}:{port}', + ) + + # Define our template arguments + # Define our template arguments + template_tokens = dict(NotifyBase.template_tokens, **{ + 'host': { + 'name': _('Hostname'), + 'type': 'string', + 'required': True, + }, + 'token': { + 'name': _('Access Token'), + 'type': 'string', + 'required': True, + }, + 'port': { + 'name': _('Port'), + 'type': 'int', + 'min': 1, + 'max': 65535, + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'token': { + 'alias_of': 'token', + }, + 'visibility': { + 'name': _('Visibility'), + 'type': 'choice:string', + 'values': MISSKEY_VISIBILITIES, + 'default': MisskeyVisibility.PUBLIC, + }, + }) + + def __init__(self, token=None, visibility=None, **kwargs): + """ + Initialize Misskey Object + """ + super().__init__(**kwargs) + + self.token = validate_regex(token) + if not self.token: + msg = 'An invalid Misskey Access Token was specified.' + self.logger.warning(msg) + raise TypeError(msg) + + if visibility: + # Input is a string; attempt to get the lookup from our + # sound mapping + vis = 'invalid' if not isinstance(visibility, str) \ + else visibility.lower().strip() + + # This little bit of black magic allows us to match against + # against multiple versions of the same string ... etc + self.visibility = \ + next((v for v in MISSKEY_VISIBILITIES + if v.startswith(vis)), None) + + if self.visibility not in MISSKEY_VISIBILITIES: + msg = 'The Misskey visibility specified ({}) is invalid.' \ + .format(visibility) + self.logger.warning(msg) + raise TypeError(msg) + else: + self.visibility = self.template_args['visibility']['default'] + + # Prepare our URL + self.schema = 'https' if self.secure else 'http' + self.api_url = '%s://%s' % (self.schema, self.host) + + if isinstance(self.port, int): + self.api_url += ':%d' % self.port + + return + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + params = { + 'visibility': self.visibility, + } + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + host = self.host + if isinstance(self.port, int): + host += ':%d' % self.port + + return '{schema}://{token}@{host}/?{params}'.format( + schema=self.secure_protocol if self.secure else self.protocol, + host=host, + token=self.pprint(self.token, privacy, safe=''), + params=NotifyMisskey.urlencode(params), + ) + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + wrapper to _send since we can alert more then one channel + """ + + # prepare our headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/json', + } + + # Prepare our payload + payload = { + 'i': self.token, + 'text': body, + 'visibility': self.visibility, + } + + api_url = f'{self.api_url}/api/notes/create' + self.logger.debug('Misskey GET URL: %s (cert_verify=%r)' % ( + api_url, self.verify_certificate)) + self.logger.debug('Misskey Payload: %s' % str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + api_url, + headers=headers, + data=dumps(payload), + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyMisskey.http_response_code_lookup(r.status_code) + + self.logger.warning( + 'Failed to send Misskey 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 Misskey notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending Misskey ' + '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. + + """ + + results = NotifyBase.parse_url(url) + if not results: + # We're done early as we couldn't load the results + return results + + if 'token' in results['qsd'] and len(results['qsd']['token']): + results['token'] = NotifyMisskey.unquote(results['qsd']['token']) + + elif not results['password'] and results['user']: + results['token'] = NotifyMisskey.unquote(results['user']) + + # Capture visibility if specified + if 'visibility' in results['qsd'] and \ + len(results['qsd']['visibility']): + results['visibility'] = \ + NotifyMisskey.unquote(results['qsd']['visibility']) + + return results diff --git a/lib/apprise/plugins/NotifyNextcloud.py b/lib/apprise/plugins/NotifyNextcloud.py index 168290a3..085d02d6 100644 --- a/lib/apprise/plugins/NotifyNextcloud.py +++ b/lib/apprise/plugins/NotifyNextcloud.py @@ -1,26 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CON +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 diff --git a/lib/apprise/plugins/NotifyNextcloudTalk.py b/lib/apprise/plugins/NotifyNextcloudTalk.py index e9c58126..18b191c2 100644 --- a/lib/apprise/plugins/NotifyNextcloudTalk.py +++ b/lib/apprise/plugins/NotifyNextcloudTalk.py @@ -1,26 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CON +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 diff --git a/lib/apprise/plugins/NotifyNotica.py b/lib/apprise/plugins/NotifyNotica.py index 6494e8d9..90bf7ef1 100644 --- a/lib/apprise/plugins/NotifyNotica.py +++ b/lib/apprise/plugins/NotifyNotica.py @@ -1,26 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CON +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # 1. Simply visit https://notica.us # 2. You'll be provided a new variation of the website which will look diff --git a/lib/apprise/plugins/NotifyNotifico.py b/lib/apprise/plugins/NotifyNotifico.py index 00bee4b1..9b1661bf 100644 --- a/lib/apprise/plugins/NotifyNotifico.py +++ b/lib/apprise/plugins/NotifyNotifico.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Notifico allows you to relay notifications into IRC channels. # diff --git a/lib/apprise/plugins/NotifyNtfy.py b/lib/apprise/plugins/NotifyNtfy.py index 8482165b..7efe3487 100644 --- a/lib/apprise/plugins/NotifyNtfy.py +++ b/lib/apprise/plugins/NotifyNtfy.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Great sources # - https://github.com/matrix-org/matrix-python-sdk @@ -40,8 +47,10 @@ from os.path import basename from .NotifyBase import NotifyBase from ..common import NotifyType +from ..common import NotifyImageSize from ..AppriseLocale import gettext_lazy as _ from ..utils import parse_list +from ..utils import parse_bool from ..utils import is_hostname from ..utils import is_ipaddr from ..utils import validate_regex @@ -65,6 +74,27 @@ NTFY_MODES = ( NtfyMode.PRIVATE, ) +# A Simple regular expression used to auto detect Auth mode if it isn't +# otherwise specified: +NTFY_AUTH_DETECT_RE = re.compile('tk_[^ \t]+', re.IGNORECASE) + + +class NtfyAuth: + """ + Define ntfy Authentication Modes + """ + # Basic auth (user and password provided) + BASIC = "basic" + + # Auth Token based + TOKEN = "token" + + +NTFY_AUTH = ( + NtfyAuth.BASIC, + NtfyAuth.TOKEN, +) + class NtfyPriority: """ @@ -142,6 +172,9 @@ class NotifyNtfy(NotifyBase): # Default upstream/cloud host if none is defined cloud_notify_url = 'https://ntfy.sh' + # Allows the user to specify the NotifyImageSize object + image_size = NotifyImageSize.XY_256 + # Message time to live (if remote client isn't around to receive it) time_to_live = 2419200 @@ -158,6 +191,8 @@ class NotifyNtfy(NotifyBase): '{schema}://{user}@{host}:{port}/{targets}', '{schema}://{user}:{password}@{host}/{targets}', '{schema}://{user}:{password}@{host}:{port}/{targets}', + '{schema}://{token}@{host}/{targets}', + '{schema}://{token}@{host}:{port}/{targets}', ) # Define our template tokens @@ -181,6 +216,11 @@ class NotifyNtfy(NotifyBase): 'type': 'string', 'private': True, }, + 'token': { + 'name': _('Token'), + 'type': 'string', + 'private': True, + }, 'topic': { 'name': _('Topic'), 'type': 'string', @@ -199,6 +239,16 @@ class NotifyNtfy(NotifyBase): 'name': _('Attach'), 'type': 'string', }, + 'image': { + 'name': _('Include Image'), + 'type': 'bool', + 'default': True, + 'map_to': 'include_image', + }, + 'avatar_url': { + 'name': _('Avatar URL'), + 'type': 'string', + }, 'filename': { 'name': _('Attach Filename'), 'type': 'string', @@ -231,6 +281,15 @@ class NotifyNtfy(NotifyBase): 'values': NTFY_MODES, 'default': NtfyMode.PRIVATE, }, + 'token': { + 'alias_of': 'token', + }, + 'auth': { + 'name': _('Authentication Type'), + 'type': 'choice:string', + 'values': NTFY_AUTH, + 'default': NtfyAuth.BASIC, + }, 'to': { 'alias_of': 'targets', }, @@ -238,6 +297,7 @@ class NotifyNtfy(NotifyBase): def __init__(self, targets=None, attach=None, filename=None, click=None, delay=None, email=None, priority=None, tags=None, mode=None, + include_image=True, avatar_url=None, auth=None, token=None, **kwargs): """ Initialize ntfy Object @@ -254,6 +314,20 @@ class NotifyNtfy(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + # Show image associated with notification + self.include_image = include_image + + # Prepare our authentication type + self.auth = auth.strip().lower() \ + if isinstance(auth, str) \ + else self.template_args['auth']['default'] + + if self.auth not in NTFY_AUTH: + msg = 'An invalid ntfy Authentication type ({}) was specified.' \ + .format(auth) + self.logger.warning(msg) + raise TypeError(msg) + # Attach a file (URL supported) self.attach = attach @@ -269,6 +343,9 @@ class NotifyNtfy(NotifyBase): # An email to forward notifications to self.email = email + # Save our token + self.token = token + # The Priority of the message self.priority = NotifyNtfy.template_args['priority']['default'] \ if not priority else \ @@ -280,6 +357,11 @@ class NotifyNtfy(NotifyBase): # Any optional tags to attach to the notification self.__tags = parse_list(tags) + # Avatar URL + # This allows a user to provide an over-ride to the otherwise + # dynamically generated avatar url images + self.avatar_url = avatar_url + # Build list of topics topics = parse_list(targets) self.topics = [] @@ -308,6 +390,15 @@ class NotifyNtfy(NotifyBase): self.logger.warning('There are no ntfy topics to notify') return False + # Acquire image_url + image_url = self.image_url(notify_type) + + if self.include_image and (image_url or self.avatar_url): + image_url = \ + self.avatar_url if self.avatar_url else image_url + else: + image_url = None + # Create a copy of the topics topics = list(self.topics) while len(topics) > 0: @@ -336,20 +427,23 @@ class NotifyNtfy(NotifyBase): attachment.url(privacy=True))) okay, response = self._send( - topic, body=_body, title=_title, attach=attachment) + topic, body=_body, title=_title, image_url=image_url, + attach=attachment) if not okay: # We can't post our attachment; abort immediately return False else: # Send our Notification Message - okay, response = self._send(topic, body=body, title=title) + okay, response = self._send( + topic, body=body, title=title, image_url=image_url) if not okay: # Mark our failure, but contiue to move on has_error = True return not has_error - def _send(self, topic, body=None, title=None, attach=None, **kwargs): + def _send(self, topic, body=None, title=None, attach=None, image_url=None, + **kwargs): """ Wrapper to the requests (post) object """ @@ -376,9 +470,17 @@ class NotifyNtfy(NotifyBase): else: # NotifyNtfy.PRVATE # Allow more settings to be applied now - if self.user: + if self.auth == NtfyAuth.BASIC and self.user: auth = (self.user, self.password) + elif self.auth == NtfyAuth.TOKEN: + if not self.token: + self.logger.warning('No Ntfy Token was specified') + return False, None + + # Set Token + headers['Authorization'] = f'Bearer {self.token}' + # Prepare our ntfy Template URL schema = 'https' if self.secure else 'http' @@ -397,6 +499,9 @@ class NotifyNtfy(NotifyBase): virt_payload = params notify_url += '/{topic}'.format(topic=topic) + if image_url: + headers['X-Icon'] = image_url + if title: virt_payload['title'] = title @@ -529,8 +634,13 @@ class NotifyNtfy(NotifyBase): params = { 'priority': self.priority, 'mode': self.mode, + 'image': 'yes' if self.include_image else 'no', + 'auth': self.auth, } + if self.avatar_url: + params['avatar_url'] = self.avatar_url + if self.attach is not None: params['attach'] = self.attach @@ -550,15 +660,22 @@ class NotifyNtfy(NotifyBase): # Determine Authentication auth = '' - if self.user and self.password: - auth = '{user}:{password}@'.format( - user=NotifyNtfy.quote(self.user, safe=''), - password=self.pprint( - self.password, privacy, mode=PrivacyMode.Secret, safe=''), - ) - elif self.user: - auth = '{user}@'.format( - user=NotifyNtfy.quote(self.user, safe=''), + if self.auth == NtfyAuth.BASIC: + if self.user and self.password: + auth = '{user}:{password}@'.format( + user=NotifyNtfy.quote(self.user, safe=''), + password=self.pprint( + self.password, privacy, mode=PrivacyMode.Secret, + safe=''), + ) + elif self.user: + auth = '{user}@'.format( + user=NotifyNtfy.quote(self.user, safe=''), + ) + + elif self.token: # NtfyAuth.TOKEN also + auth = '{token}@'.format( + token=self.pprint(self.token, privacy, safe=''), ) if self.mode == NtfyMode.PRIVATE: @@ -623,6 +740,15 @@ class NotifyNtfy(NotifyBase): results['tags'] = \ parse_list(NotifyNtfy.unquote(results['qsd']['tags'])) + # Boolean to include an image or not + results['include_image'] = parse_bool(results['qsd'].get( + 'image', NotifyNtfy.template_args['image']['default'])) + + # Extract avatar url if it was specified + if 'avatar_url' in results['qsd']: + results['avatar_url'] = \ + NotifyNtfy.unquote(results['qsd']['avatar_url']) + # Acquire our targets/topics results['targets'] = NotifyNtfy.split_path(results['fullpath']) @@ -631,6 +757,37 @@ class NotifyNtfy(NotifyBase): results['targets'] += \ NotifyNtfy.parse_list(results['qsd']['to']) + # Token Specified + if 'token' in results['qsd'] and len(results['qsd']['token']): + # Token presumed to be the one in use + results['auth'] = NtfyAuth.TOKEN + results['token'] = NotifyNtfy.unquote(results['qsd']['token']) + + # Auth override + if 'auth' in results['qsd'] and results['qsd']['auth']: + results['auth'] = NotifyNtfy.unquote( + results['qsd']['auth'].strip().lower()) + + if not results.get('auth') and results['user'] \ + and not results['password']: + # We can try to detect the authentication type on the formatting of + # the username. Look for tk_.* + # + # This isn't a surfire way to do things though; it's best to + # specify the auth= flag + results['auth'] = NtfyAuth.TOKEN \ + if NTFY_AUTH_DETECT_RE.match(results['user']) \ + else NtfyAuth.BASIC + + if results.get('auth') == NtfyAuth.TOKEN and not results.get('token'): + if results['user'] and not results['password']: + # Make sure we properly set our token + results['token'] = NotifyNtfy.unquote(results['user']) + + elif results['password']: + # Make sure we properly set our token + results['token'] = NotifyNtfy.unquote(results['password']) + # Mode override if 'mode' in results['qsd'] and results['qsd']['mode']: results['mode'] = NotifyNtfy.unquote( diff --git a/lib/apprise/plugins/NotifyOffice365.py b/lib/apprise/plugins/NotifyOffice365.py index 1d2f3a0d..0778dd85 100644 --- a/lib/apprise/plugins/NotifyOffice365.py +++ b/lib/apprise/plugins/NotifyOffice365.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # API Details: # https://docs.microsoft.com/en-us/previous-versions/office/\ diff --git a/lib/apprise/plugins/NotifyOneSignal.py b/lib/apprise/plugins/NotifyOneSignal.py index 27212216..70cf0a18 100644 --- a/lib/apprise/plugins/NotifyOneSignal.py +++ b/lib/apprise/plugins/NotifyOneSignal.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # One Signal requires that you've signed up with the service and # generated yourself an API Key and APP ID. diff --git a/lib/apprise/plugins/NotifyOpsgenie.py b/lib/apprise/plugins/NotifyOpsgenie.py index 26d75c4d..858056b2 100644 --- a/lib/apprise/plugins/NotifyOpsgenie.py +++ b/lib/apprise/plugins/NotifyOpsgenie.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Signup @ https://www.opsgenie.com # diff --git a/lib/apprise/plugins/NotifyPagerDuty.py b/lib/apprise/plugins/NotifyPagerDuty.py index 2096e283..a2417275 100644 --- a/lib/apprise/plugins/NotifyPagerDuty.py +++ b/lib/apprise/plugins/NotifyPagerDuty.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # API Refererence: # - https://developer.pagerduty.com/api-reference/\ diff --git a/lib/apprise/plugins/NotifyPagerTree.py b/lib/apprise/plugins/NotifyPagerTree.py new file mode 100644 index 00000000..65a19f61 --- /dev/null +++ b/lib/apprise/plugins/NotifyPagerTree.py @@ -0,0 +1,424 @@ +# -*- coding: utf-8 -*- +# BSD 3-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron +# +# 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. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 json import dumps + +from uuid import uuid4 + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import parse_list +from ..utils import validate_regex +from ..AppriseLocale import gettext_lazy as _ + + +# Actions +class PagerTreeAction: + CREATE = 'create' + ACKNOWLEDGE = 'acknowledge' + RESOLVE = 'resolve' + + +# Urgencies +class PagerTreeUrgency: + SILENT = "silent" + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + + +PAGERTREE_ACTIONS = { + PagerTreeAction.CREATE: 'create', + PagerTreeAction.ACKNOWLEDGE: 'acknowledge', + PagerTreeAction.RESOLVE: 'resolve', +} + +PAGERTREE_URGENCIES = { + # Note: This also acts as a reverse lookup mapping + PagerTreeUrgency.SILENT: 'silent', + PagerTreeUrgency.LOW: 'low', + PagerTreeUrgency.MEDIUM: 'medium', + PagerTreeUrgency.HIGH: 'high', + PagerTreeUrgency.CRITICAL: 'critical', +} +# Extend HTTP Error Messages +PAGERTREE_HTTP_ERROR_MAP = { + 402: 'Payment Required - Please subscribe or upgrade', + 403: 'Forbidden - Blocked', + 404: 'Not Found - Invalid Integration ID', + 405: 'Method Not Allowed - Integration Disabled', + 429: 'Too Many Requests - Rate Limit Exceeded', +} + + +class NotifyPagerTree(NotifyBase): + """ + A wrapper for PagerTree Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'PagerTree' + + # The services URL + service_url = 'https://pagertree.com/' + + # All PagerTree requests are secure + secure_protocol = 'pagertree' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_pagertree' + + # PagerTree uses the http protocol with JSON requests + notify_url = 'https://api.pagertree.com/integration/{}' + + # Define object templates + templates = ( + '{schema}://{integration}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'integration': { + 'name': _('Integration ID'), + 'type': 'string', + 'private': True, + 'required': True, + } + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'action': { + 'name': _('Action'), + 'type': 'choice:string', + 'values': PAGERTREE_ACTIONS, + 'default': PagerTreeAction.CREATE, + }, + 'thirdparty': { + 'name': _('Third Party ID'), + 'type': 'string', + }, + 'urgency': { + 'name': _('Urgency'), + 'type': 'choice:string', + 'values': PAGERTREE_URGENCIES, + }, + 'tags': { + 'name': _('Tags'), + 'type': 'string', + }, + }) + + # Define any kwargs we're using + template_kwargs = { + 'headers': { + 'name': _('HTTP Header'), + 'prefix': '+', + }, + 'payload_extras': { + 'name': _('Payload Extras'), + 'prefix': ':', + }, + 'meta_extras': { + 'name': _('Meta Extras'), + 'prefix': '-', + }, + } + + def __init__(self, integration, action=None, thirdparty=None, + urgency=None, tags=None, headers=None, + payload_extras=None, meta_extras=None, **kwargs): + """ + Initialize PagerTree Object + """ + super().__init__(**kwargs) + + # Integration ID (associated with account) + self.integration = \ + validate_regex(integration, r'^int_[a-zA-Z0-9\-_]{7,14}$') + if not self.integration: + msg = 'An invalid PagerTree Integration ID ' \ + '({}) was specified.'.format(integration) + self.logger.warning(msg) + raise TypeError(msg) + + # thirdparty (optional, in case they want to pass the + # acknowledge or resolve action) + self.thirdparty = None + if thirdparty: + # An id was specified, we want to validate it + self.thirdparty = validate_regex(thirdparty) + if not self.thirdparty: + msg = 'An invalid PagerTree third party ID ' \ + '({}) was specified.'.format(thirdparty) + self.logger.warning(msg) + raise TypeError(msg) + + self.headers = {} + if headers: + # Store our extra headers + self.headers.update(headers) + + self.payload_extras = {} + if payload_extras: + # Store our extra payload entries + self.payload_extras.update(payload_extras) + + self.meta_extras = {} + if meta_extras: + # Store our extra payload entries + self.meta_extras.update(meta_extras) + + # Setup our action + self.action = NotifyPagerTree.template_args['action']['default'] \ + if action not in PAGERTREE_ACTIONS else \ + PAGERTREE_ACTIONS[action] + + # Setup our urgency + self.urgency = \ + None if urgency not in PAGERTREE_URGENCIES else \ + PAGERTREE_URGENCIES[urgency] + + # Any optional tags to attach to the notification + self.__tags = parse_list(tags) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform PagerTree Notification + """ + + # Prepare our headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/json', + } + + # Apply any/all header over-rides defined + # For things like PagerTree Token + headers.update(self.headers) + + # prepare JSON Object + payload = { + # Generate an ID (unless one was explicitly forced to be used) + 'id': self.thirdparty if self.thirdparty else str(uuid4()), + 'event_type': self.action, + } + + if self.action == PagerTreeAction.CREATE: + payload['title'] = title if title else self.app_desc + payload['description'] = body + + payload['meta'] = self.meta_extras + payload['tags'] = self.__tags + + if self.urgency is not None: + payload['urgency'] = self.urgency + + # Apply any/all payload over-rides defined + payload.update(self.payload_extras) + + # Prepare our URL based on integration + notify_url = self.notify_url.format(self.integration) + + self.logger.debug('PagerTree POST URL: %s (cert_verify=%r)' % ( + notify_url, self.verify_certificate, + )) + self.logger.debug('PagerTree Payload: %s' % str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + notify_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + if r.status_code not in ( + requests.codes.ok, requests.codes.created, + requests.codes.accepted): + # We had a problem + status_str = \ + NotifyPagerTree.http_response_code_lookup( + r.status_code) + + self.logger.warning( + 'Failed to send PagerTree 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 PagerTree notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending PagerTree ' + 'notification to %s.' % self.host) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Return; we're done + return False + + return True + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = { + 'action': self.action, + } + + if self.thirdparty: + params['tid'] = self.thirdparty + + if self.urgency: + params['urgency'] = self.urgency + + if self.__tags: + params['tags'] = ','.join([x for x in self.__tags]) + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # Headers prefixed with a '+' sign + # Append our headers into our parameters + params.update({'+{}'.format(k): v for k, v in self.headers.items()}) + + # Meta: {} prefixed with a '-' sign + # Append our meta extras into our parameters + params.update( + {'-{}'.format(k): v for k, v in self.meta_extras.items()}) + + # Payload body extras prefixed with a ':' sign + # Append our payload extras into our parameters + params.update( + {':{}'.format(k): v for k, v in self.payload_extras.items()}) + + return '{schema}://{integration}?{params}'.format( + schema=self.secure_protocol, + # never encode hostname since we're expecting it to be a valid one + integration=self.pprint(self.integration, privacy, safe=''), + params=NotifyPagerTree.urlencode(params), + ) + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + results = NotifyBase.parse_url(url, verify_host=False) + + if not results: + # We're done early as we couldn't load the results + return results + + # Add our headers that the user can potentially over-ride if they wish + # to to our returned result set and tidy entries by unquoting them + results['headers'] = { + NotifyPagerTree.unquote(x): NotifyPagerTree.unquote(y) + for x, y in results['qsd+'].items() + } + + # store any additional payload extra's defined + results['payload_extras'] = { + NotifyPagerTree.unquote(x): NotifyPagerTree.unquote(y) + for x, y in results['qsd:'].items() + } + + # store any additional meta extra's defined + results['meta_extras'] = { + NotifyPagerTree.unquote(x): NotifyPagerTree.unquote(y) + for x, y in results['qsd-'].items() + } + + # Integration ID + if 'id' in results['qsd'] and len(results['qsd']['id']): + # Shortened version of integration id + results['integration'] = \ + NotifyPagerTree.unquote(results['qsd']['id']) + + elif 'integration' in results['qsd'] and \ + len(results['qsd']['integration']): + results['integration'] = \ + NotifyPagerTree.unquote(results['qsd']['integration']) + + else: + results['integration'] = \ + NotifyPagerTree.unquote(results['host']) + + # Set our thirdparty + + if 'tid' in results['qsd'] and len(results['qsd']['tid']): + # Shortened version of thirdparty + results['thirdparty'] = \ + NotifyPagerTree.unquote(results['qsd']['tid']) + + elif 'thirdparty' in results['qsd'] and \ + len(results['qsd']['thirdparty']): + results['thirdparty'] = \ + NotifyPagerTree.unquote(results['qsd']['thirdparty']) + + # Set our urgency + if 'action' in results['qsd'] and \ + len(results['qsd']['action']): + results['action'] = \ + NotifyPagerTree.unquote(results['qsd']['action']) + + # Set our urgency + if 'urgency' in results['qsd'] and len(results['qsd']['urgency']): + results['urgency'] = \ + NotifyPagerTree.unquote(results['qsd']['urgency']) + + # Set our tags + if 'tags' in results['qsd'] and len(results['qsd']['tags']): + results['tags'] = \ + parse_list(NotifyPagerTree.unquote(results['qsd']['tags'])) + + return results diff --git a/lib/apprise/plugins/NotifyParsePlatform.py b/lib/apprise/plugins/NotifyParsePlatform.py index 3944b902..69efb61c 100644 --- a/lib/apprise/plugins/NotifyParsePlatform.py +++ b/lib/apprise/plugins/NotifyParsePlatform.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Official API reference: https://developer.gitter.im/docs/user-resource diff --git a/lib/apprise/plugins/NotifyPopcornNotify.py b/lib/apprise/plugins/NotifyPopcornNotify.py index 0fda030c..9ea0c77e 100644 --- a/lib/apprise/plugins/NotifyPopcornNotify.py +++ b/lib/apprise/plugins/NotifyPopcornNotify.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 diff --git a/lib/apprise/plugins/NotifyProwl.py b/lib/apprise/plugins/NotifyProwl.py index 2bb714cb..cebe0701 100644 --- a/lib/apprise/plugins/NotifyProwl.py +++ b/lib/apprise/plugins/NotifyProwl.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 diff --git a/lib/apprise/plugins/NotifyPushBullet.py b/lib/apprise/plugins/NotifyPushBullet.py index e84b67de..c37532d1 100644 --- a/lib/apprise/plugins/NotifyPushBullet.py +++ b/lib/apprise/plugins/NotifyPushBullet.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 json import dumps diff --git a/lib/apprise/plugins/NotifyPushSafer.py b/lib/apprise/plugins/NotifyPushSafer.py index 6275185e..48a0f3bb 100644 --- a/lib/apprise/plugins/NotifyPushSafer.py +++ b/lib/apprise/plugins/NotifyPushSafer.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 base64 import requests diff --git a/lib/apprise/plugins/NotifyPushed.py b/lib/apprise/plugins/NotifyPushed.py index 4677b151..822ea1ad 100644 --- a/lib/apprise/plugins/NotifyPushed.py +++ b/lib/apprise/plugins/NotifyPushed.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests diff --git a/lib/apprise/plugins/NotifyPushjet.py b/lib/apprise/plugins/NotifyPushjet.py index d335a993..c6e36a39 100644 --- a/lib/apprise/plugins/NotifyPushjet.py +++ b/lib/apprise/plugins/NotifyPushjet.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 json import dumps diff --git a/lib/apprise/plugins/NotifyPushover.py b/lib/apprise/plugins/NotifyPushover.py index 24e21395..1e943db1 100644 --- a/lib/apprise/plugins/NotifyPushover.py +++ b/lib/apprise/plugins/NotifyPushover.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests @@ -158,7 +165,7 @@ class NotifyPushover(NotifyBase): notify_url = 'https://api.pushover.net/1/messages.json' # The maximum allowable characters allowed in the body per message - body_maxlen = 512 + body_maxlen = 1024 # Default Pushover sound default_pushover_sound = PushoverSound.PUSHOVER diff --git a/lib/apprise/plugins/NotifyReddit.py b/lib/apprise/plugins/NotifyReddit.py index 3526f298..4e54109c 100644 --- a/lib/apprise/plugins/NotifyReddit.py +++ b/lib/apprise/plugins/NotifyReddit.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + # # 1. Visit https://www.reddit.com/prefs/apps and scroll to the bottom # 2. Click on the button that reads 'are you a developer? create an app...' diff --git a/lib/apprise/plugins/NotifyRocketChat.py b/lib/apprise/plugins/NotifyRocketChat.py index 27ca0deb..c8f5e965 100644 --- a/lib/apprise/plugins/NotifyRocketChat.py +++ b/lib/apprise/plugins/NotifyRocketChat.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests diff --git a/lib/apprise/plugins/NotifyRyver.py b/lib/apprise/plugins/NotifyRyver.py index ea983d98..b8b34a3c 100644 --- a/lib/apprise/plugins/NotifyRyver.py +++ b/lib/apprise/plugins/NotifyRyver.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, you need to first generate a webhook. diff --git a/lib/apprise/plugins/NotifySES.py b/lib/apprise/plugins/NotifySES.py index fb87084e..e58893bd 100644 --- a/lib/apprise/plugins/NotifySES.py +++ b/lib/apprise/plugins/NotifySES.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # API Information: # - https://docs.aws.amazon.com/ses/latest/APIReference/API_SendRawEmail.html diff --git a/lib/apprise/plugins/NotifySMSEagle.py b/lib/apprise/plugins/NotifySMSEagle.py index ef2909d7..50b44cf3 100644 --- a/lib/apprise/plugins/NotifySMSEagle.py +++ b/lib/apprise/plugins/NotifySMSEagle.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests diff --git a/lib/apprise/plugins/NotifySMTP2Go.py b/lib/apprise/plugins/NotifySMTP2Go.py index 4a87ae07..3c672694 100644 --- a/lib/apprise/plugins/NotifySMTP2Go.py +++ b/lib/apprise/plugins/NotifySMTP2Go.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Signup @ https://smtp2go.com (free accounts available) # diff --git a/lib/apprise/plugins/NotifySNS.py b/lib/apprise/plugins/NotifySNS.py index f3aff85f..28eea46b 100644 --- a/lib/apprise/plugins/NotifySNS.py +++ b/lib/apprise/plugins/NotifySNS.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import hmac diff --git a/lib/apprise/plugins/NotifySendGrid.py b/lib/apprise/plugins/NotifySendGrid.py index 46690fc4..d811fa1d 100644 --- a/lib/apprise/plugins/NotifySendGrid.py +++ b/lib/apprise/plugins/NotifySendGrid.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + # # You will need an API Key for this plugin to work. # From the Settings -> API Keys you can click "Create API Key" if you don't diff --git a/lib/apprise/plugins/NotifyServerChan.py b/lib/apprise/plugins/NotifyServerChan.py index 21d97a78..6fa8c557 100644 --- a/lib/apprise/plugins/NotifyServerChan.py +++ b/lib/apprise/plugins/NotifyServerChan.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests diff --git a/lib/apprise/plugins/NotifySignalAPI.py b/lib/apprise/plugins/NotifySignalAPI.py index 73037a15..46708d19 100644 --- a/lib/apprise/plugins/NotifySignalAPI.py +++ b/lib/apprise/plugins/NotifySignalAPI.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2022 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests diff --git a/lib/apprise/plugins/NotifySimplePush.py b/lib/apprise/plugins/NotifySimplePush.py index 2cd0f8ef..25066067 100644 --- a/lib/apprise/plugins/NotifySimplePush.py +++ b/lib/apprise/plugins/NotifySimplePush.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + from os import urandom from json import loads import requests diff --git a/lib/apprise/plugins/NotifySinch.py b/lib/apprise/plugins/NotifySinch.py index 802ed368..c4d400b0 100644 --- a/lib/apprise/plugins/NotifySinch.py +++ b/lib/apprise/plugins/NotifySinch.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this service you will need a Sinch account to which you can get your # API_TOKEN and SERVICE_PLAN_ID right from your console/dashboard at: diff --git a/lib/apprise/plugins/NotifySlack.py b/lib/apprise/plugins/NotifySlack.py index fb081d7d..1a437ffa 100644 --- a/lib/apprise/plugins/NotifySlack.py +++ b/lib/apprise/plugins/NotifySlack.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # There are 2 ways to use this plugin... # Method 1: Via Webhook: diff --git a/lib/apprise/plugins/NotifySparkPost.py b/lib/apprise/plugins/NotifySparkPost.py index f4dbc65a..bf83a9f5 100644 --- a/lib/apprise/plugins/NotifySparkPost.py +++ b/lib/apprise/plugins/NotifySparkPost.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Signup @ https://www.sparkpost.com # diff --git a/lib/apprise/plugins/NotifySpontit.py b/lib/apprise/plugins/NotifySpontit.py index d1bfdc56..4df8b538 100644 --- a/lib/apprise/plugins/NotifySpontit.py +++ b/lib/apprise/plugins/NotifySpontit.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2020 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this service you will need a Spontit account from their website # at https://spontit.com/ diff --git a/lib/apprise/plugins/NotifyStreamlabs.py b/lib/apprise/plugins/NotifyStreamlabs.py index ae2371ea..3489519a 100644 --- a/lib/apprise/plugins/NotifyStreamlabs.py +++ b/lib/apprise/plugins/NotifyStreamlabs.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2021 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # For this to work correctly you need to register an app # and generate an access token diff --git a/lib/apprise/plugins/NotifySyslog.py b/lib/apprise/plugins/NotifySyslog.py index 25be43a6..433aab9c 100644 --- a/lib/apprise/plugins/NotifySyslog.py +++ b/lib/apprise/plugins/NotifySyslog.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 os import syslog import socket diff --git a/lib/apprise/plugins/NotifyTechulusPush.py b/lib/apprise/plugins/NotifyTechulusPush.py index dbef9880..0f3e79e5 100644 --- a/lib/apprise/plugins/NotifyTechulusPush.py +++ b/lib/apprise/plugins/NotifyTechulusPush.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, you need to download the app # - Apple: https://itunes.apple.com/us/app/\ diff --git a/lib/apprise/plugins/NotifyTelegram.py b/lib/apprise/plugins/NotifyTelegram.py index 9c8fa92a..e7d75f5a 100644 --- a/lib/apprise/plugins/NotifyTelegram.py +++ b/lib/apprise/plugins/NotifyTelegram.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, you need to first access https://api.telegram.org # You need to create a bot and acquire it's Token Identifier (bot_token) diff --git a/lib/apprise/plugins/NotifyTwilio.py b/lib/apprise/plugins/NotifyTwilio.py index 3af118a9..78a1ba82 100644 --- a/lib/apprise/plugins/NotifyTwilio.py +++ b/lib/apprise/plugins/NotifyTwilio.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this service you will need a Twilio account to which you can get your # AUTH_TOKEN and ACCOUNT SID right from your console/dashboard at: diff --git a/lib/apprise/plugins/NotifyTwist.py b/lib/apprise/plugins/NotifyTwist.py index 9f8415c3..a19f03a9 100644 --- a/lib/apprise/plugins/NotifyTwist.py +++ b/lib/apprise/plugins/NotifyTwist.py @@ -1,27 +1,35 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + # # All of the documentation needed to work with the Twist API can be found # here: https://developer.twist.com/v3/ diff --git a/lib/apprise/plugins/NotifyTwitter.py b/lib/apprise/plugins/NotifyTwitter.py index 33f97c97..76c1a8e6 100644 --- a/lib/apprise/plugins/NotifyTwitter.py +++ b/lib/apprise/plugins/NotifyTwitter.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # See https://developer.twitter.com/en/docs/direct-messages/\ # sending-and-receiving/api-reference/new-event.html diff --git a/lib/apprise/plugins/NotifyVoipms.py b/lib/apprise/plugins/NotifyVoipms.py new file mode 100644 index 00000000..42379b6b --- /dev/null +++ b/lib/apprise/plugins/NotifyVoipms.py @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- +# BSD 3-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron +# +# 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. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. + +# Create an account https://voip.ms/ if you don't already have one +# +# Enable API and set an API password here: +# - https://voip.ms/m/api.php +# +# Read more about VoIP.ms API here: +# - https://voip.ms/m/apidocs.php + +import requests +from json import loads + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import is_phone_no +from ..utils import is_email +from ..utils import parse_phone_no +from ..AppriseLocale import gettext_lazy as _ + + +class NotifyVoipms(NotifyBase): + """ + A wrapper for Voipms Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'VoIPms' + + # The services URL + service_url = 'https://voip.ms' + + # The default protocol + secure_protocol = 'voipms' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_voipms' + + # Voipms uses the http protocol with JSON requests + notify_url = 'https://voip.ms/api/v1/rest.php' + + # The maximum length of the body + body_maxlen = 160 + + # A title can not be used for SMS Messages. Setting this to zero will + # cause any title (if defined) to get placed into the message body. + title_maxlen = 0 + + # Define object templates + templates = ( + '{schema}://{password}:{email}', + '{schema}://{password}:{email}/{from_phone}/{targets}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'email': { + 'name': _('User Email'), + 'type': 'string', + 'required': True, + }, + 'password': { + 'name': _('Password'), + 'type': 'string', + 'private': True, + 'required': True, + }, + 'from_phone': { + 'name': _('From Phone No'), + 'type': 'string', + 'regex': (r'^\+?[0-9\s)(+-]+$', 'i'), + 'map_to': 'source', + }, + 'target_phone': { + 'name': _('Target Phone No'), + 'type': 'string', + 'prefix': '+', + 'regex': (r'^[0-9\s)(+-]+$', 'i'), + 'map_to': 'targets', + }, + 'targets': { + 'name': _('Targets'), + 'type': 'list:string', + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'to': { + 'alias_of': 'targets', + }, + 'from': { + 'alias_of': 'from_phone', + }, + }) + + def __init__(self, email, source=None, targets=None, **kwargs): + """ + Initialize Voipms Object + """ + super().__init__(**kwargs) + + # Validate our params here. + + if self.password is None: + msg = 'Password has to be specified.' + self.logger.warning(msg) + raise TypeError(msg) + + # User is the email associated with the account + result = is_email(email) + if not result: + msg = 'An invalid Voipms user email: ' \ + '({}) was specified.'.format(email) + self.logger.warning(msg) + raise TypeError(msg) + self.email = result['full_email'] + + # Validate our source Phone # + result = is_phone_no(source) + if not result: + msg = 'An invalid Voipms source phone # ' \ + '({}) was specified.'.format(source) + self.logger.warning(msg) + raise TypeError(msg) + + # Source Phone # only supports +1 country code + # Allow 7 digit phones (presume they're local with +1 country code) + if result['country'] and result['country'] != '1': + msg = 'Voipms only supports +1 country code ' \ + '({}) was specified.'.format(source) + self.logger.warning(msg) + raise TypeError(msg) + + # Store our source phone number (without country code) + self.source = result['area'] + result['line'] + + # Parse our targets + self.targets = list() + + if targets: + for target in parse_phone_no(targets): + # Validate targets and drop bad ones: + result = is_phone_no(target) + + # Target Phone # only supports +1 country code + if result['country'] != '1': + self.logger.warning( + 'Dropped invalid phone # ' + '({}) specified.'.format(target), + ) + continue + + # store valid phone number + self.targets.append(result['area'] + result['line']) + + else: + # Send a message to ourselves + self.targets.append(self.source) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform Voipms Notification + """ + + if len(self.targets) == 0: + # There were no services to notify + self.logger.warning('There were no Voipms targets to notify.') + return False + + # error tracking (used for function return) + has_error = False + + # Prepare our headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/x-www-form-urlencoded', + } + + # Prepare our payload + payload = { + 'api_username': self.email, + 'api_password': self.password, + 'did': self.source, + 'message': body, + 'method': 'sendSMS', + + # Gets filled in the loop below + 'dst': None + } + + # Create a copy of the targets list + targets = list(self.targets) + + while len(targets): + # Get our target to notify + target = targets.pop(0) + + # Add target Phone # + payload['dst'] = target + + # Some Debug Logging + self.logger.debug('Voipms GET URL: {} (cert_verify={})'.format( + self.notify_url, self.verify_certificate)) + self.logger.debug('Voipms Payload: {}' .format(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + response = {'status': 'unknown', 'message': ''} + + try: + r = requests.get( + self.notify_url, + params=payload, + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + + try: + response = loads(r.content) + + except (AttributeError, TypeError, ValueError): + # ValueError = r.content is Unparsable + # TypeError = r.content is None + # AttributeError = r is None + pass + + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyVoipms.http_response_code_lookup( + r.status_code) + + self.logger.warning( + 'Failed to send Voipms notification to {}: ' + '{}{}error={}.'.format( + target, + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + # Mark our failure + has_error = True + continue + + # Voipms sends 200 OK even if there is an error + # check if status in response and if it is not success + + if response is not None and response['status'] != 'success': + self.logger.warning( + 'Failed to send Voipms notification to {}: ' + 'status: {}, message: {}'.format( + target, response['status'], response['message']) + ) + + # Mark our failure + has_error = True + continue + else: + self.logger.info( + 'Sent Voipms notification to %s' % target) + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending Voipms:%s ' + 'notification.' % target + ) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Mark our failure + has_error = True + continue + + return not has_error + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any URL parameters + params = self.url_parameters(privacy=privacy, *args, **kwargs) + + schemaStr = \ + '{schema}://{password}:{email}/{from_phone}/{targets}/?{params}' + return schemaStr.format( + schema=self.secure_protocol, + email=self.email, + password=self.pprint(self.password, privacy, safe=''), + from_phone='1' + self.pprint(self.source, privacy, safe=''), + targets='/'.join( + ['1' + NotifyVoipms.quote(x, safe='') for x in self.targets]), + params=NotifyVoipms.urlencode(params)) + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to re-instantiate this object. + + """ + + results = NotifyBase.parse_url(url, verify_host=False) + if not results: + # We're done early as we couldn't load the results + return results + + results['targets'] = \ + NotifyVoipms.split_path(results['fullpath']) + + if 'from' in results['qsd'] and len(results['qsd']['from']): + results['source'] = \ + NotifyVoipms.unquote(results['qsd']['from']) + + elif results['targets']: + # The from phone no is the first entry in the list otherwise + results['source'] = results['targets'].pop(0) + + # Swap user for pass since our input is: password:email + # where email is user@hostname (or user@domain) + user = results['password'] + password = results['user'] + results['password'] = password + results['user'] = user + + results['email'] = '{}@{}'.format( + NotifyVoipms.unquote(user), + NotifyVoipms.unquote(results['host']), + ) + + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyVoipms.parse_phone_no(results['qsd']['to']) + + return results diff --git a/lib/apprise/plugins/NotifyVonage.py b/lib/apprise/plugins/NotifyVonage.py index bf5713ca..812c3643 100644 --- a/lib/apprise/plugins/NotifyVonage.py +++ b/lib/apprise/plugins/NotifyVonage.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # Sign-up with https://dashboard.nexmo.com/ # diff --git a/lib/apprise/plugins/NotifyWebexTeams.py b/lib/apprise/plugins/NotifyWebexTeams.py index 3f88f812..6b953b71 100644 --- a/lib/apprise/plugins/NotifyWebexTeams.py +++ b/lib/apprise/plugins/NotifyWebexTeams.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # At the time I created this plugin, their website had lots of issues with the # Firefox Browser. I fell back to Chrome and had no problems. @@ -88,7 +95,7 @@ class NotifyWebexTeams(NotifyBase): service_url = 'https://webex.teams.com/' # The default secure protocol - secure_protocol = 'wxteams' + secure_protocol = ('wxteams', 'webex') # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_wxteams' @@ -117,7 +124,7 @@ class NotifyWebexTeams(NotifyBase): 'type': 'string', 'private': True, 'required': True, - 'regex': (r'^[a-z0-9]{80}$', 'i'), + 'regex': (r'^[a-z0-9]{80,160}$', 'i'), }, }) @@ -213,7 +220,7 @@ class NotifyWebexTeams(NotifyBase): params = self.url_parameters(privacy=privacy, *args, **kwargs) return '{schema}://{token}/?{params}'.format( - schema=self.secure_protocol, + schema=self.secure_protocol[0], token=self.pprint(self.token, privacy, safe=''), params=NotifyWebexTeams.urlencode(params), ) @@ -242,14 +249,15 @@ class NotifyWebexTeams(NotifyBase): """ result = re.match( - r'^https?://api\.ciscospark\.com/v[1-9][0-9]*/webhooks/incoming/' + r'^https?://(api\.ciscospark\.com|webexapis\.com)' + r'/v[1-9][0-9]*/webhooks/incoming/' r'(?P[A-Z0-9_-]+)/?' r'(?P\?.+)?$', url, re.I) if result: return NotifyWebexTeams.parse_url( '{schema}://{webhook_token}/{params}'.format( - schema=NotifyWebexTeams.secure_protocol, + schema=NotifyWebexTeams.secure_protocol[0], webhook_token=result.group('webhook_token'), params='' if not result.group('params') else result.group('params'))) diff --git a/lib/apprise/plugins/NotifyWindows.py b/lib/apprise/plugins/NotifyWindows.py index 99f7390c..b05e2ebb 100644 --- a/lib/apprise/plugins/NotifyWindows.py +++ b/lib/apprise/plugins/NotifyWindows.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. from __future__ import absolute_import from __future__ import print_function diff --git a/lib/apprise/plugins/NotifyXBMC.py b/lib/apprise/plugins/NotifyXBMC.py index 6c2a6d49..963a74d8 100644 --- a/lib/apprise/plugins/NotifyXBMC.py +++ b/lib/apprise/plugins/NotifyXBMC.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 json import dumps diff --git a/lib/apprise/plugins/NotifyXML.py b/lib/apprise/plugins/NotifyXML.py index 96c2f561..bbb3046a 100644 --- a/lib/apprise/plugins/NotifyXML.py +++ b/lib/apprise/plugins/NotifyXML.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import requests @@ -40,7 +47,8 @@ METHODS = ( 'GET', 'DELETE', 'PUT', - 'HEAD' + 'HEAD', + 'PATCH' ) @@ -360,6 +368,9 @@ class NotifyXML(NotifyBase): elif self.method == 'PUT': method = requests.put + elif self.method == 'PATCH': + method = requests.patch + elif self.method == 'DELETE': method = requests.delete diff --git a/lib/apprise/plugins/NotifyZulip.py b/lib/apprise/plugins/NotifyZulip.py index 362d309e..19f3e29e 100644 --- a/lib/apprise/plugins/NotifyZulip.py +++ b/lib/apprise/plugins/NotifyZulip.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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. # To use this plugin, you must have a ZulipChat bot defined; See here: # https://zulipchat.com/help/add-a-bot-or-integration diff --git a/lib/apprise/plugins/__init__.py b/lib/apprise/plugins/__init__.py index 5c0b3532..5560568b 100644 --- a/lib/apprise/plugins/__init__.py +++ b/lib/apprise/plugins/__init__.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 os import re diff --git a/lib/apprise/utils.py b/lib/apprise/utils.py index 0f1876b2..60db30b1 100644 --- a/lib/apprise/utils.py +++ b/lib/apprise/utils.py @@ -1,27 +1,34 @@ # -*- coding: utf-8 -*- +# BSD 3-Clause License # -# Copyright (C) 2019 Chris Caron -# All rights reserved. +# Apprise - Push Notification Library. +# Copyright (c) 2023, Chris Caron # -# This code is licensed under the MIT License. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files(the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions : +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# 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 re import sys @@ -47,10 +54,6 @@ def import_module(path, name): """ Load our module based on path """ - # if path.endswith('test_module_detection0/a/hook.py'): - # import pdb - # pdb.set_trace() - spec = importlib.util.spec_from_file_location(name, path) try: module = importlib.util.module_from_spec(spec) From 8e8568adb3eb219c0ad2e571cdc62767100a0c07 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Thu, 16 Mar 2023 04:19:03 +0000 Subject: [PATCH 11/36] Change add jobs to centralise scheduler activities. Change refactor scene_exceptions. --- CHANGES.md | 2 + sickgear.py | 8 +- sickgear/__init__.py | 176 ++-- ...ost_processer.py => auto_media_process.py} | 15 +- sickgear/config.py | 12 +- sickgear/event_queue.py | 23 +- sickgear/generic_queue.py | 68 +- sickgear/name_cache.py | 50 +- sickgear/name_parser/parser.py | 3 +- sickgear/processTV.py | 11 +- sickgear/properFinder.py | 4 +- sickgear/providers/generic.py | 8 +- sickgear/providers/newznab.py | 6 +- sickgear/scene_exceptions.py | 972 +++++++++--------- sickgear/scene_numbering.py | 6 +- sickgear/scheduler.py | 85 +- sickgear/search_backlog.py | 15 +- sickgear/search_propers.py | 13 +- sickgear/search_recent.py | 12 +- sickgear/show_name_helpers.py | 8 +- sickgear/show_queue.py | 8 +- sickgear/show_updater.py | 20 +- sickgear/subtitles.py | 9 +- sickgear/version_checker.py | 23 +- sickgear/watchedstate.py | 16 +- sickgear/webapi.py | 39 +- sickgear/webserve.py | 88 +- tests/scene_helpers_tests.py | 23 +- tests/webapi_tests.py | 2 +- 29 files changed, 929 insertions(+), 796 deletions(-) rename sickgear/{auto_post_processer.py => auto_media_process.py} (73%) diff --git a/CHANGES.md b/CHANGES.md index 0bcb07a3..a1c15055 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) * Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) * Update urllib3 1.26.14 (a06c05c) to 1.26.15 (25cca389) +* Change add jobs to centralise scheduler activities +* Change refactor scene_exceptions ### 3.28.0 (2023-04-12 13:05:00 UTC) diff --git a/sickgear.py b/sickgear.py index 056fcd3b..b664f58f 100755 --- a/sickgear.py +++ b/sickgear.py @@ -555,9 +555,9 @@ class SickGear(object): name_cache.build_name_cache() # load all ids from xem - sickgear.classes.loading_msg.message = 'Loading xem data' - startup_background_tasks = threading.Thread(name='XEMUPDATER', target=sickgear.scene_exceptions.get_xem_ids) - startup_background_tasks.start() +# sickgear.classes.loading_msg.message = 'Loading xem data' +# startup_background_tasks = threading.Thread(name='XEMUPDATER', target=sickgear.scene_exceptions.ReleaseMap().fetch_xem_ids) +# startup_background_tasks.start() sickgear.classes.loading_msg.message = 'Checking history' # check history snatched_proper update @@ -624,7 +624,7 @@ class SickGear(object): if not switching and (self.force_update or sickgear.UPDATE_SHOWS_ON_START): sickgear.classes.loading_msg.message = 'Starting a forced show update' background_start_forced_show_update = threading.Thread(name='STARTUP-FORCE-SHOW-UPDATE', - target=sickgear.show_update_scheduler.action.run) + target=sickgear.update_show_scheduler.action.run) background_start_forced_show_update.start() sickgear.classes.loading_msg.message = 'Switching to default web server' diff --git a/sickgear/__init__.py b/sickgear/__init__.py index 305952a2..1accc16d 100644 --- a/sickgear/__init__.py +++ b/sickgear/__init__.py @@ -37,7 +37,7 @@ import zlib from . import classes, db, helpers, image_cache, indexermapper, logger, metadata, naming, people_queue, providers, \ scene_exceptions, scene_numbering, scheduler, search_backlog, search_propers, search_queue, search_recent, \ show_queue, show_updater, subtitles, trakt_helpers, version_checker, watchedstate_queue -from . import auto_post_processer, properFinder # must come after the above imports +from . import auto_media_process, properFinder # must come after the above imports from .common import SD, SKIPPED, USER_AGENT from .config import check_section, check_setting_int, check_setting_str, ConfigMigrator, minimax from .databases import cache_db, failed_db, mainDB @@ -61,7 +61,7 @@ import sg_helpers # noinspection PyUnreachableCode if False: - from typing import AnyStr, Dict, List + from typing import AnyStr, Dict, List, Optional from adba import Connection from .event_queue import Events from .tv import TVShow @@ -88,23 +88,25 @@ DATA_DIR = '' # noinspection PyTypeChecker events = None # type: Events -recent_search_scheduler = None -backlog_search_scheduler = None -show_update_scheduler = None -people_queue_scheduler = None -update_software_scheduler = None -update_packages_scheduler = None -show_queue_scheduler = None -search_queue_scheduler = None -proper_finder_scheduler = None -media_process_scheduler = None -subtitles_finder_scheduler = None -# trakt_checker_scheduler = None -emby_watched_state_scheduler = None -plex_watched_state_scheduler = None -watched_state_queue_scheduler = None +show_queue_scheduler = None # type: Optional[scheduler.Scheduler] +search_queue_scheduler = None # type: Optional[scheduler.Scheduler] +people_queue_scheduler = None # type: Optional[scheduler.Scheduler] +watched_state_queue_scheduler = None # type: Optional[scheduler.Scheduler] +update_software_scheduler = None # type: Optional[scheduler.Scheduler] +update_packages_scheduler = None # type: Optional[scheduler.Scheduler] +update_show_scheduler = None # type: Optional[scheduler.Scheduler] +update_release_mappings_scheduler = None # type: Optional[scheduler.Scheduler] +search_recent_scheduler = None # type: Optional[scheduler.Scheduler] +search_backlog_scheduler = None # type: Optional[search_backlog.BacklogSearchScheduler] +search_propers_scheduler = None # type: Optional[scheduler.Scheduler] +search_subtitles_scheduler = None # type: Optional[scheduler.Scheduler] +emby_watched_state_scheduler = None # type: Optional[scheduler.Scheduler] +plex_watched_state_scheduler = None # type: Optional[scheduler.Scheduler] +process_media_scheduler = None # type: Optional[scheduler.Scheduler] # noinspection PyTypeChecker background_mapping_task = None # type: threading.Thread +# deprecated +# trakt_checker_scheduler = None provider_ping_thread_pool = {} @@ -624,9 +626,11 @@ __INITIALIZED__ = False __INIT_STAGE__ = 0 # don't reassign MEMCACHE var without reassigning sg_helpers.MEMCACHE +# and scene_exceptions.MEMCACHE # as long as the pointer is the same (dict only modified) all is fine MEMCACHE = {} sg_helpers.MEMCACHE = MEMCACHE +scene_exceptions.MEMCACHE = MEMCACHE MEMCACHE_FLAG_IMAGES = {} @@ -1518,11 +1522,14 @@ def init_stage_2(): global __INITIALIZED__, MEMCACHE, MEMCACHE_FLAG_IMAGES, RECENTSEARCH_STARTUP # Schedulers # global trakt_checker_scheduler - global recent_search_scheduler, backlog_search_scheduler, people_queue_scheduler, show_update_scheduler, \ - update_software_scheduler, update_packages_scheduler, show_queue_scheduler, search_queue_scheduler, \ - proper_finder_scheduler, media_process_scheduler, subtitles_finder_scheduler, \ - background_mapping_task, \ - watched_state_queue_scheduler, emby_watched_state_scheduler, plex_watched_state_scheduler + global update_software_scheduler, update_packages_scheduler, \ + update_show_scheduler, update_release_mappings_scheduler, \ + search_backlog_scheduler, search_propers_scheduler, \ + search_recent_scheduler, search_subtitles_scheduler, \ + search_queue_scheduler, show_queue_scheduler, people_queue_scheduler, \ + watched_state_queue_scheduler, emby_watched_state_scheduler, plex_watched_state_scheduler, \ + process_media_scheduler, background_mapping_task + # Gen Config/Misc global SHOW_UPDATE_HOUR, UPDATE_INTERVAL, UPDATE_PACKAGES_INTERVAL # Search Settings/Episode @@ -1570,32 +1577,17 @@ def init_stage_2(): metadata_provider_dict[tmp_provider.name] = tmp_provider # initialize schedulers - # updaters - update_now = datetime.timedelta(minutes=0) - update_software_scheduler = scheduler.Scheduler( - version_checker.SoftwareUpdater(), - cycle_time=datetime.timedelta(hours=UPDATE_INTERVAL), - thread_name='SOFTWAREUPDATER', - silent=False) - - update_packages_scheduler = scheduler.Scheduler( - version_checker.PackagesUpdater(), - cycle_time=datetime.timedelta(hours=UPDATE_PACKAGES_INTERVAL), - # run_delay=datetime.timedelta(minutes=2), - thread_name='PACKAGESUPDATER', - silent=False) - + # / + # queues must be first show_queue_scheduler = scheduler.Scheduler( show_queue.ShowQueue(), cycle_time=datetime.timedelta(seconds=3), thread_name='SHOWQUEUE') - show_update_scheduler = scheduler.Scheduler( - show_updater.ShowUpdater(), - cycle_time=datetime.timedelta(hours=1), - start_time=datetime.time(hour=SHOW_UPDATE_HOUR), - thread_name='SHOWUPDATER', - prevent_cycle_run=show_queue_scheduler.action.is_show_update_running) # 3AM + search_queue_scheduler = scheduler.Scheduler( + search_queue.SearchQueue(), + cycle_time=datetime.timedelta(seconds=3), + thread_name='SEARCHQUEUE') people_queue_scheduler = scheduler.Scheduler( people_queue.PeopleQueue(), @@ -1603,21 +1595,52 @@ def init_stage_2(): thread_name='PEOPLEQUEUE' ) - # searchers - search_queue_scheduler = scheduler.Scheduler( - search_queue.SearchQueue(), + watched_state_queue_scheduler = scheduler.Scheduler( + watchedstate_queue.WatchedStateQueue(), cycle_time=datetime.timedelta(seconds=3), - thread_name='SEARCHQUEUE') + thread_name='WATCHEDSTATEQUEUE') + # / + # updaters + update_software_scheduler = scheduler.Scheduler( + version_checker.SoftwareUpdater(), + cycle_time=datetime.timedelta(hours=UPDATE_INTERVAL), + thread_name='SOFTWAREUPDATE', + silent=False) + + update_packages_scheduler = scheduler.Scheduler( + version_checker.PackagesUpdater(), + cycle_time=datetime.timedelta(hours=UPDATE_PACKAGES_INTERVAL), + # run_delay=datetime.timedelta(minutes=2), + thread_name='PACKAGESUPDATE', + silent=False) + + update_show_scheduler = scheduler.Scheduler( + show_updater.ShowUpdater(), + cycle_time=datetime.timedelta(hours=1), + start_time=datetime.time(hour=SHOW_UPDATE_HOUR), + thread_name='SHOWDATAUPDATE', + prevent_cycle_run=show_queue_scheduler.action.is_show_update_running) # 3AM + + classes.loading_msg.message = 'Loading show maps' + update_release_mappings_scheduler = scheduler.Scheduler( + scene_exceptions.ReleaseMap(), + cycle_time=datetime.timedelta(hours=2), + thread_name='SHOWMAPSUPDATE', + silent=False) + + # / + # searchers init_search_delay = int(os.environ.get('INIT_SEARCH_DELAY', 0)) # enter 4499 (was 4489) for experimental internal provider intervals update_interval = datetime.timedelta(minutes=(RECENTSEARCH_INTERVAL, 1)[4499 == RECENTSEARCH_INTERVAL]) - recent_search_scheduler = scheduler.Scheduler( + update_now = datetime.timedelta(minutes=0) + search_recent_scheduler = scheduler.Scheduler( search_recent.RecentSearcher(), cycle_time=update_interval, run_delay=update_now if RECENTSEARCH_STARTUP else datetime.timedelta(minutes=init_search_delay or 5), - thread_name='RECENTSEARCHER', + thread_name='RECENTSEARCH', prevent_cycle_run=search_queue_scheduler.action.is_recentsearch_in_progress) if [x for x in providers.sorted_sources() @@ -1635,14 +1658,13 @@ def init_stage_2(): backlogdelay = helpers.try_int((time_diff.total_seconds() / 60) + 10, 10) else: backlogdelay = 10 - backlog_search_scheduler = search_backlog.BacklogSearchScheduler( + search_backlog_scheduler = search_backlog.BacklogSearchScheduler( search_backlog.BacklogSearcher(), cycle_time=datetime.timedelta(minutes=get_backlog_cycle_time()), run_delay=datetime.timedelta(minutes=init_search_delay or backlogdelay), - thread_name='BACKLOG', + thread_name='BACKLOGSEARCH', prevent_cycle_run=search_queue_scheduler.action.is_standard_backlog_in_progress) - propers_searcher = search_propers.ProperSearcher() last_proper_search = datetime.datetime.fromtimestamp(properFinder.get_last_proper_search()) time_diff = datetime.timedelta(days=1) - (datetime.datetime.now() - last_proper_search) if time_diff < datetime.timedelta(seconds=0): @@ -1650,34 +1672,21 @@ def init_stage_2(): else: properdelay = helpers.try_int((time_diff.total_seconds() / 60) + 5, 20) - proper_finder_scheduler = scheduler.Scheduler( - propers_searcher, + search_propers_scheduler = scheduler.Scheduler( + search_propers.ProperSearcher(), cycle_time=datetime.timedelta(days=1), run_delay=datetime.timedelta(minutes=init_search_delay or properdelay), - thread_name='FINDPROPERS', + thread_name='PROPERSSEARCH', prevent_cycle_run=search_queue_scheduler.action.is_propersearch_in_progress) - # processors - media_process_scheduler = scheduler.Scheduler( - auto_post_processer.PostProcesser(), - cycle_time=datetime.timedelta(minutes=MEDIAPROCESS_INTERVAL), - thread_name='POSTPROCESSER', - silent=not PROCESS_AUTOMATICALLY) - - subtitles_finder_scheduler = scheduler.Scheduler( + search_subtitles_scheduler = scheduler.Scheduler( subtitles.SubtitlesFinder(), cycle_time=datetime.timedelta(hours=SUBTITLES_FINDER_INTERVAL), - thread_name='FINDSUBTITLES', + thread_name='SUBTITLESEARCH', silent=not USE_SUBTITLES) - background_mapping_task = threading.Thread(name='MAPPINGSUPDATER', target=indexermapper.load_mapped_ids, - kwargs={'load_all': True}) - - watched_state_queue_scheduler = scheduler.Scheduler( - watchedstate_queue.WatchedStateQueue(), - cycle_time=datetime.timedelta(seconds=3), - thread_name='WATCHEDSTATEQUEUE') - + # / + # others emby_watched_state_scheduler = scheduler.Scheduler( EmbyWatchedStateUpdater(), cycle_time=datetime.timedelta(minutes=EMBY_WATCHEDSTATE_INTERVAL), @@ -1690,6 +1699,15 @@ def init_stage_2(): run_delay=datetime.timedelta(minutes=5), thread_name='PLEXWATCHEDSTATE') + process_media_scheduler = scheduler.Scheduler( + auto_media_process.MediaProcess(), + cycle_time=datetime.timedelta(minutes=MEDIAPROCESS_INTERVAL), + thread_name='PROCESSMEDIA', + silent=not PROCESS_AUTOMATICALLY) + + background_mapping_task = threading.Thread(name='MAPPINGUPDATES', target=indexermapper.load_mapped_ids, + kwargs={'load_all': True}) + MEMCACHE['history_tab_limit'] = 11 MEMCACHE['history_tab'] = History.menu_tab(MEMCACHE['history_tab_limit']) @@ -1707,11 +1725,15 @@ def init_stage_2(): def enabled_schedulers(is_init=False): # ([], [trakt_checker_scheduler])[USE_TRAKT] + \ return ([], [events])[is_init] \ - + ([], [recent_search_scheduler, backlog_search_scheduler, show_update_scheduler, people_queue_scheduler, - update_software_scheduler, update_packages_scheduler, - show_queue_scheduler, search_queue_scheduler, proper_finder_scheduler, - media_process_scheduler, subtitles_finder_scheduler, - emby_watched_state_scheduler, plex_watched_state_scheduler, watched_state_queue_scheduler] + + ([], [update_software_scheduler, update_packages_scheduler, + update_show_scheduler, update_release_mappings_scheduler, + search_recent_scheduler, search_backlog_scheduler, + search_propers_scheduler, search_subtitles_scheduler, + show_queue_scheduler, search_queue_scheduler, + people_queue_scheduler, watched_state_queue_scheduler, + emby_watched_state_scheduler, plex_watched_state_scheduler, + process_media_scheduler + ] )[not MEMCACHE.get('update_restart')] \ + ([events], [])[is_init] diff --git a/sickgear/auto_post_processer.py b/sickgear/auto_media_process.py similarity index 73% rename from sickgear/auto_post_processer.py rename to sickgear/auto_media_process.py index 124e8b4a..fd707037 100644 --- a/sickgear/auto_post_processer.py +++ b/sickgear/auto_media_process.py @@ -18,32 +18,31 @@ import os.path import sickgear from . import logger, processTV +from .scheduler import Job -class PostProcesser(object): +class MediaProcess(Job): def __init__(self): - self.amActive = False + super(MediaProcess, self).__init__(self.job_run, kwargs={}) @staticmethod def is_enabled(): return sickgear.PROCESS_AUTOMATICALLY - def run(self): + def job_run(self): if self.is_enabled(): - self.amActive = True self._main() - self.amActive = False @staticmethod def _main(): if not os.path.isdir(sickgear.TV_DOWNLOAD_DIR): - logger.error('Automatic post-processing attempted but dir %s doesn\'t exist' % sickgear.TV_DOWNLOAD_DIR) + logger.error('Automatic media processing attempted but dir %s doesn\'t exist' % sickgear.TV_DOWNLOAD_DIR) return if not os.path.isabs(sickgear.TV_DOWNLOAD_DIR): - logger.error('Automatic post-processing attempted but dir %s is relative ' + logger.error('Automatic media processing attempted but dir %s is relative ' '(and probably not what you really want to process)' % sickgear.TV_DOWNLOAD_DIR) return - processTV.processDir(sickgear.TV_DOWNLOAD_DIR, is_basedir=True) + processTV.process_dir(sickgear.TV_DOWNLOAD_DIR, is_basedir=True) diff --git a/sickgear/config.py b/sickgear/config.py index c7adc21a..92ed1db8 100644 --- a/sickgear/config.py +++ b/sickgear/config.py @@ -152,8 +152,8 @@ def schedule_mediaprocess(iv): if sickgear.MEDIAPROCESS_INTERVAL < sickgear.MIN_MEDIAPROCESS_INTERVAL: sickgear.MEDIAPROCESS_INTERVAL = sickgear.MIN_MEDIAPROCESS_INTERVAL - sickgear.media_process_scheduler.cycle_time = datetime.timedelta(minutes=sickgear.MEDIAPROCESS_INTERVAL) - sickgear.media_process_scheduler.set_paused_state() + sickgear.process_media_scheduler.cycle_time = datetime.timedelta(minutes=sickgear.MEDIAPROCESS_INTERVAL) + sickgear.process_media_scheduler.set_paused_state() def schedule_recentsearch(iv): @@ -162,14 +162,14 @@ def schedule_recentsearch(iv): if sickgear.RECENTSEARCH_INTERVAL < sickgear.MIN_RECENTSEARCH_INTERVAL: sickgear.RECENTSEARCH_INTERVAL = sickgear.MIN_RECENTSEARCH_INTERVAL - sickgear.recent_search_scheduler.cycle_time = datetime.timedelta(minutes=sickgear.RECENTSEARCH_INTERVAL) + sickgear.search_recent_scheduler.cycle_time = datetime.timedelta(minutes=sickgear.RECENTSEARCH_INTERVAL) def schedule_backlog(iv): sickgear.BACKLOG_PERIOD = minimax(iv, sickgear.DEFAULT_BACKLOG_PERIOD, sickgear.MIN_BACKLOG_PERIOD, sickgear.MAX_BACKLOG_PERIOD) - sickgear.backlog_search_scheduler.action.cycle_time = sickgear.BACKLOG_PERIOD + sickgear.search_backlog_scheduler.action.cycle_time = sickgear.BACKLOG_PERIOD def schedule_update_software(iv): @@ -220,7 +220,7 @@ def schedule_update_packages_notify(update_packages_notify): def schedule_download_propers(download_propers): if sickgear.DOWNLOAD_PROPERS != download_propers: sickgear.DOWNLOAD_PROPERS = download_propers - sickgear.proper_finder_scheduler.set_paused_state() + sickgear.search_propers_scheduler.set_paused_state() def schedule_trakt(use_trakt): @@ -233,7 +233,7 @@ def schedule_trakt(use_trakt): def schedule_subtitles(use_subtitles): if sickgear.USE_SUBTITLES != use_subtitles: sickgear.USE_SUBTITLES = use_subtitles - sickgear.subtitles_finder_scheduler.set_paused_state() + sickgear.search_subtitles_scheduler.set_paused_state() def schedule_emby_watched(emby_watched_interval): diff --git a/sickgear/event_queue.py b/sickgear/event_queue.py index 0c894e00..1d028d84 100644 --- a/sickgear/event_queue.py +++ b/sickgear/event_queue.py @@ -1,5 +1,4 @@ -from lib.six import moves - +import queue import threading @@ -15,7 +14,7 @@ class Event(object): class Events(threading.Thread): def __init__(self, callback): super(Events, self).__init__() - self.queue = moves.queue.Queue() + self.queue = queue.Queue() self.daemon = True self.callback = callback self.name = 'EVENT-QUEUE' @@ -31,24 +30,24 @@ class Events(threading.Thread): while not self._stopper.is_set(): try: # get event type - etype = self.queue.get(True, 1) - except moves.queue.Empty: - etype = 'Empty' + ev_type = self.queue.get(True, 1) + except queue.Empty: + ev_type = 'Empty' except(BaseException, Exception): - etype = None - if etype in (self.SystemEvent.RESTART, self.SystemEvent.SHUTDOWN, None, 'Empty'): - if etype in ('Empty',): + ev_type = None + if ev_type in (self.SystemEvent.RESTART, self.SystemEvent.SHUTDOWN, None, 'Empty'): + if ev_type in ('Empty',): continue from sickgear import logger - logger.debug(f'Callback {self.callback.__name__}(event type:{etype})') + logger.debug(f'Callback {self.callback.__name__}(event type:{ev_type})') try: # perform callback if we got an event type - self.callback(etype) + self.callback(ev_type) # event completed self.queue.task_done() - except moves.queue.Empty: + except queue.Empty: pass # exiting thread diff --git a/sickgear/generic_queue.py b/sickgear/generic_queue.py index 11c77bbe..7f6b0d4d 100644 --- a/sickgear/generic_queue.py +++ b/sickgear/generic_queue.py @@ -19,6 +19,7 @@ import datetime import threading from . import db, logger +from .scheduler import Job from exceptions_helper import ex from six import integer_types @@ -37,9 +38,10 @@ class QueuePriorities(object): VERYHIGH = 40 -class GenericQueue(object): +class GenericQueue(Job): def __init__(self, cache_db_tables=None, main_db_tables=None): # type: (List[AnyStr], List[AnyStr]) -> None + super(GenericQueue, self).__init__(self.job_run, silent=True, kwargs={}, reentrant_lock=True) self.currentItem = None # type: QueueItem or None @@ -51,13 +53,41 @@ class GenericQueue(object): self.events = {} # type: Dict[int, List[Callable]] - self.lock = threading.RLock() - self.cache_db_tables = cache_db_tables or [] # type: List[AnyStr] self.main_db_tables = main_db_tables or [] # type: List[AnyStr] self._id_counter = self._load_init_id() # type: integer_types + def job_run(self): + + # only start a new task if one isn't already going + with self.lock: + if None is self.currentItem or not self.currentItem.is_alive(): + + # if the thread is dead then the current item should be finished + if self.currentItem: + self.currentItem.finish() + try: + self.delete_item(self.currentItem, finished_run=True) + except (BaseException, Exception): + pass + self.currentItem = None + + # if there's something in the queue then run it in a thread and take it out of the queue + if 0 < len(self.queue): + + self.queue.sort(key=lambda y: (-y.priority, y.added)) + if self.queue[0].priority < self.min_priority: + return + + # launch the queue item in a thread + self.currentItem = self.queue.pop(0) + if 'SEARCHQUEUE' != self.queue_name: + self.currentItem.name = self.queue_name + '-' + self.currentItem.name + self.currentItem.start() + + self.check_events() + def _load_init_id(self): # type: (...) -> integer_types """ @@ -216,7 +246,7 @@ class GenericQueue(object): self.min_priority = 999999999999 def unpause(self): - logger.log('Unpausing queue') + logger.log('Un-pausing queue') with self.lock: self.min_priority = 0 @@ -269,36 +299,6 @@ class GenericQueue(object): except (BaseException, Exception) as e: logger.error('Error executing Event: %s' % ex(e)) - def run(self): - - # only start a new task if one isn't already going - with self.lock: - if None is self.currentItem or not self.currentItem.is_alive(): - - # if the thread is dead then the current item should be finished - if self.currentItem: - self.currentItem.finish() - try: - self.delete_item(self.currentItem, finished_run=True) - except (BaseException, Exception): - pass - self.currentItem = None - - # if there's something in the queue then run it in a thread and take it out of the queue - if 0 < len(self.queue): - - self.queue.sort(key=lambda y: (-y.priority, y.added)) - if self.queue[0].priority < self.min_priority: - return - - # launch the queue item in a thread - self.currentItem = self.queue.pop(0) - if 'SEARCHQUEUE' != self.queue_name: - self.currentItem.name = self.queue_name + '-' + self.currentItem.name - self.currentItem.start() - - self.check_events() - class QueueItem(threading.Thread): def __init__(self, name, action_id=0, uid=None): diff --git a/sickgear/name_cache.py b/sickgear/name_cache.py index c7225a27..d2ebedad 100644 --- a/sickgear/name_cache.py +++ b/sickgear/name_cache.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +from collections import defaultdict import threading import sickgear @@ -21,6 +22,7 @@ from . import db from .helpers import full_sanitize_scene_name, try_int from six import iteritems +from _23 import map_consume # noinspection PyUnreachableCode if False: @@ -85,18 +87,19 @@ def build_name_cache(show_obj=None, update_only_scene=False): if show_obj: # search for only the requested show id and flush old show entries from namecache show_ids = {show_obj.tvid: [show_obj.prodid]} + nameCache = dict([(k, v) for k, v in iteritems(nameCache) if not (v[0] == show_obj.tvid and v[1] == show_obj.prodid)]) sceneNameCache = dict([(k, v) for k, v in iteritems(sceneNameCache) if not (v[0] == show_obj.tvid and v[1] == show_obj.prodid)]) # add standard indexer name to namecache - nameCache[full_sanitize_scene_name(show_obj.unique_name or show_obj.name)] = [show_obj.tvid, show_obj.prodid, -1] + nameCache[full_sanitize_scene_name(show_obj.unique_name or show_obj.name)] = \ + [show_obj.tvid, show_obj.prodid, -1] else: # generate list of production ids to look up in cache.db - show_ids = {} - for cur_show_obj in sickgear.showList: - show_ids.setdefault(cur_show_obj.tvid, []).append(cur_show_obj.prodid) + show_ids = defaultdict(list) + map_consume(lambda _so: show_ids[_so.tvid].append(_so.prodid), sickgear.showList) # add all standard show indexer names to namecache nameCache = dict( @@ -104,33 +107,32 @@ def build_name_cache(show_obj=None, update_only_scene=False): for cur_so in sickgear.showList if cur_so]) sceneNameCache = {} - cache_db = db.DBConnection() - - cache_results = [] - if update_only_scene: - # generate list of production ids to look up in cache.db - show_ids = {} - for cur_show_obj in sickgear.showList: - show_ids.setdefault(cur_show_obj.tvid, []).append(cur_show_obj.prodid) - tmp_scene_name_cache = {} - else: tmp_scene_name_cache = sceneNameCache.copy() - for t, s in iteritems(show_ids): + else: + # generate list of production ids to look up in cache.db + show_ids = defaultdict(list) + map_consume(lambda _so: show_ids[_so.tvid].append(_so.prodid), sickgear.showList) + + tmp_scene_name_cache = {} + + cache_results = [] + cache_db = db.DBConnection() + for cur_tvid, cur_prodid_list in iteritems(show_ids): cache_results += cache_db.select( - 'SELECT show_name, indexer AS tv_id, indexer_id AS prod_id, season' - ' FROM scene_exceptions' - ' WHERE indexer = %s AND indexer_id IN (%s)' % (t, ','.join(['%s' % i for i in s]))) + f'SELECT show_name, indexer AS tv_id, indexer_id AS prod_id, season' + f' FROM scene_exceptions' + f' WHERE indexer = {cur_tvid} AND indexer_id IN ({",".join(map(str, cur_prodid_list))})') if cache_results: - for cache_result in cache_results: - tvid = int(cache_result['tv_id']) - prodid = int(cache_result['prod_id']) - season = try_int(cache_result['season'], -1) - name = full_sanitize_scene_name(cache_result['show_name']) + for cur_result in cache_results: + tvid = int(cur_result['tv_id']) + prodid = int(cur_result['prod_id']) + season = try_int(cur_result['season'], -1) + name = full_sanitize_scene_name(cur_result['show_name']) tmp_scene_name_cache[name] = [tvid, prodid, season] - sceneNameCache = tmp_scene_name_cache + sceneNameCache = tmp_scene_name_cache.copy() def remove_from_namecache(tvid, prodid): diff --git a/sickgear/name_parser/parser.py b/sickgear/name_parser/parser.py index 0132056c..9c0bd936 100644 --- a/sickgear/name_parser/parser.py +++ b/sickgear/name_parser/parser.py @@ -407,7 +407,8 @@ class NameParser(object): new_season_numbers.append(s) elif show_obj.is_anime and len(best_result.ab_episode_numbers) and not self.testing: - scene_season = scene_exceptions.get_scene_exception_by_name(best_result.series_name)[2] + scene_season = scene_exceptions.get_scene_exception_by_name( + best_result.series_name)[2] for epAbsNo in best_result.ab_episode_numbers: a = epAbsNo diff --git a/sickgear/processTV.py b/sickgear/processTV.py index 16326af2..08c53072 100644 --- a/sickgear/processTV.py +++ b/sickgear/processTV.py @@ -1141,10 +1141,9 @@ class ProcessTVShow(object): self._buffer(processor.log.strip('\n')) -# backward compatibility prevents the case of this function name from being updated to PEP8 -def processDir(dir_name, nzb_name=None, process_method=None, force=False, force_replace=None, - failed=False, pp_type='auto', cleanup=False, webhandler=None, show_obj=None, is_basedir=True, - skip_failure_processing=False, client=None): +def process_dir(dir_name, nzb_name=None, process_method=None, force=False, force_replace=None, + failed=False, pp_type='auto', cleanup=False, webhandler=None, show_obj=None, is_basedir=True, + skip_failure_processing=False, client=None): """ :param dir_name: dir name @@ -1182,6 +1181,10 @@ def processDir(dir_name, nzb_name=None, process_method=None, force=False, force_ pp_type, cleanup, show_obj) +# backward compatibility +processDir = process_dir + + def process_minimal(nzb_name, show_obj, failed, webhandler): # type: (AnyStr, TVShow, bool, Any) -> None ProcessTVShow(webhandler).process_minimal(nzb_name, show_obj, failed, webhandler) diff --git a/sickgear/properFinder.py b/sickgear/properFinder.py index 1397e06a..1a7a6339 100644 --- a/sickgear/properFinder.py +++ b/sickgear/properFinder.py @@ -71,7 +71,7 @@ def search_propers(provider_proper_obj=None): if None is provider_proper_obj: _set_last_proper_search(datetime.datetime.now()) - proper_sch = sickgear.proper_finder_scheduler + proper_sch = sickgear.search_propers_scheduler if None is proper_sch.start_time: run_in = proper_sch.last_run + proper_sch.cycle_time - datetime.datetime.now() run_at = ', next check ' @@ -696,7 +696,7 @@ def _set_last_proper_search(when): def next_proper_timeleft(): - return sickgear.proper_finder_scheduler.time_left() + return sickgear.search_propers_scheduler.time_left() def get_last_proper_search(): diff --git a/sickgear/providers/generic.py b/sickgear/providers/generic.py index dab1fc5c..e5ccb797 100644 --- a/sickgear/providers/generic.py +++ b/sickgear/providers/generic.py @@ -37,7 +37,7 @@ from ..classes import NZBSearchResult, TorrentSearchResult, SearchResult from ..common import Quality, MULTI_EP_RESULT, SEASON_RESULT, USER_AGENT from ..helpers import maybe_plural, remove_file_perm from ..name_parser.parser import InvalidNameException, InvalidShowException, NameParser -from ..scene_exceptions import has_season_exceptions +from ..scene_exceptions import ReleaseMap from ..show_name_helpers import get_show_names_all_possible from ..sgdatetime import SGDatetime from ..tv import TVEpisode, TVShow @@ -1743,7 +1743,8 @@ class TorrentProvider(GenericProvider): return [] show_obj = ep_obj.show_obj - season = (-1, ep_obj.season)[has_season_exceptions(ep_obj.show_obj.tvid, ep_obj.show_obj.prodid, ep_obj.season)] + season = (-1, ep_obj.season)[ReleaseMap().has_season_exceptions( + ep_obj.show_obj.tvid, ep_obj.show_obj.prodid, ep_obj.season)] ep_dict = self._ep_dict(ep_obj) sp_detail = (show_obj.air_by_date or show_obj.is_sports) and str(ep_obj.airdate).split('-')[0] or \ (show_obj.is_anime and ep_obj.scene_absolute_number or @@ -1779,7 +1780,8 @@ class TorrentProvider(GenericProvider): return [] show_obj = ep_obj.show_obj - season = (-1, ep_obj.season)[has_season_exceptions(ep_obj.show_obj.tvid, ep_obj.show_obj.prodid, ep_obj.season)] + season = (-1, ep_obj.season)[ReleaseMap().has_season_exceptions( + ep_obj.show_obj.tvid, ep_obj.show_obj.prodid, ep_obj.season)] if show_obj.air_by_date or show_obj.is_sports: ep_detail = [str(ep_obj.airdate).replace('-', sep_date)]\ if 'date_detail' not in kwargs else kwargs['date_detail'](ep_obj.airdate) diff --git a/sickgear/providers/newznab.py b/sickgear/providers/newznab.py index b9cac5e2..244b92e2 100644 --- a/sickgear/providers/newznab.py +++ b/sickgear/providers/newznab.py @@ -34,7 +34,7 @@ from ..network_timezones import SG_TIMEZONE from ..sgdatetime import SGDatetime from ..search import get_aired_in_season, get_wanted_qualities from ..show_name_helpers import get_show_names -from ..scene_exceptions import has_season_exceptions +from ..scene_exceptions import ReleaseMap from ..tv import TVEpisode, TVShow from lib.dateutil import parser @@ -470,7 +470,7 @@ class NewznabProvider(generic.NZBProvider): # id search params = base_params.copy() use_id = False - if not has_season_exceptions(ep_obj.show_obj.tvid, ep_obj.show_obj.prodid, ep_obj.season): + if not ReleaseMap().has_season_exceptions(ep_obj.show_obj.tvid, ep_obj.show_obj.prodid, ep_obj.season): for i in sickgear.TVInfoAPI().all_sources: if i in ep_obj.show_obj.ids and 0 < ep_obj.show_obj.ids[i]['id'] and i in self.caps: params[self.caps[i]] = ep_obj.show_obj.ids[i]['id'] @@ -528,7 +528,7 @@ class NewznabProvider(generic.NZBProvider): # id search params = base_params.copy() use_id = False - if not has_season_exceptions(ep_obj.show_obj.tvid, ep_obj.show_obj.prodid, ep_obj.season): + if not ReleaseMap().has_season_exceptions(ep_obj.show_obj.tvid, ep_obj.show_obj.prodid, ep_obj.season): for i in sickgear.TVInfoAPI().all_sources: if i in ep_obj.show_obj.ids and 0 < ep_obj.show_obj.ids[i]['id'] and i in self.caps: params[self.caps[i]] = ep_obj.show_obj.ids[i]['id'] diff --git a/sickgear/scene_exceptions.py b/sickgear/scene_exceptions.py index 148285cc..f89abe76 100644 --- a/sickgear/scene_exceptions.py +++ b/sickgear/scene_exceptions.py @@ -16,12 +16,10 @@ from collections import defaultdict -import datetime import io import os import re import sys -import threading import traceback import sickgear @@ -31,6 +29,7 @@ from . import db, helpers, logger, name_cache from .anime import create_anidb_obj from .classes import OrderedDefaultdict from .indexers.indexer_config import TVINFO_TVDB +from .scheduler import Job from .sgdatetime import SGDatetime import lib.rarfile.rarfile as rarfile @@ -41,544 +40,533 @@ from six import iteritems # noinspection PyUnreachableCode if False: # noinspection PyUnresolvedReferences - from typing import AnyStr, List, Tuple, Union + from typing import AnyStr, List, Tuple, Optional, Union + from .tv import TVShow -exception_dict = {} -anidb_exception_dict = {} -xem_exception_dict = {} -xem_ids_list = defaultdict(list) - -exceptionsCache = {} -exceptionsSeasonCache = {} - -exceptionLock = threading.Lock() +MEMCACHE = {} -def should_refresh(name, max_refresh_age_secs=86400, remaining=False): - # type: (AnyStr, int, bool) -> Union[bool, int] - """ +class ReleaseMap(Job): + def __init__(self): + super(ReleaseMap, self).__init__(self.job_run, thread_lock=True, kwargs={}) - :param name: name - :param max_refresh_age_secs: - :param remaining: True to return remaining seconds - :return: - """ - my_db = db.DBConnection() - rows = my_db.select('SELECT last_refreshed FROM scene_exceptions_refresh WHERE list = ?', [name]) - if rows: - last_refresh = int(rows[0]['last_refreshed']) - if remaining: - time_left = (last_refresh + max_refresh_age_secs - SGDatetime.timestamp_near()) - return (0, time_left)[time_left > 0] - return SGDatetime.timestamp_near() > last_refresh + max_refresh_age_secs - return True + MEMCACHE.setdefault('release_map', {}) + MEMCACHE.setdefault('release_map_season', {}) + MEMCACHE.setdefault('release_map_xem', defaultdict(list)) + def job_run(self): -def set_last_refresh(name): - """ + # update xem id lists + self.fetch_xem_ids() - :param name: name - :type name: AnyStr - """ - my_db = db.DBConnection() - my_db.upsert('scene_exceptions_refresh', - {'last_refreshed': SGDatetime.timestamp_near()}, - {'list': name}) + # update release exceptions + self.fetch_exceptions() + def fetch_xem_ids(self): -def has_season_exceptions(tvid, prodid, season): - get_scene_exceptions(tvid, prodid, season=season) - if (tvid, prodid) in exceptionsCache and -1 < season and season in exceptionsCache[(tvid, prodid)]: + for cur_tvid, cur_name in iteritems(sickgear.TVInfoAPI().xem_supported_sources): + xem_ids = self._get_xem_ids(cur_name, sickgear.TVInfoAPI(cur_tvid).config['xem_origin']) + if len(xem_ids): + MEMCACHE['release_map_xem'][cur_tvid] = xem_ids + + @staticmethod + def _get_xem_ids(infosrc_name, xem_origin): + # type: (AnyStr, AnyStr) -> List + """ + + :param infosrc_name: + :param xem_origin: + """ + result = [] + + url = 'https://thexem.info/map/havemap?origin=%s' % xem_origin + + task = 'Fetching show ids with%s xem scene mapping%s for origin' + logger.log(f'{task % ("", "s")} {infosrc_name}') + parsed_json = helpers.get_url(url, parse_json=True, timeout=90) + if not isinstance(parsed_json, dict) or not parsed_json: + logger.error(f'Failed {task.lower() % ("", "s")} {infosrc_name},' + f' Unable to get URL: {url}') + else: + if 'success' == parsed_json.get('result', '') and 'data' in parsed_json: + result = list(set(filter(lambda prodid: 0 < prodid, + map(lambda pid: helpers.try_int(pid), parsed_json['data'])))) + if 0 == len(result): + logger.warning(f'Failed {task.lower() % ("", "s")} {infosrc_name},' + f' no data items parsed from URL: {url}') + + logger.log(f'Finished {task.lower() % (f" {len(result)}", helpers.maybe_plural(result))} {infosrc_name}') + return result + + def _xem_exceptions_fetcher(self): + + result = {} + + xem_list = 'xem_us' + for cur_show_obj in sickgear.showList: + if cur_show_obj.is_anime and not cur_show_obj.paused: + xem_list = 'xem' + break + + if self._should_refresh(xem_list): + for cur_tvid in [_i for _i in sickgear.TVInfoAPI().sources + if 'xem_origin' in sickgear.TVInfoAPI(_i).config]: + logger.log(f'Checking for XEM scene exception updates for {sickgear.TVInfoAPI(cur_tvid).name}') + + url = 'https://thexem.info/map/allNames?origin=%s%s&seasonNumbers=1'\ + % (sickgear.TVInfoAPI(cur_tvid).config['xem_origin'], ('&language=us', '')['xem' == xem_list]) + + parsed_json = helpers.get_url(url, parse_json=True, timeout=90) + if not parsed_json: + logger.error(f'Check scene exceptions update failed for {sickgear.TVInfoAPI(cur_tvid).name},' + f' Unable to get URL: {url}') + continue + + if 'failure' == parsed_json['result']: + continue + + for cur_prodid, cur_names in iteritems(parsed_json['data']): + try: + result[(cur_tvid, int(cur_prodid))] = cur_names + except (BaseException, Exception): + continue + + self._set_last_refresh(xem_list) + + return result + + def _anidb_exceptions_fetcher(self): + + result = {} + + if self._should_refresh('anidb'): + logger.log('Checking for AniDB scene exception updates') + for cur_show_obj in filter(lambda _s: _s.is_anime and TVINFO_TVDB == _s.tvid, sickgear.showList): + try: + anime = create_anidb_obj(name=cur_show_obj.name, tvdbid=cur_show_obj.prodid, autoCorrectName=True) + except (BaseException, Exception): + continue + if anime.name and anime.name != cur_show_obj.name: + result[(cur_show_obj.tvid, cur_show_obj.prodid)] = [{anime.name: -1}] + + self._set_last_refresh('anidb') + + return result + + def fetch_exceptions(self): + """ + Looks up release exceptions on GitHub, Xem, and Anidb, parses them into a dict, and inserts them into the + scene_exceptions table in cache.db. Finally, clears the scene name cache. + """ + def _merge_exceptions(source, dest): + for cur_ex in source: + dest[cur_ex] = source[cur_ex] + ([] if cur_ex not in dest else dest[cur_ex]) + + exceptions = self._xem_exceptions_fetcher() # XEM scene exceptions + _merge_exceptions(self._anidb_exceptions_fetcher(), exceptions) # AniDB scene exceptions + _merge_exceptions(self._github_exceptions_fetcher(), exceptions) # GitHub stored release exceptions + + exceptions_custom, count_updated_numbers, min_remain_iv = self._custom_exceptions_fetcher() + _merge_exceptions(exceptions_custom, exceptions) # Custom exceptions + + is_changed_exceptions = False + + # write all the exceptions we got off the net into the database + my_db = db.DBConnection() + cl = [] + for cur_tvid_prodid in exceptions: + + # get a list of the existing exceptions for this ID + existing_exceptions = [{_x['show_name']: _x['season']} for _x in + my_db.select('SELECT show_name, season' + ' FROM [scene_exceptions]' + ' WHERE indexer = ? AND indexer_id = ?', + list(cur_tvid_prodid))] + + # if this exception isn't already in the DB then add it + for cur_ex_dict in filter(lambda e: e not in existing_exceptions, exceptions[cur_tvid_prodid]): + try: + exception, season = next(iteritems(cur_ex_dict)) + except (BaseException, Exception): + logger.error('release exception error') + logger.error(traceback.format_exc()) + continue + + cl.append(['INSERT INTO [scene_exceptions]' + ' (indexer, indexer_id, show_name, season) VALUES (?,?,?,?)', + list(cur_tvid_prodid) + [exception, season]]) + is_changed_exceptions = True + + if cl: + my_db.mass_action(cl) + name_cache.build_name_cache(update_only_scene=True) + + # since this could invalidate the results of the cache we clear it out after updating + if is_changed_exceptions: + logger.log('Updated release exceptions') + else: + logger.log('No release exceptions update needed') + + # cleanup + exceptions.clear() + + return is_changed_exceptions, count_updated_numbers, min_remain_iv + + def _github_exceptions_fetcher(self): + """ + Looks up the exceptions on GitHub + """ + + # global exception_dict + result = {} + + # exceptions are stored on GitHub pages + for cur_tvid in sickgear.TVInfoAPI().sources: + if self._should_refresh(sickgear.TVInfoAPI(cur_tvid).name): + url = sickgear.TVInfoAPI(cur_tvid).config.get('scene_url') + if not url: + continue + + logger.log(f'Checking for release exception updates for {sickgear.TVInfoAPI(cur_tvid).name}') + + url_data = helpers.get_url(url) + if None is url_data: + # When None is urlData, trouble connecting to GitHub + logger.error(f'Check release exceptions update failed. Unable to get URL: {url}') + continue + + else: + self._set_last_refresh(sickgear.TVInfoAPI(cur_tvid).name) + + # each exception is on one line with the format indexer_id: 'show name 1', 'show name 2', etc + for cur_line in url_data.splitlines(): + prodid, sep, aliases = cur_line.partition(':') + + if not aliases: + continue + + prodid = int(prodid) + + # regex out the list of shows, taking \' into account + alias_list = [{re.sub(r'\\(.)', r'\1', _x): -1} for _x in + re.findall(r"'(.*?)(? tuple + """ + + :param data: json text + """ + # handle data + from .scene_numbering import find_scene_numbering, set_scene_numbering_helper + from .tv import TVidProdid + + result = {} + count_updated_numbers = 0 + for cur_tvid_prodid, cur_season_data in iteritems(data): + show_obj = sickgear.helpers.find_show_by_id(cur_tvid_prodid, no_mapped_ids=True) + if not show_obj: + continue + + used = set() + for cur_for_season, cur_data in iteritems(cur_season_data): + cur_for_season = helpers.try_int(cur_for_season, None) + tvid, prodid = TVidProdid(cur_tvid_prodid).tuple + if cur_data.get('n'): # alt names + result.setdefault((tvid, prodid), []) + result[(tvid, prodid)] += [{_name: cur_for_season} for _name in cur_data.get('n')] + + for cur_update in cur_data.get('se') or []: + for cur_for_episode, cur_se_range in iteritems(cur_update): # scene episode alt numbers + cur_for_episode = helpers.try_int(cur_for_episode, None) + + target_season, episode_range = cur_se_range.split('x') + scene_episodes = [int(_x) for _x in episode_range.split('-') + if None is not helpers.try_int(_x, None)] + + if 2 == len(scene_episodes): + desc = scene_episodes[0] > scene_episodes[1] + if desc: # handle a descending range case + scene_episodes.reverse() + scene_episodes = list_range(*[scene_episodes[0], scene_episodes[1] + 1]) + if desc: + scene_episodes.reverse() + + target_season = helpers.try_int(target_season, None) + for cur_target_episode in scene_episodes: + sn = find_scene_numbering(tvid, prodid, cur_for_season, cur_for_episode) + used.add((cur_for_season, cur_for_episode, target_season, cur_target_episode)) + if sn and ((cur_for_season, cur_for_episode) + sn) not in used \ + and (cur_for_season, cur_for_episode) not in used: + logger.debug(f'Skipped setting "{show_obj.unique_name}"' + f' episode {cur_for_season}x{cur_for_episode}' + f' to target a release {target_season}x{cur_target_episode}' + f' because set to {sn[0]}x{sn[1]}') + else: + used.add((cur_for_season, cur_for_episode)) + if not sn or sn != (target_season, cur_target_episode): # not already set + result = set_scene_numbering_helper( + tvid, prodid, for_season=cur_for_season, for_episode=cur_for_episode, + scene_season=target_season, scene_episode=cur_target_episode) + if result.get('success'): + count_updated_numbers += 1 + + cur_for_episode += 1 + + return result, count_updated_numbers + + @staticmethod + def _should_refresh(name, max_refresh_age_secs=86400, remaining=False): + # type: (AnyStr, int, bool) -> Union[bool, int] + """ + + :param name: name + :param max_refresh_age_secs: + :param remaining: True to return remaining seconds + :return: + """ + my_db = db.DBConnection() + rows = my_db.select('SELECT last_refreshed FROM [scene_exceptions_refresh] WHERE list = ?', [name]) + if rows: + last_refresh = int(rows[0]['last_refreshed']) + if remaining: + time_left = (last_refresh + max_refresh_age_secs - SGDatetime.timestamp_near()) + return (0, time_left)[time_left > 0] + return SGDatetime.timestamp_near() > last_refresh + max_refresh_age_secs return True - return False + @staticmethod + def _set_last_refresh(name): + # type: (AnyStr) -> None + """ -def get_scene_exceptions(tvid, prodid, season=-1): - """ - Given a indexer_id, return a list of all the scene exceptions. - :param tvid: tvid - :type tvid: int - :param prodid: prodid - :type prodid: int or long - :param season: season number - :type season: int - :return: - :rtype: List - """ - global exceptionsCache - exceptions_list = [] - - if (tvid, prodid) not in exceptionsCache or season not in exceptionsCache[(tvid, prodid)]: + :param name: name + :type name: AnyStr + """ my_db = db.DBConnection() - exceptions = my_db.select('SELECT show_name' - ' FROM scene_exceptions' + my_db.upsert('scene_exceptions_refresh', + {'last_refreshed': SGDatetime.timestamp_near()}, + {'list': name}) + + @staticmethod + def update_exceptions(show_obj, release_exceptions): + # type: (TVShow, list) -> None + """ + Given a show object and a list of alternative names, + update MEMCACHE['release_map'], the db, and rebuild name_cache. + """ + logger.log(f'Updating release exceptions for {show_obj.unique_name or show_obj.name}') + + my_db = db.DBConnection() + my_db.action('DELETE FROM [scene_exceptions]' + ' WHERE indexer = ? AND indexer_id = ?', + [show_obj.tvid, show_obj.prodid]) + + # A change has been made to the scene exception list. Clear the cache, to make this visible + MEMCACHE['release_map'][(show_obj.tvid, show_obj.prodid)] = defaultdict(list) + + for cur_ex in release_exceptions: + + season, alt_name = cur_ex.split('|', 1) + try: + season = int(season) + except (BaseException, Exception): + logger.error(f'invalid season for release exception: {show_obj.tvid_prodid} - {season}:{alt_name}') + continue + + MEMCACHE['release_map'][(show_obj.tvid, show_obj.prodid)][season].append(alt_name) + + my_db.action('INSERT INTO [scene_exceptions]' + ' (indexer, indexer_id, show_name, season) VALUES (?,?,?,?)', + [show_obj.tvid, show_obj.prodid, alt_name, season]) + + sickgear.name_cache.build_name_cache(update_only_scene=True) + + def has_season_exceptions(self, tvid, prodid, season): + # type: (int, int, int) -> bool + + self.get_alt_names(tvid, prodid, season) + return (-1 < season) and season in MEMCACHE['release_map'].get((tvid, prodid), {}) + + def get_alt_names(self, tvid, prodid, season=-1): + # type: (int, int, Optional[int]) -> List + """ + Return a list and update MEMCACHE['release_map'] of alternative show names from db + for all seasons, or a specific show season. + + :param tvid: show tvid + :param prodid: show prodid + :param season: optional season number + """ + alt_names = MEMCACHE['release_map'].get((tvid, prodid), {}).get(season, []) + + if not alt_names: + my_db = db.DBConnection() + exceptions = my_db.select('SELECT show_name' + ' FROM [scene_exceptions]' + ' WHERE indexer = ? AND indexer_id = ?' + ' AND season = ?', + [tvid, prodid, season]) + if exceptions: + alt_names = list(set([_ex['show_name'] for _ex in exceptions])) + + if (tvid, prodid) not in MEMCACHE['release_map']: + MEMCACHE['release_map'][(tvid, prodid)] = {} + MEMCACHE['release_map'][(tvid, prodid)][season] = alt_names + + if 1 == season: # if we were looking for season 1 we can add generic names + alt_names += self.get_alt_names(tvid, prodid) + + return alt_names + + @staticmethod + def get_show_exceptions(tvid_prodid): + # type: (AnyStr) -> OrderedDefaultdict + """ + return a scene exceptions dict for a show + + :param tvid_prodid: a show tvid:prodid + """ + exceptions_dict = OrderedDefaultdict(list) + + from .tv import TVidProdid + + my_db = db.DBConnection() + exceptions = my_db.select('SELECT show_name, season' + ' FROM [scene_exceptions]' ' WHERE indexer = ? AND indexer_id = ?' - ' AND season = ?', - [tvid, prodid, season]) + ' ORDER BY season DESC, show_name DESC', + TVidProdid(tvid_prodid).list) + + exceptions_seasons = [] if exceptions: - exceptions_list = list(set([cur_exception['show_name'] for cur_exception in exceptions])) + for cur_ex in exceptions: + # order as, s*, and then season desc, show_name also desc (so years in names fall the newest on top) + if -1 == cur_ex['season']: + exceptions_dict[-1].append(cur_ex['show_name']) + else: + exceptions_seasons += [cur_ex] - if (tvid, prodid) not in exceptionsCache: - exceptionsCache[(tvid, prodid)] = {} - exceptionsCache[(tvid, prodid)][season] = exceptions_list - else: - exceptions_list = exceptionsCache[(tvid, prodid)][season] + for cur_ex in exceptions_seasons: + exceptions_dict[cur_ex['season']].append(cur_ex['show_name']) - if 1 == season: # if we where looking for season 1 we can add generic names - exceptions_list += get_scene_exceptions(tvid, prodid, season=-1) + return exceptions_dict - return exceptions_list + @staticmethod + def get_exception_seasons(tvid, prodid): + # type: (int, int) -> List[int] + """ + return a list of season numbers that have alternative names + :param tvid: show tvid + :param prodid: show prodid + """ + exception_seasons = MEMCACHE['release_map_season'].get((tvid, prodid), []) + if not exception_seasons: + my_db = db.DBConnection() + sql_result = my_db.select('SELECT DISTINCT(season) AS season' + ' FROM [scene_exceptions]' + ' WHERE indexer = ? AND indexer_id = ?', + [tvid, prodid]) + if sql_result: + exception_seasons = list(set([int(_x['season']) for _x in sql_result])) -def get_all_scene_exceptions(tvid_prodid): - """ + if (tvid, prodid) not in MEMCACHE['release_map_season']: + MEMCACHE['release_map_season'][(tvid, prodid)] = {} - :param tvid_prodid: - :type tvid_prodid: AnyStr - :return: - :rtype: OrderedDefaultdict - """ - exceptions_dict = OrderedDefaultdict(list) + MEMCACHE['release_map_season'][(tvid, prodid)] = exception_seasons - from sickgear.tv import TVidProdid - - my_db = db.DBConnection() - exceptions = my_db.select('SELECT show_name,season' - ' FROM scene_exceptions' - ' WHERE indexer = ? AND indexer_id = ?' - ' ORDER BY season DESC, show_name DESC', - TVidProdid(tvid_prodid).list) - - exceptions_seasons = [] - if exceptions: - for cur_exception in exceptions: - # order as, s*, and then season desc, show_name also desc (so years in names may fall newest on top) - if -1 == cur_exception['season']: - exceptions_dict[cur_exception['season']].append(cur_exception['show_name']) - else: - exceptions_seasons += [cur_exception] - - for cur_exception in exceptions_seasons: - exceptions_dict[cur_exception['season']].append(cur_exception['show_name']) - - return exceptions_dict - - -def get_scene_seasons(tvid, prodid): - """ - return a list of season numbers that have scene exceptions - :param tvid: tvid - :type tvid: int - :param prodid: prodid - :type prodid: int or long - :return: - :rtype: List - """ - global exceptionsSeasonCache - exception_season_list = [] - - if (tvid, prodid) not in exceptionsSeasonCache: - my_db = db.DBConnection() - sql_result = my_db.select('SELECT DISTINCT(season) AS season' - ' FROM scene_exceptions' - ' WHERE indexer = ? AND indexer_id = ?', - [tvid, prodid]) - if sql_result: - exception_season_list = list(set([int(x['season']) for x in sql_result])) - - if (tvid, prodid) not in exceptionsSeasonCache: - exceptionsSeasonCache[(tvid, prodid)] = {} - - exceptionsSeasonCache[(tvid, prodid)] = exception_season_list - else: - exception_season_list = exceptionsSeasonCache[(tvid, prodid)] - - return exception_season_list + return exception_seasons def get_scene_exception_by_name(show_name): + # type: (AnyStr) -> List[None, None, None] or List[int, int, int] """ :param show_name: show name - :type show_name: AnyStr - :return: - :rtype: Tuple[None, None, None] or Tuple[int, int or long, int] """ - return get_scene_exception_by_name_multiple(show_name)[0] + return _get_scene_exception_by_name_multiple(show_name)[0] -def get_scene_exception_by_name_multiple(show_name): +def _get_scene_exception_by_name_multiple(show_name): + # type: (AnyStr) -> List[List[None, None, None] or List[int, int, int]] """ :param show_name: show name - :type show_name: AnyStr :return: (tvid, prodid, season) of the exception, None if no exception is present. - :rtype: Tuple[None, None, None] or Tuple[int, int or long, int] """ try: exception_result = name_cache.sceneNameCache[helpers.full_sanitize_scene_name(show_name)] - return [exception_result] except (BaseException, Exception): return [[None, None, None]] - -def retrieve_exceptions(): - """ - Looks up the exceptions on github, parses them into a dict, and inserts them into the - scene_exceptions table in cache.db. Also clears the scene name cache. - """ - global exception_dict, anidb_exception_dict, xem_exception_dict - - # exceptions are stored on GitHub pages - for tvid in sickgear.TVInfoAPI().sources: - if should_refresh(sickgear.TVInfoAPI(tvid).name): - logger.log(f'Checking for scene exception updates for {sickgear.TVInfoAPI(tvid).name}') - - url = sickgear.TVInfoAPI(tvid).config.get('scene_url') - if not url: - continue - - url_data = helpers.get_url(url) - if None is url_data: - # When None is urlData, trouble connecting to github - logger.error(f'Check scene exceptions update failed. Unable to get URL: {url}') - continue - - else: - set_last_refresh(sickgear.TVInfoAPI(tvid).name) - - # each exception is on one line with the format indexer_id: 'show name 1', 'show name 2', etc - for cur_line in url_data.splitlines(): - cur_line = cur_line - prodid, sep, aliases = cur_line.partition(':') - - if not aliases: - continue - - prodid = int(prodid) - - # regex out the list of shows, taking \' into account - # alias_list = [re.sub(r'\\(.)', r'\1', x) for x in re.findall(r"'(.*?)(? scene_episodes[1] - if desc: # handle a descending range case - scene_episodes.reverse() - scene_episodes = list_range(*[scene_episodes[0], scene_episodes[1] + 1]) - if desc: - scene_episodes.reverse() - - target_season = helpers.try_int(target_season, None) - for target_episode in scene_episodes: - sn = find_scene_numbering(tvid, prodid, for_season, for_episode) - used.add((for_season, for_episode, target_season, target_episode)) - if sn and ((for_season, for_episode) + sn) not in used \ - and (for_season, for_episode) not in used: - logger.debug(f'Skipped setting "{show_obj.unique_name}" episode {for_season}x{for_episode}' - f' to target a release {target_season}x{target_episode}' - f' because set to {sn[0]}x{sn[1]}') - else: - used.add((for_season, for_episode)) - if not sn or sn != (target_season, target_episode): # not already set - result = set_scene_numbering_helper( - tvid, prodid, for_season=for_season, for_episode=for_episode, - scene_season=target_season, scene_episode=target_episode) - if result.get('success'): - cnt_updated_numbers += 1 - - for_episode = for_episode + 1 - - return custom_exception_dict, cnt_updated_numbers, should_refresh(src_id, iv, remaining=True) - - -def _anidb_exceptions_fetcher(): - global anidb_exception_dict - - if should_refresh('anidb'): - logger.log('Checking for AniDB scene exception updates') - for cur_show_obj in filter(lambda _s: _s.is_anime and TVINFO_TVDB == _s.tvid, sickgear.showList): - try: - anime = create_anidb_obj(name=cur_show_obj.name, tvdbid=cur_show_obj.prodid, autoCorrectName=True) - except (BaseException, Exception): - continue - if anime.name and anime.name != cur_show_obj.name: - anidb_exception_dict[(cur_show_obj.tvid, cur_show_obj.prodid)] = [{anime.name: -1}] - - set_last_refresh('anidb') - return anidb_exception_dict - - -def _xem_exceptions_fetcher(): - global xem_exception_dict - - xem_list = 'xem_us' - for cur_show_obj in sickgear.showList: - if cur_show_obj.is_anime and not cur_show_obj.paused: - xem_list = 'xem' - break - - if should_refresh(xem_list): - for tvid in [i for i in sickgear.TVInfoAPI().sources if 'xem_origin' in sickgear.TVInfoAPI(i).config]: - logger.log(f'Checking for XEM scene exception updates for {sickgear.TVInfoAPI(tvid).name}') - - url = 'https://thexem.info/map/allNames?origin=%s%s&seasonNumbers=1'\ - % (sickgear.TVInfoAPI(tvid).config['xem_origin'], ('&language=us', '')['xem' == xem_list]) - - parsed_json = helpers.get_url(url, parse_json=True, timeout=90) - if not parsed_json: - logger.error(f'Check scene exceptions update failed for {sickgear.TVInfoAPI(tvid).name},' - f' Unable to get URL: {url}') - continue - - if 'failure' == parsed_json['result']: - continue - - for prodid, names in iteritems(parsed_json['data']): - try: - xem_exception_dict[(tvid, int(prodid))] = names - except (BaseException, Exception): - continue - - set_last_refresh(xem_list) - - return xem_exception_dict - - -def _xem_get_ids(infosrc_name, xem_origin): - """ - - :param infosrc_name: - :type infosrc_name: AnyStr - :param xem_origin: - :type xem_origin: AnyStr - :return: - :rtype: List - """ - xem_ids = [] - - url = 'https://thexem.info/map/havemap?origin=%s' % xem_origin - - task = 'Fetching show ids with%s xem scene mapping%s for origin' - logger.log(f'{task % ("", "s")} {infosrc_name}') - parsed_json = helpers.get_url(url, parse_json=True, timeout=90) - if not isinstance(parsed_json, dict) or not parsed_json: - logger.error(f'Failed {task.lower() % ("", "s")} {infosrc_name},' - f' Unable to get URL: {url}') - else: - if 'success' == parsed_json.get('result', '') and 'data' in parsed_json: - xem_ids = list(set(filter(lambda prodid: 0 < prodid, - map(lambda pid: helpers.try_int(pid), parsed_json['data'])))) - if 0 == len(xem_ids): - logger.warning(f'Failed {task.lower() % ("", "s")} {infosrc_name},' - f' no data items parsed from URL: {url}') - - logger.log(f'Finished {task.lower() % (f" {len(xem_ids)}", helpers.maybe_plural(xem_ids))} {infosrc_name}') - return xem_ids - - -def get_xem_ids(): - global xem_ids_list - - for tvid, name in iteritems(sickgear.TVInfoAPI().xem_supported_sources): - xem_ids = _xem_get_ids(name, sickgear.TVInfoAPI(tvid).config['xem_origin']) - if len(xem_ids): - xem_ids_list[tvid] = xem_ids + return [exception_result] def has_abs_episodes(ep_obj=None, name=None): + # type: (Optional[sickgear.tv.TVEpisode], Optional[AnyStr]) -> bool """ :param ep_obj: episode object - :type ep_obj: sickgear.tv.TVEpisode or None :param name: name - :type name: AnyStr - :return: - :rtype: bool """ - return any((name or ep_obj.show_obj.name or '').lower().startswith(x.lower()) for x in [ + return any((name or ep_obj.show_obj.name or '').lower().startswith(_x.lower()) for _x in [ 'The Eighties', 'The Making of the Mob', 'The Night Of', 'Roots 2016', 'Trepalium' ]) diff --git a/sickgear/scene_numbering.py b/sickgear/scene_numbering.py index 2afc2914..b2e01223 100644 --- a/sickgear/scene_numbering.py +++ b/sickgear/scene_numbering.py @@ -19,17 +19,14 @@ # @copyright: Dermot Buckley # -import datetime import traceback from sqlite3 import Row from exceptions_helper import ex - import sickgear from . import db, logger from .helpers import try_int -from .scene_exceptions import xem_ids_list from .sgdatetime import SGDatetime # noinspection PyUnreachableCode @@ -774,7 +771,8 @@ def xem_refresh(tvid, prodid, force=False): tvid, prodid = int(tvid), int(prodid) tvinfo = sickgear.TVInfoAPI(tvid) - if 'xem_origin' not in tvinfo.config or prodid not in xem_ids_list.get(tvid, []): + if 'xem_origin' not in tvinfo.config \ + or prodid not in sickgear.scene_exceptions.MEMCACHE['release_map_xem'].get(tvid, []): return xem_origin = tvinfo.config['xem_origin'] diff --git a/sickgear/scheduler.py b/sickgear/scheduler.py index 88bc2976..130e5771 100644 --- a/sickgear/scheduler.py +++ b/sickgear/scheduler.py @@ -24,10 +24,12 @@ import traceback from . import logger from exceptions_helper import ex +import sickgear + class Scheduler(threading.Thread): def __init__(self, action, cycle_time=datetime.timedelta(minutes=10), run_delay=datetime.timedelta(minutes=0), - start_time=None, thread_name="ScheduledThread", silent=True, prevent_cycle_run=None, paused=False): + start_time=None, thread_name='ScheduledThread', silent=True, prevent_cycle_run=None, paused=False): super(Scheduler, self).__init__() self.last_run = datetime.datetime.now() + run_delay - cycle_time @@ -41,10 +43,18 @@ class Scheduler(threading.Thread): self._stopper = threading.Event() self._unpause = threading.Event() if not paused: - self._unpause.set() + self.unpause() self.lock = threading.Lock() self.force = False + @property + def is_running_job(self): + # type: (...) -> bool + """ + Return running state of the scheduled action + """ + return self.action.amActive + def pause(self): self._unpause.clear() @@ -69,7 +79,7 @@ class Scheduler(threading.Thread): return self.cycle_time - (datetime.datetime.now() - self.last_run) def force_run(self): - if not self.action.amActive: + if not self.is_running_job: self.force = True return True return False @@ -139,3 +149,72 @@ class Scheduler(threading.Thread): # exiting thread self._stopper.clear() self._unpause.clear() + + @staticmethod + def blocking_jobs(): + # type (...) -> bool + """ + Return description of running jobs, or False if none are running + + These jobs should prevent a restart/shutdown while running. + """ + + job_report = [] + if sickgear.process_media_scheduler.is_running_job: + job_report.append('Media processing') + + if sickgear.update_show_scheduler.is_running_job: + job_report.append(f'{("U", "u")[len(job_report)]}pdating shows data') + + # this idea is not ready for production. issues identified... + # 1. many are just the queue filling process, so this doesn't actually prevents restart/shutdown during those + # 2. in case something goes wrong or there is a bad bug in ithe code, the restart would be prevented + # (meaning no auto- / manual update via ui, user is forced to kill the process, manually update and restart) + # 3. just by bad timing the autoupdate process maybe at the same time as another blocking thread = updates but + # never restarts + # therefore, with these issues, the next two lines cannot allow this feature to be brought into service :( + + # if len(job_report): + # return '%s %s running' % (' and '.join(job_report), ('are', 'is')[1 == len(job_report)]) + + return False + + +class Job(object): + """ + The job class centralises tasks with states + """ + def __init__(self, func, silent=False, thread_lock=False, reentrant_lock=False, args=(), kwargs=None): + + self.amActive = False + + self._func = func + self._silent = silent + self._args = args + self._kwargs = (kwargs, {})[None is kwargs] + + if thread_lock: + self.lock = threading.Lock() + elif reentrant_lock: + self.lock = threading.RLock() + + def run(self): + + if self.amActive and self.__class__.__name__ in ('BacklogSearcher', 'MediaProcess'): + logger.log(u'%s is still running, not starting it again' % self.__class__.__name__) + return + + if self._func: + result, re_raise = None, False + try: + self.amActive = True + result = self._func(*self._args, **self._kwargs) + except(BaseException, Exception) as e: + re_raise = e + finally: + self.amActive = False + not self._silent and logger.log(u'%s(%s) completed' % (self.__class__.__name__, self._func.__name__)) + if re_raise: + raise re_raise + + return result diff --git a/sickgear/search_backlog.py b/sickgear/search_backlog.py index ecd9b369..1181581b 100644 --- a/sickgear/search_backlog.py +++ b/sickgear/search_backlog.py @@ -17,13 +17,13 @@ from __future__ import with_statement, division import datetime -import threading from math import ceil import sickgear from . import db, logger, scheduler, search_queue, ui from .helpers import find_show_by_id from .providers.generic import GenericProvider +from .scheduler import Job from .search import wanted_episodes from .sgdatetime import SGDatetime from .tv import TVidProdid, TVEpisode, TVShow @@ -74,13 +74,12 @@ class BacklogSearchScheduler(scheduler.Scheduler): return self.action.nextBacklog - now if self.action.nextBacklog > now else datetime.timedelta(seconds=0) -class BacklogSearcher(object): +class BacklogSearcher(Job): def __init__(self): + super(BacklogSearcher, self).__init__(self.job_run, kwargs={}, thread_lock=True) self.last_backlog = self._get_last_backlog() self.cycle_time = sickgear.BACKLOG_PERIOD - self.lock = threading.Lock() - self.amActive = False # type: bool self.amPaused = False # type: bool self.amWaiting = False # type: bool self.forcetype = NORMAL_BACKLOG # type: int @@ -196,9 +195,6 @@ class BacklogSearcher(object): :return: nothing :rtype: None """ - if self.amActive and not which_shows: - logger.debug('Backlog is still running, not starting it again') - return if which_shows: show_list = which_shows @@ -225,7 +221,6 @@ class BacklogSearcher(object): return self._get_last_backlog() - self.amActive = True self.amPaused = False cur_date = datetime.date.today().toordinal() @@ -328,7 +323,6 @@ class BacklogSearcher(object): if standard_backlog and not any_torrent_enabled: self._set_last_runtime(now) - self.amActive = False self._reset_progress_indicator() @staticmethod @@ -401,7 +395,7 @@ class BacklogSearcher(object): # noinspection SqlConstantCondition my_db.action('UPDATE info SET last_backlog=%s WHERE 1=1' % when) - def run(self): + def job_run(self): try: force_type = self.forcetype force = self.force @@ -409,5 +403,4 @@ class BacklogSearcher(object): self.force = False self.search_backlog(force_type=force_type, force=force) except (BaseException, Exception): - self.amActive = False raise diff --git a/sickgear/search_propers.py b/sickgear/search_propers.py index 602c5125..87c7ced9 100644 --- a/sickgear/search_propers.py +++ b/sickgear/search_propers.py @@ -16,24 +16,21 @@ from __future__ import with_statement -import threading import sickgear +from .scheduler import Job -class ProperSearcher(object): +class ProperSearcher(Job): def __init__(self): - self.lock = threading.Lock() - self.amActive = False + super(ProperSearcher, self).__init__(self.job_run, kwargs={}, thread_lock=True) @staticmethod def is_enabled(): # type: (...) -> bool return sickgear.DOWNLOAD_PROPERS - def run(self): - self.amActive = True + @staticmethod + def job_run(): propersearch_queue_item = sickgear.search_queue.ProperSearchQueueItem() sickgear.search_queue_scheduler.action.add_item(propersearch_queue_item) - - self.amActive = False diff --git a/sickgear/search_recent.py b/sickgear/search_recent.py index f559178f..5ad92515 100644 --- a/sickgear/search_recent.py +++ b/sickgear/search_recent.py @@ -16,19 +16,15 @@ from __future__ import with_statement -import threading import sickgear +from .scheduler import Job -class RecentSearcher(object): +class RecentSearcher(Job): def __init__(self): - self.lock = threading.Lock() - self.amActive = False + super(RecentSearcher, self).__init__(self.job_run, kwargs={}, thread_lock=True) - def run(self): - self.amActive = True + def job_run(self): recentsearch_queue_item = sickgear.search_queue.RecentSearchQueueItem() sickgear.search_queue_scheduler.action.add_item(recentsearch_queue_item) - - self.amActive = False diff --git a/sickgear/show_name_helpers.py b/sickgear/show_name_helpers.py index f688c1d5..926e9e5d 100644 --- a/sickgear/show_name_helpers.py +++ b/sickgear/show_name_helpers.py @@ -25,7 +25,7 @@ import sickgear from . import common, db, logger from .helpers import sanitize_scene_name from .name_parser.parser import InvalidNameException, InvalidShowException, NameParser -from .scene_exceptions import get_scene_exceptions +from .scene_exceptions import ReleaseMap from sg_helpers import scantree from _23 import quote_plus @@ -384,10 +384,10 @@ def all_possible_show_names(show_obj, season=-1, force_anime=False): :return: a list of all the possible show names """ - show_names = get_scene_exceptions(show_obj.tvid, show_obj.prodid, season=season)[:] - if not show_names: # if we don't have any season specific exceptions fallback to generic exceptions + show_names = ReleaseMap().get_alt_names(show_obj.tvid, show_obj.prodid, season)[:] + if -1 != season and not show_names: # fallback to generic exceptions if no season specific exceptions season = -1 - show_names = get_scene_exceptions(show_obj.tvid, show_obj.prodid, season=season)[:] + show_names = ReleaseMap().get_alt_names(show_obj.tvid, show_obj.prodid)[:] if -1 == season: show_names.append(show_obj.name) diff --git a/sickgear/show_queue.py b/sickgear/show_queue.py index e083bb40..8f90d4d2 100644 --- a/sickgear/show_queue.py +++ b/sickgear/show_queue.py @@ -70,7 +70,7 @@ class ShowQueue(generic_queue.GenericQueue): def check_events(self): if self.daily_update_running and \ - not (self.is_show_update_running() or sickgear.show_update_scheduler.action.amActive): + not (self.is_show_update_running() or sickgear.update_show_scheduler.is_running_job): self.execute_events(DAILY_SHOW_UPDATE_FINISHED_EVENT) self.daily_update_running = False @@ -1139,7 +1139,7 @@ class QueueItemAdd(ShowQueueItem): self.show_obj.tvid, self.show_obj.prodid) # if "scene" numbering is disabled during add show, output availability to log if None is not self.scene and not self.show_obj.scene and \ - self.show_obj.prodid in sickgear.scene_exceptions.xem_ids_list[self.show_obj.tvid]: + self.show_obj.prodid in sickgear.scene_exceptions.MEMCACHE['release_map_xem'][self.show_obj.tvid]: logger.log('No scene number mappings found at TheXEM. Therefore, episode scene numbering disabled, ' 'edit show and enable it to manually add custom numbers for search and media processing.') try: @@ -1179,7 +1179,7 @@ class QueueItemAdd(ShowQueueItem): # if started with WANTED eps then run the backlog if WANTED == self.default_status or items_wanted: logger.log('Launching backlog for this show since episodes are WANTED') - sickgear.backlog_search_scheduler.action.search_backlog([self.show_obj], prevent_same=True) + sickgear.search_backlog_scheduler.action.search_backlog([self.show_obj], prevent_same=True) ui.notifications.message('Show added/search', 'Adding and searching for episodes of' + msg) else: ui.notifications.message('Show added', 'Adding' + msg) @@ -1253,7 +1253,7 @@ class QueueItemRefresh(ShowQueueItem): self.show_obj.populate_cache(self.force_image_cache) # Load XEM data to DB for show - if self.show_obj.prodid in sickgear.scene_exceptions.xem_ids_list[self.show_obj.tvid]: + if self.show_obj.prodid in sickgear.scene_exceptions.MEMCACHE['release_map_xem'][self.show_obj.tvid]: sickgear.scene_numbering.xem_refresh(self.show_obj.tvid, self.show_obj.prodid) if 'pausestatus_after' in self.kwargs and None is not self.kwargs['pausestatus_after']: diff --git a/sickgear/show_updater.py b/sickgear/show_updater.py index 144398a7..abf20fe6 100644 --- a/sickgear/show_updater.py +++ b/sickgear/show_updater.py @@ -23,6 +23,7 @@ from exceptions_helper import ex import sickgear from . import db, logger, network_timezones, properFinder, ui +from .scheduler import Job # noinspection PyUnreachableCode if False: @@ -54,13 +55,12 @@ def clean_ignore_require_words(): pass -class ShowUpdater(object): +class ShowUpdater(Job): def __init__(self): - self.amActive = False + super(ShowUpdater, self).__init__(self.job_run, kwargs={}) - def run(self): - - self.amActive = True + @staticmethod + def job_run(): try: update_datetime = datetime.datetime.now() @@ -89,14 +89,14 @@ class ShowUpdater(object): # update xem id lists try: - sickgear.scene_exceptions.get_xem_ids() + sickgear.scene_exceptions.ReleaseMap().fetch_xem_ids() except (BaseException, Exception): logger.error('xem id list update error') logger.error(traceback.format_exc()) # update scene exceptions try: - sickgear.scene_exceptions.retrieve_exceptions() + sickgear.scene_exceptions.ReleaseMap().fetch_exceptions() except (BaseException, Exception): logger.error('scene exceptions update error') logger.error(traceback.format_exc()) @@ -147,7 +147,7 @@ class ShowUpdater(object): import threading try: sickgear.background_mapping_task = threading.Thread( - name='MAPPINGSUPDATER', target=sickgear.indexermapper.load_mapped_ids, kwargs={'update': True}) + name='MAPPINGUPDATES', target=sickgear.indexermapper.load_mapped_ids, kwargs={'update': True}) sickgear.background_mapping_task.start() except (BaseException, Exception): logger.error('missing mapped ids update error') @@ -224,8 +224,8 @@ class ShowUpdater(object): logger.log('Added all shows to show queue for full update') - finally: - self.amActive = False + except(BaseException, Exception): + pass def __del__(self): pass diff --git a/sickgear/subtitles.py b/sickgear/subtitles.py index c8cda3a0..28570bdc 100644 --- a/sickgear/subtitles.py +++ b/sickgear/subtitles.py @@ -19,6 +19,7 @@ import datetime from . import db, helpers, logger from .common import * +from .scheduler import Job import sickgear @@ -103,24 +104,22 @@ def subtitle_language_filter(): return [language for language in subliminal.language.LANGUAGES if language[2] != ""] -class SubtitlesFinder(object): +class SubtitlesFinder(Job): """ The SubtitlesFinder will be executed every hour but will not necessarily search and download subtitles. Only if the defined rule is true """ def __init__(self): - self.amActive = False + super(SubtitlesFinder, self).__init__(self.job_run, kwargs={}, thread_lock=True) @staticmethod def is_enabled(): return sickgear.USE_SUBTITLES - def run(self): + def job_run(self): if self.is_enabled(): - self.amActive = True self._main() - self.amActive = False def _main(self): if 1 > len(sickgear.subtitles.get_enabled_service_list()): diff --git a/sickgear/version_checker.py b/sickgear/version_checker.py index aee6ccd1..3f0ea6df 100644 --- a/sickgear/version_checker.py +++ b/sickgear/version_checker.py @@ -29,6 +29,7 @@ from exceptions_helper import ex import sickgear from . import logger, notifiers, ui +from .scheduler import (Scheduler, Job) from .piper import check_pip_outdated from sg_helpers import cmdline_runner, get_url @@ -41,12 +42,14 @@ if False: from typing import Tuple -class PackagesUpdater(object): +class PackagesUpdater(Job): def __init__(self): + super(PackagesUpdater, self).__init__(self.job_run, kwargs={}) + self.install_type = 'Python package updates' - def run(self, force=False): + def job_run(self, force=False): if not sickgear.EXT_UPDATES \ and self.check_for_new_version(force) \ and sickgear.UPDATE_PACKAGES_AUTO: @@ -64,6 +67,11 @@ class PackagesUpdater(object): :returns: True when package install/updates are available :rtype: bool """ + response = Scheduler.blocking_jobs() + if response: + logger.log(f'Update skipped because {response}', logger.DEBUG) + return False + if force and not sickgear.UPDATE_PACKAGES_MENU: logger.log('Checking not enabled from menu action for %s' % self.install_type) return False @@ -100,12 +108,14 @@ class PackagesUpdater(object): return True -class SoftwareUpdater(object): +class SoftwareUpdater(Job): """ Version check class meant to run as a thread object with the sg scheduler. """ def __init__(self): + super(SoftwareUpdater, self).__init__(self.job_run, kwargs={}) + self._min_python = (100, 0) # set default to absurdly high to prevent update self.install_type = self.find_install_type() @@ -150,7 +160,7 @@ class SoftwareUpdater(object): except (BaseException, Exception): pass - def run(self, force=False): + def job_run(self, force=False): # set current branch version sickgear.BRANCH = self.get_branch() @@ -219,6 +229,11 @@ class SoftwareUpdater(object): # update branch with current config branch value self.updater.branch = sickgear.BRANCH + response = Scheduler.blocking_jobs() + if response: + logger.log(f'Update skipped because {response}', logger.DEBUG) + return False + if not self.is_updatable: self._log_cannot_update() return False diff --git a/sickgear/watchedstate.py b/sickgear/watchedstate.py index 14454cbc..5c9fcc88 100644 --- a/sickgear/watchedstate.py +++ b/sickgear/watchedstate.py @@ -14,17 +14,15 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -import threading - import sickgear +from .scheduler import Job from sickgear import watchedstate_queue -class WatchedStateUpdater(object): +class WatchedStateUpdater(Job): def __init__(self, name, queue_item): + super(WatchedStateUpdater, self).__init__(self.job_run, silent=True, kwargs={}, thread_lock=True) - self.amActive = False - self.lock = threading.Lock() self.name = name self.queue_item = queue_item @@ -32,13 +30,15 @@ class WatchedStateUpdater(object): def prevent_run(self): return sickgear.watched_state_queue_scheduler.action.is_in_queue(self.queue_item) - def run(self): + @staticmethod + def is_enabled(): + return True + + def job_run(self): # noinspection PyUnresolvedReferences if self.is_enabled(): - self.amActive = True new_item = self.queue_item() sickgear.watched_state_queue_scheduler.action.add_item(new_item) - self.amActive = False class EmbyWatchedStateUpdater(WatchedStateUpdater): diff --git a/sickgear/webapi.py b/sickgear/webapi.py index 908a32eb..cafec481 100644 --- a/sickgear/webapi.py +++ b/sickgear/webapi.py @@ -48,6 +48,7 @@ from .indexers import indexer_api, indexer_config from .indexers.indexer_config import * from lib.tvinfo_base.exceptions import * from .scene_numbering import set_scene_numbering_helper +from .scheduler import Scheduler from .search_backlog import FORCED_BACKLOG from .show_updater import clean_ignore_require_words from .sgdatetime import SGDatetime @@ -1915,9 +1916,9 @@ class CMD_SickGearPostProcess(ApiCall): if not self.type: self.type = 'manual' - data = processTV.processDir(self.path, process_method=self.process_method, force=self.force_replace, - force_replace=self.is_priority, failed=self.failed, pp_type=self.type, - client=self.client) + data = processTV.process_dir(self.path, process_method=self.process_method, force=self.force_replace, + force_replace=self.is_priority, failed=self.failed, pp_type=self.type, + client=self.client) if not self.return_data: data = "" @@ -2074,7 +2075,7 @@ class CMD_SickGearCheckScheduler(ApiCall): backlogPaused = sickgear.search_queue_scheduler.action.is_backlog_paused() backlogRunning = sickgear.search_queue_scheduler.action.is_backlog_in_progress() - nextBacklog = sickgear.backlog_search_scheduler.next_run().strftime(dateFormat) + nextBacklog = sickgear.search_backlog_scheduler.next_run().strftime(dateFormat) data = {"backlog_is_paused": int(backlogPaused), "backlog_is_running": int(backlogRunning), "last_backlog": (0 < len(sql_result) and _ordinal_to_dateForm(sql_result[0]["last_backlog"])) or '', @@ -2177,15 +2178,15 @@ class CMD_SickGearForceSearch(ApiCall): """ force the specified search type to run """ result = None if 'recent' == self.searchtype and not sickgear.search_queue_scheduler.action.is_recentsearch_in_progress() \ - and not sickgear.recent_search_scheduler.action.amActive: - result = sickgear.recent_search_scheduler.force_run() + and not sickgear.search_recent_scheduler.is_running_job: + result = sickgear.search_recent_scheduler.force_run() elif 'backlog' == self.searchtype and not sickgear.search_queue_scheduler.action.is_backlog_in_progress() \ - and not sickgear.backlog_search_scheduler.action.amActive: - sickgear.backlog_search_scheduler.force_search(force_type=FORCED_BACKLOG) + and not sickgear.search_backlog_scheduler.is_running_job: + sickgear.search_backlog_scheduler.force_search(force_type=FORCED_BACKLOG) result = True elif 'proper' == self.searchtype and not sickgear.search_queue_scheduler.action.is_propersearch_in_progress() \ - and not sickgear.proper_finder_scheduler.action.amActive: - result = sickgear.proper_finder_scheduler.force_run() + and not sickgear.search_propers_scheduler.is_running_job: + result = sickgear.search_propers_scheduler.force_run() if result: return _responds(RESULT_SUCCESS, msg='%s search successfully forced' % self.searchtype) return _responds(RESULT_FAILURE, @@ -2499,6 +2500,13 @@ class CMD_SickGearRestart(ApiCall): def run(self): """ restart sickgear """ + + response = Scheduler.blocking_jobs() + if response: + msg = f'Restart aborted from API because {response.lower()}' + logger.log(msg, logger.DEBUG) + return _responds(RESULT_FAILURE, msg=msg) + sickgear.restart(soft=False) return _responds(RESULT_SUCCESS, msg="SickGear is restarting...") @@ -2817,6 +2825,13 @@ class CMD_SickGearShutdown(ApiCall): def run(self): """ shutdown sickgear """ + + response = Scheduler.blocking_jobs() + if response: + msg = f'Shutdown aborted from API because {response.lower()}' + logger.log(msg, logger.DEBUG) + return _responds(RESULT_FAILURE, msg=msg) + sickgear.events.put(sickgear.events.SystemEvent.SHUTDOWN) return _responds(RESULT_SUCCESS, msg="SickGear is shutting down...") @@ -4668,10 +4683,10 @@ class CMD_SickGearShowsForceUpdate(ApiCall): def run(self): """ force the daily show update now """ if sickgear.show_queue_scheduler.action.is_show_update_running() \ - or sickgear.show_update_scheduler.action.amActive: + or sickgear.update_show_scheduler.is_running_job: return _responds(RESULT_FAILURE, msg="show update already running.") - result = sickgear.show_update_scheduler.force_run() + result = sickgear.update_show_scheduler.force_run() if result: return _responds(RESULT_SUCCESS, msg="daily show update started") return _responds(RESULT_FAILURE, msg="can't start show update currently") diff --git a/sickgear/webserve.py b/sickgear/webserve.py index 9ccafc64..409870fb 100644 --- a/sickgear/webserve.py +++ b/sickgear/webserve.py @@ -65,6 +65,7 @@ from .name_parser.parser import InvalidNameException, InvalidShowException, Name from .providers import newznab, rsstorrent from .scene_numbering import get_scene_absolute_numbering_for_show, get_scene_numbering_for_show, \ get_xem_absolute_numbering_for_show, get_xem_numbering_for_show, set_scene_numbering_helper +from .scheduler import Scheduler from .search_backlog import FORCED_BACKLOG from .sgdatetime import SGDatetime from .show_name_helpers import abbr_showname @@ -1327,8 +1328,8 @@ class MainHandler(WebHandler): now = datetime.datetime.now() events = [ - ('recent', sickgear.recent_search_scheduler.time_left), - ('backlog', sickgear.backlog_search_scheduler.next_backlog_timeleft), + ('recent', sickgear.search_recent_scheduler.time_left), + ('backlog', sickgear.search_backlog_scheduler.next_backlog_timeleft), ] if sickgear.DOWNLOAD_PROPERS: @@ -2070,6 +2071,9 @@ class Home(MainHandler): if str(pid) != str(sickgear.PID): return self.redirect('/home/') + if self.maybe_ignore('Shutdown'): + return + t = PageTemplate(web_handler=self, file='restart.tmpl') t.shutdown = True @@ -2082,6 +2086,9 @@ class Home(MainHandler): if str(pid) != str(sickgear.PID): return self.redirect('/home/') + if self.maybe_ignore('Restart'): + return + t = PageTemplate(web_handler=self, file='restart.tmpl') t.shutdown = False @@ -2089,6 +2096,17 @@ class Home(MainHandler): return t.respond() + def maybe_ignore(self, task): + response = Scheduler.blocking_jobs() + if response: + task and logger.log('%s aborted because %s' % (task, response.lower()), logger.DEBUG) + + self.redirect(self.request.headers['Referer']) + if task: + ui.notifications.message(u'Fail %s because %s, please try later' % (task.lower(), response.lower())) + return True + return False + def update(self, pid=None): if str(pid) != str(sickgear.PID): @@ -2326,15 +2344,15 @@ class Home(MainHandler): t.season_min = ([], [1])[2 < t.latest_season] + [t.latest_season] t.other_seasons = (list(set(all_seasons) - set(t.season_min)), [])[display_show_minimum] t.seasons = [] - for x in all_seasons: - t.seasons += [(x, [None] if x not in (t.season_min + t.other_seasons) else my_db.select( + for cur_season in all_seasons: + t.seasons += [(cur_season, [None] if cur_season not in (t.season_min + t.other_seasons) else my_db.select( 'SELECT *' ' FROM tv_episodes' ' WHERE indexer = ? AND showid = ?' ' AND season = ?' ' ORDER BY episode DESC', - [show_obj.tvid, show_obj.prodid, x] - ), scene_exceptions.has_season_exceptions(show_obj.tvid, show_obj.prodid, x))] + [show_obj.tvid, show_obj.prodid, cur_season] + ), scene_exceptions.ReleaseMap().has_season_exceptions(show_obj.tvid, show_obj.prodid, cur_season))] for row in my_db.select('SELECT season, episode, status' ' FROM tv_episodes' @@ -2423,7 +2441,7 @@ class Home(MainHandler): t.clean_show_name = quote_plus(sickgear.indexermapper.clean_show_name(show_obj.name)) t.min_initial = Quality.get_quality_ui(min(Quality.split_quality(show_obj.quality)[0])) - t.show_obj.exceptions = scene_exceptions.get_scene_exceptions(show_obj.tvid, show_obj.prodid) + t.show_obj.exceptions = scene_exceptions.ReleaseMap().get_alt_names(show_obj.tvid, show_obj.prodid) # noinspection PyUnresolvedReferences t.all_scene_exceptions = show_obj.exceptions # normally Unresolved as not a class attribute, force set above t.scene_numbering = get_scene_numbering_for_show(show_obj.tvid, show_obj.prodid) @@ -2562,7 +2580,7 @@ class Home(MainHandler): @staticmethod def scene_exceptions(tvid_prodid, wanted_season=None): - exceptions_list = sickgear.scene_exceptions.get_all_scene_exceptions(tvid_prodid) + exceptions_list = scene_exceptions.ReleaseMap().get_show_exceptions(tvid_prodid) wanted_season = helpers.try_int(wanted_season, None) wanted_not_found = None is not wanted_season and wanted_season not in exceptions_list if not exceptions_list or wanted_not_found: @@ -2754,7 +2772,7 @@ class Home(MainHandler): return [err_string] return self._generic_message('Error', err_string) - show_obj.exceptions = scene_exceptions.get_all_scene_exceptions(tvid_prodid) + show_obj.exceptions = scene_exceptions.ReleaseMap().get_show_exceptions(tvid_prodid) if None is not quality_preset and int(quality_preset): best_qualities = [] @@ -2971,7 +2989,7 @@ class Home(MainHandler): if do_update_exceptions: try: - scene_exceptions.update_scene_exceptions(show_obj.tvid, show_obj.prodid, exceptions_list) + scene_exceptions.ReleaseMap().update_exceptions(show_obj, exceptions_list) helpers.cpu_sleep() except exceptions_helper.CantUpdateException: errors.append('Unable to force an update on scene exceptions of the show.') @@ -3912,16 +3930,16 @@ class HomeProcessMedia(Home): m = sickgear.NZBGET_MAP.split('=') dir_name, not_used = helpers.path_mapper(m[0], m[1], dir_name) - result = processTV.processDir(dir_name if dir_name else None, - None if not nzb_name else decode_str(nzb_name), - process_method=process_method, pp_type=process_type, - cleanup=cleanup, - force=force in ('on', '1'), - force_replace=force_replace in ('on', '1'), - failed='0' != failed, - webhandler=None if '0' == stream else self.send_message, - show_obj=show_obj, is_basedir=is_basedir in ('on', '1'), - skip_failure_processing=skip_failure_processing, client=client) + result = processTV.process_dir(dir_name if dir_name else None, + None if not nzb_name else decode_str(nzb_name), + process_method=process_method, pp_type=process_type, + cleanup=cleanup, + force=force in ('on', '1'), + force_replace=force_replace in ('on', '1'), + failed='0' != failed, + webhandler=None if '0' == stream else self.send_message, + show_obj=show_obj, is_basedir=is_basedir in ('on', '1'), + skip_failure_processing=skip_failure_processing, client=client) if '0' == stream: regexp = re.compile(r'(?i)', flags=re.UNICODE) @@ -4448,7 +4466,7 @@ class AddShows(Home): t.blocklist = [] t.groups = [] - t.show_scene_maps = list(itervalues(sickgear.scene_exceptions.xem_ids_list)) + t.show_scene_maps = list(itervalues(scene_exceptions.MEMCACHE['release_map_xem'])) has_shows = len(sickgear.showList) t.try_id = [] # [dict try_tip: try_term] @@ -6616,7 +6634,7 @@ class Manage(MainHandler): show_obj = helpers.find_show_by_id(tvid_prodid) if show_obj: - sickgear.backlog_search_scheduler.action.search_backlog([show_obj]) + sickgear.search_backlog_scheduler.action.search_backlog([show_obj]) self.redirect('/manage/backlog-overview/') @@ -7116,11 +7134,11 @@ class ManageSearch(Manage): def index(self): t = PageTemplate(web_handler=self, file='manage_manageSearches.tmpl') - # t.backlog_pi = sickgear.backlog_search_scheduler.action.get_progress_indicator() + # t.backlog_pi = sickgear.search_backlog_scheduler.action.get_progress_indicator() t.backlog_paused = sickgear.search_queue_scheduler.action.is_backlog_paused() t.scheduled_backlog_active_providers = sickgear.search_backlog.BacklogSearcher.providers_active(scheduled=True) t.backlog_running = sickgear.search_queue_scheduler.action.is_backlog_in_progress() - t.backlog_is_active = sickgear.backlog_search_scheduler.action.am_running() + t.backlog_is_active = sickgear.search_backlog_scheduler.action.am_running() t.standard_backlog_running = sickgear.search_queue_scheduler.action.is_standard_backlog_in_progress() t.backlog_running_type = sickgear.search_queue_scheduler.action.type_of_backlog_in_progress() t.recent_search_status = sickgear.search_queue_scheduler.action.is_recentsearch_in_progress() @@ -7161,7 +7179,7 @@ class ManageSearch(Manage): def force_backlog(self): # force it to run the next time it looks if not sickgear.search_queue_scheduler.action.is_standard_backlog_in_progress(): - sickgear.backlog_search_scheduler.force_search(force_type=FORCED_BACKLOG) + sickgear.search_backlog_scheduler.force_search(force_type=FORCED_BACKLOG) logger.log('Backlog search forced') ui.notifications.message('Backlog search started') @@ -7172,7 +7190,7 @@ class ManageSearch(Manage): # force it to run the next time it looks if not sickgear.search_queue_scheduler.action.is_recentsearch_in_progress(): - result = sickgear.recent_search_scheduler.force_run() + result = sickgear.search_recent_scheduler.force_run() if result: logger.log('Recent search forced') ui.notifications.message('Recent search started') @@ -7183,7 +7201,7 @@ class ManageSearch(Manage): def force_find_propers(self): # force it to run the next time it looks - result = sickgear.proper_finder_scheduler.force_run() + result = sickgear.search_propers_scheduler.force_run() if result: logger.log('Find propers search forced') ui.notifications.message('Find propers search started') @@ -7207,10 +7225,10 @@ class ShowTasks(Manage): t = PageTemplate(web_handler=self, file='manage_showProcesses.tmpl') t.queue_length = sickgear.show_queue_scheduler.action.queue_length() t.people_queue = sickgear.people_queue_scheduler.action.queue_data() - t.next_run = sickgear.show_update_scheduler.last_run.replace( - hour=sickgear.show_update_scheduler.start_time.hour) + t.next_run = sickgear.update_show_scheduler.last_run.replace( + hour=sickgear.update_show_scheduler.start_time.hour) t.show_update_running = sickgear.show_queue_scheduler.action.is_show_update_running() \ - or sickgear.show_update_scheduler.action.amActive + or sickgear.update_show_scheduler.is_running_job my_db = db.DBConnection(row_type='dict') sql_result = my_db.select('SELECT n.indexer || ? || n.indexer_id AS tvid_prodid,' @@ -7293,7 +7311,7 @@ class ShowTasks(Manage): def force_show_update(self): - result = sickgear.show_update_scheduler.force_run() + result = sickgear.update_show_scheduler.force_run() if result: logger.log('Show Update forced') ui.notifications.message('Forced Show Update started') @@ -7982,7 +8000,7 @@ class ConfigGeneral(Config): def update_alt(): """ Load scene exceptions """ - changed_exceptions, cnt_updated_numbers, min_remain_iv = scene_exceptions.retrieve_exceptions() + changed_exceptions, cnt_updated_numbers, min_remain_iv = scene_exceptions.ReleaseMap().fetch_exceptions() return json_dumps(dict(names=int(changed_exceptions), numbers=cnt_updated_numbers, min_remain_iv=min_remain_iv)) @@ -7991,7 +8009,7 @@ class ConfigGeneral(Config): """ Return alternative release names and numbering as json text""" # alternative release names and numbers - alt_names = scene_exceptions.get_all_scene_exceptions(tvid_prodid) + alt_names = scene_exceptions.ReleaseMap().get_show_exceptions(tvid_prodid) alt_numbers = get_scene_numbering_for_show(*TVidProdid(tvid_prodid).tuple) # arbitrary order ui_output = 'No alternative names or numbers to export' @@ -8180,8 +8198,8 @@ class ConfigGeneral(Config): sickgear.UPDATE_SHOWS_ON_START = config.checkbox_to_value(update_shows_on_start) sickgear.SHOW_UPDATE_HOUR = config.minimax(show_update_hour, 3, 0, 23) try: - with sickgear.show_update_scheduler.lock: - sickgear.show_update_scheduler.start_time = datetime.time(hour=sickgear.SHOW_UPDATE_HOUR) + with sickgear.update_show_scheduler.lock: + sickgear.update_show_scheduler.start_time = datetime.time(hour=sickgear.SHOW_UPDATE_HOUR) except (BaseException, Exception) as e: logger.error('Could not change Show Update Scheduler time: %s' % ex(e)) sickgear.TRASH_REMOVE_SHOW = config.checkbox_to_value(trash_remove_show) diff --git a/tests/scene_helpers_tests.py b/tests/scene_helpers_tests.py index 29f80deb..ff0d9bd3 100644 --- a/tests/scene_helpers_tests.py +++ b/tests/scene_helpers_tests.py @@ -74,14 +74,15 @@ class SceneExceptionTestCase(test.SickbeardTestDBCase): sickgear.showList.append(s) sickgear.showDict[s.sid_int] = s sickgear.webserve.Home.make_showlist_unique_names() - scene_exceptions.retrieve_exceptions() + scene_exceptions.ReleaseMap().fetch_exceptions() name_cache.build_name_cache() def test_sceneExceptionsEmpty(self): - self.assertEqual(scene_exceptions.get_scene_exceptions(0, 0), []) + self.assertEqual(scene_exceptions.ReleaseMap().get_alt_names(0, 0), []) def test_sceneExceptionsBlack_Lagoon(self): - self.assertEqual(sorted(scene_exceptions.get_scene_exceptions(1, 79604)), ['Black-Lagoon']) + self.assertEqual(sorted( + scene_exceptions.ReleaseMap().get_alt_names(1, 79604)), ['Black-Lagoon']) def test_sceneExceptionByName(self): self.assertEqual(scene_exceptions.get_scene_exception_by_name( @@ -98,14 +99,18 @@ class SceneExceptionTestCase(test.SickbeardTestDBCase): s.anime = 1 sickgear.showList.append(s) sickgear.showDict[s.sid_int] = s - scene_exceptions.retrieve_exceptions() + scene_exceptions.ReleaseMap().fetch_exceptions() name_cache.build_name_cache() - self.assertEqual(scene_exceptions.get_scene_exception_by_name('ブラック・ラグーン'), [1, 79604, -1]) - self.assertEqual(scene_exceptions.get_scene_exception_by_name('Burakku Ragūn'), [1, 79604, -1]) - self.assertEqual(scene_exceptions.get_scene_exception_by_name('Rokka no Yuusha'), [1, 295243, -1]) + self.assertEqual(scene_exceptions.get_scene_exception_by_name( + 'ブラック・ラグーン'), [1, 79604, -1]) + self.assertEqual(scene_exceptions.get_scene_exception_by_name( + 'Burakku Ragūn'), [1, 79604, -1]) + self.assertEqual(scene_exceptions.get_scene_exception_by_name( + 'Rokka no Yuusha'), [1, 295243, -1]) def test_sceneExceptionByNameEmpty(self): - self.assertEqual(scene_exceptions.get_scene_exception_by_name('nothing useful'), [None, None, None]) + self.assertEqual(scene_exceptions.get_scene_exception_by_name( + 'nothing useful'), [None, None, None]) def test_sceneExceptionsResetNameCache(self): # clear the exceptions @@ -117,7 +122,7 @@ class SceneExceptionTestCase(test.SickbeardTestDBCase): name_cache.add_name_to_cache('Cached Name', prodid=0) # updating should not clear the cache this time since our exceptions didn't change - scene_exceptions.retrieve_exceptions() + scene_exceptions.ReleaseMap().fetch_exceptions() self.assertEqual(name_cache.retrieve_name_from_cache('Cached Name'), (0, 0)) diff --git a/tests/webapi_tests.py b/tests/webapi_tests.py index a7b1c9a9..816de55b 100644 --- a/tests/webapi_tests.py +++ b/tests/webapi_tests.py @@ -180,7 +180,7 @@ class WebAPICase(test.SickbeardTestDBCase): search_queue.SearchQueue(), cycle_time=datetime.timedelta(seconds=3), thread_name='SEARCHQUEUE') - sickgear.backlog_search_scheduler = search_backlog.BacklogSearchScheduler( + sickgear.search_backlog_scheduler = search_backlog.BacklogSearchScheduler( search_backlog.BacklogSearcher(), cycle_time=datetime.timedelta(minutes=60), run_delay=datetime.timedelta(minutes=60), From 8223edb2cf961fc0844c93d5fefd477da6326dc2 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sun, 16 Apr 2023 01:31:51 +0100 Subject: [PATCH 12/36] Add config to change media process log message if there is no media to process. --- CHANGES.md | 1 + .../interfaces/default/config_postProcessing.tmpl | 13 ++++++++++++- sickgear/__init__.py | 5 ++++- sickgear/processTV.py | 9 +++++---- sickgear/webserve.py | 3 ++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a1c15055..9fbc8cc8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ * Update urllib3 1.26.14 (a06c05c) to 1.26.15 (25cca389) * Change add jobs to centralise scheduler activities * Change refactor scene_exceptions +* Add config to change media process log message if there is no media to process ### 3.28.0 (2023-04-12 13:05:00 UTC) diff --git a/gui/slick/interfaces/default/config_postProcessing.tmpl b/gui/slick/interfaces/default/config_postProcessing.tmpl index b6e52c34..a3a4a191 100644 --- a/gui/slick/interfaces/default/config_postProcessing.tmpl +++ b/gui/slick/interfaces/default/config_postProcessing.tmpl @@ -85,7 +85,7 @@

files in the completed TV downloads folder

-

note: do not enable with external post processing scripts like sabTosickgear for SABnzbd, or NZBMedia for NZBGET

+

note: do not enable with external post processing scripts like sabTosickgear for SABnzbd, or NZBMedia for NZBGET unless you're an expert

@@ -109,6 +109,17 @@
+ +
+ +
#if False: """
diff --git a/sickgear/__init__.py b/sickgear/__init__.py index 1accc16d..94102c50 100644 --- a/sickgear/__init__.py +++ b/sickgear/__init__.py @@ -295,6 +295,7 @@ PROCESS_LAST_CLEANUP = False PROCESS_METHOD = None MOVE_ASSOCIATED_FILES = False POSTPONE_IF_SYNC_FILES = True +PROCESS_POSITIVE_LOG = True NFO_RENAME = True TV_DOWNLOAD_DIR = None UNPACK = False @@ -716,7 +717,7 @@ def init_stage_1(console_logging): SUBTITLES_HISTORY, SUBTITLES_SERVICES_LIST, SUBTITLES_SERVICES_ENABLED, SUBTITLES_SERVICES_AUTH # Media Process/Post-Processing global TV_DOWNLOAD_DIR, PROCESS_METHOD, PROCESS_AUTOMATICALLY, MEDIAPROCESS_INTERVAL, \ - POSTPONE_IF_SYNC_FILES, EXTRA_SCRIPTS, SG_EXTRA_SCRIPTS, \ + POSTPONE_IF_SYNC_FILES, PROCESS_POSITIVE_LOG, EXTRA_SCRIPTS, SG_EXTRA_SCRIPTS, \ DEFAULT_MEDIAPROCESS_INTERVAL, MIN_MEDIAPROCESS_INTERVAL, \ UNPACK, SKIP_REMOVED_FILES, MOVE_ASSOCIATED_FILES, NFO_RENAME, RENAME_EPISODES, AIRDATE_EPISODES, \ USE_FAILED_DOWNLOADS, DELETE_FAILED @@ -1022,6 +1023,7 @@ def init_stage_1(console_logging): PROCESS_LAST_CLEANUP = bool(check_setting_int(CFG, 'General', 'process_last_cleanup', 0)) MOVE_ASSOCIATED_FILES = bool(check_setting_int(CFG, 'General', 'move_associated_files', 0)) POSTPONE_IF_SYNC_FILES = bool(check_setting_int(CFG, 'General', 'postpone_if_sync_files', 1)) + PROCESS_POSITIVE_LOG = bool(check_setting_int(CFG, 'General', 'process_positive_log', 0)) NFO_RENAME = bool(check_setting_int(CFG, 'General', 'nfo_rename', 1)) CREATE_MISSING_SHOW_DIRS = bool(check_setting_int(CFG, 'General', 'create_missing_show_dirs', 0)) SHOW_DIRS_WITH_DOTS = bool(check_setting_int(CFG, 'General', 'show_dirs_with_dots', 0)) @@ -1989,6 +1991,7 @@ def save_config(): new_config['General']['process_last_cleanup'] = int(PROCESS_LAST_CLEANUP) new_config['General']['move_associated_files'] = int(MOVE_ASSOCIATED_FILES) new_config['General']['postpone_if_sync_files'] = int(POSTPONE_IF_SYNC_FILES) + new_config['General']['process_positive_log'] = int(PROCESS_POSITIVE_LOG) new_config['General']['nfo_rename'] = int(NFO_RENAME) new_config['General']['process_automatically'] = int(PROCESS_AUTOMATICALLY) new_config['General']['unpack'] = int(UNPACK) diff --git a/sickgear/processTV.py b/sickgear/processTV.py index 08c53072..c3fafbae 100644 --- a/sickgear/processTV.py +++ b/sickgear/processTV.py @@ -514,7 +514,7 @@ class ProcessTVShow(object): for f in sorted(list(set([os.path.dirname(item) for item in work_files]) - {path}), key=len, reverse=True): self._delete_folder(f) - def _bottom_line(text, log_level=logger.DEBUG): + def _bottom_line(text, log_level=logger.MESSAGE): self._buffer('-' * len(text)) self._log_helper(text, log_level) @@ -524,11 +524,12 @@ class ProcessTVShow(object): if self.any_vid_processed: if not self.files_failed: - _bottom_line('Successfully processed.', logger.MESSAGE) + _bottom_line('Successfully processed.') else: _bottom_line(f'Successfully processed at least one video file' - f'{(", others were skipped", " and skipped another")[1 == self.files_failed]}.', - logger.MESSAGE) + f'{(", others were skipped", " and skipped another")[1 == self.files_failed]}.') + elif sickgear.PROCESS_POSITIVE_LOG: + _bottom_line('Success, no media to process.') else: _bottom_line('Failed! Did not process any files.', logger.WARNING) diff --git a/sickgear/webserve.py b/sickgear/webserve.py index 409870fb..4cdeb8f8 100644 --- a/sickgear/webserve.py +++ b/sickgear/webserve.py @@ -8504,7 +8504,7 @@ class ConfigMediaProcess(Config): unpack=None, keep_processed_dir=None, process_method=None, extra_scripts='', sg_extra_scripts='', rename_episodes=None, airdate_episodes=None, - move_associated_files=None, postpone_if_sync_files=None, + move_associated_files=None, postpone_if_sync_files=None, process_positive_log=None, naming_custom_abd=None, naming_custom_sports=None, naming_custom_anime=None, naming_strip_year=None, use_failed_downloads=None, delete_failed=None, skip_removed_files=None, nfo_rename=None, @@ -8541,6 +8541,7 @@ class ConfigMediaProcess(Config): sickgear.AIRDATE_EPISODES = config.checkbox_to_value(airdate_episodes) sickgear.MOVE_ASSOCIATED_FILES = config.checkbox_to_value(move_associated_files) sickgear.POSTPONE_IF_SYNC_FILES = config.checkbox_to_value(postpone_if_sync_files) + sickgear.PROCESS_POSITIVE_LOG = config.checkbox_to_value(process_positive_log) sickgear.NAMING_CUSTOM_ABD = config.checkbox_to_value(naming_custom_abd) sickgear.NAMING_CUSTOM_SPORTS = config.checkbox_to_value(naming_custom_sports) sickgear.NAMING_CUSTOM_ANIME = config.checkbox_to_value(naming_custom_anime) From 1f7e467e07cb262453d407a84941d6002c1e77cf Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Tue, 18 Apr 2023 13:33:57 +0200 Subject: [PATCH 13/36] Change minimum required Python to version 3.8 --- CHANGES.md | 1 + sickgear.py | 2 +- sickgear/py_requirement.data | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9fbc8cc8..42b50d8c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 3.29.0 (2023-xx-xx xx:xx:00 UTC) +* Change minimum required Python to version 3.8 * Update Apprise 1.2.1 (3d07004) to 1.3.0 (6458ab0) * Update attr 22.2.0 (a9960de) to 22.2.0 (683d056) * Update diskcache 5.4.0 (1cb1425) to 5.6.1 (4d30686) diff --git a/sickgear.py b/sickgear.py index b664f58f..0b5925dc 100755 --- a/sickgear.py +++ b/sickgear.py @@ -39,7 +39,7 @@ warnings.filterwarnings('ignore', module=r'.*ssl_.*', message='.*SSLContext obje warnings.filterwarnings('ignore', module=r'.*zoneinfo.*', message='.*file or directory.*') warnings.filterwarnings('ignore', message='.*deprecated in cryptography.*') -versions = [((3, 7, 1), (3, 8, 16)), +versions = [((3, 8, 0), (3, 8, 16)), ((3, 9, 0), (3, 9, 2)), ((3, 9, 4), (3, 9, 16)), ((3, 10, 0), (3, 11, 3))] # inclusive version ranges if not any(list(map(lambda v: v[0] <= sys.version_info[:3] <= v[1], versions))) and not int(os.environ.get('PYT', 0)): diff --git a/sickgear/py_requirement.data b/sickgear/py_requirement.data index 548d7136..98fccd6d 100644 --- a/sickgear/py_requirement.data +++ b/sickgear/py_requirement.data @@ -1 +1 @@ -3.7 \ No newline at end of file +3.8 \ No newline at end of file From fb6ef08f011a052342cce02be0b69a7e92510c41 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sat, 22 Apr 2023 08:16:49 +0100 Subject: [PATCH 14/36] Change remove redundant py2 import futures. --- CHANGES.md | 1 + SickBeard.py | 1 - lib/api_tmdb/tmdb_api.py | 1 - lib/api_tvdb/tvdb_api.py | 1 - lib/api_tvdb/tvdb_cache.py | 1 - lib/sg_helpers.py | 1 - lib/simplejson/tool.py | 1 - sickgear.py | 3 --- sickgear/__init__.py | 2 -- sickgear/common.py | 2 -- sickgear/db.py | 2 -- sickgear/failedProcessor.py | 2 -- sickgear/helpers.py | 4 ---- sickgear/logger.py | 2 -- sickgear/metadata/generic.py | 1 - sickgear/metadata/kodi.py | 2 -- sickgear/metadata/mede8er.py | 2 -- sickgear/metadata/mediabrowser.py | 2 -- sickgear/metadata/tivo.py | 2 -- sickgear/metadata/wdtv.py | 1 - sickgear/metadata/xbmc.py | 1 - sickgear/metadata/xbmc_12plus.py | 1 - sickgear/name_parser/parser.py | 2 -- sickgear/notifiers/growl.py | 1 - sickgear/notifiers/telegram.py | 1 - sickgear/nzbSplitter.py | 2 -- sickgear/people_queue.py | 2 -- sickgear/postProcessor.py | 2 -- sickgear/processTV.py | 2 -- sickgear/providers/btn.py | 2 -- sickgear/providers/generic.py | 2 -- sickgear/providers/newznab.py | 1 - sickgear/providers/thepiratebay.py | 2 -- sickgear/scheduler.py | 2 -- sickgear/search.py | 2 -- sickgear/search_backlog.py | 2 -- sickgear/search_propers.py | 2 -- sickgear/search_queue.py | 2 -- sickgear/search_recent.py | 2 -- sickgear/show_queue.py | 2 -- sickgear/tv.py | 2 -- sickgear/tvcache.py | 2 -- sickgear/watchedstate_queue.py | 2 -- sickgear/webapi.py | 2 -- sickgear/webserve.py | 2 -- tests/all_tests.py | 1 - tests/db_tests.py | 1 - tests/migration_tests.py | 2 -- tests/name_parser_tests.py | 1 - tests/newznab_tests.py | 1 - tests/pp_tests.py | 1 - tests/show_tests.py | 2 -- tests/snatch_tests.py | 1 - tests/test_lib.py | 3 --- tests/tv_tests.py | 5 ++--- tests/webapi_tests.py | 1 - tests/xem_tests.py | 3 --- 57 files changed, 3 insertions(+), 98 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 42b50d8c..6d1ebde0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) * Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) * Update urllib3 1.26.14 (a06c05c) to 1.26.15 (25cca389) +* Change remove redundant py2 import futures * Change add jobs to centralise scheduler activities * Change refactor scene_exceptions * Add config to change media process log message if there is no media to process diff --git a/SickBeard.py b/SickBeard.py index 2c8add05..a19d66f5 100644 --- a/SickBeard.py +++ b/SickBeard.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function from time import sleep import runpy print( diff --git a/lib/api_tmdb/tmdb_api.py b/lib/api_tmdb/tmdb_api.py index 2f3a8fad..faa41608 100644 --- a/lib/api_tmdb/tmdb_api.py +++ b/lib/api_tmdb/tmdb_api.py @@ -1,7 +1,6 @@ # encoding:utf-8 # author:Prinz23 # project:tmdb_api -from __future__ import division __author__ = 'Prinz23' __version__ = '1.0' diff --git a/lib/api_tvdb/tvdb_api.py b/lib/api_tvdb/tvdb_api.py index c254d9a0..3edf1eb0 100644 --- a/lib/api_tvdb/tvdb_api.py +++ b/lib/api_tvdb/tvdb_api.py @@ -5,7 +5,6 @@ # repository:http://github.com/dbr/tvdb_api # license:un license (http://unlicense.org/) -from __future__ import absolute_import from functools import wraps __author__ = 'dbr/Ben' diff --git a/lib/api_tvdb/tvdb_cache.py b/lib/api_tvdb/tvdb_cache.py index 9edc9b97..1d7b7e68 100644 --- a/lib/api_tvdb/tvdb_cache.py +++ b/lib/api_tvdb/tvdb_cache.py @@ -9,7 +9,6 @@ urllib2 caching handler Modified from http://code.activestate.com/recipes/491261/ """ -from __future__ import with_statement __author__ = "dbr/Ben" __version__ = "1.9" diff --git a/lib/sg_helpers.py b/lib/sg_helpers.py index 4dc99d1c..6111a441 100644 --- a/lib/sg_helpers.py +++ b/lib/sg_helpers.py @@ -2,7 +2,6 @@ # --------------- # functions are placed here to remove cyclic import issues from placement in helpers # -from __future__ import division import ast import codecs import datetime diff --git a/lib/simplejson/tool.py b/lib/simplejson/tool.py index 062e8e2c..35627db7 100644 --- a/lib/simplejson/tool.py +++ b/lib/simplejson/tool.py @@ -10,7 +10,6 @@ Usage:: Expecting property name: line 1 column 2 (char 2) """ -from __future__ import with_statement import sys import simplejson as json diff --git a/sickgear.py b/sickgear.py index 0b5925dc..c37e2b01 100755 --- a/sickgear.py +++ b/sickgear.py @@ -16,9 +16,6 @@ # along with SickGear. If not, see . # Check needed software dependencies to nudge users to fix their setup -from __future__ import print_function -from __future__ import with_statement - import codecs import datetime import errno diff --git a/sickgear/__init__.py b/sickgear/__init__.py index 94102c50..de2f55ea 100644 --- a/sickgear/__init__.py +++ b/sickgear/__init__.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - from collections import OrderedDict from threading import Lock diff --git a/sickgear/common.py b/sickgear/common.py index ae5ecaa2..20ba2777 100644 --- a/sickgear/common.py +++ b/sickgear/common.py @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import division - from functools import reduce import operator import os.path diff --git a/sickgear/db.py b/sickgear/db.py index b20485fa..6bdaf26e 100644 --- a/sickgear/db.py +++ b/sickgear/db.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import datetime import itertools import os.path diff --git a/sickgear/failedProcessor.py b/sickgear/failedProcessor.py index 7af0483b..ff9d261c 100644 --- a/sickgear/failedProcessor.py +++ b/sickgear/failedProcessor.py @@ -14,8 +14,6 @@ # # You should have received a copy of the GNU General Public License -from __future__ import with_statement - import exceptions_helper import sickgear diff --git a/sickgear/helpers.py b/sickgear/helpers.py index 117ec034..4d610df8 100644 --- a/sickgear/helpers.py +++ b/sickgear/helpers.py @@ -15,10 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import division -from __future__ import print_function -from __future__ import with_statement - from itertools import cycle import datetime import hashlib diff --git a/sickgear/logger.py b/sickgear/logger.py index 1ecbca02..beaaebb1 100644 --- a/sickgear/logger.py +++ b/sickgear/logger.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import codecs import datetime import logging diff --git a/sickgear/metadata/generic.py b/sickgear/metadata/generic.py index 33cd8e1c..ad075d2c 100644 --- a/sickgear/metadata/generic.py +++ b/sickgear/metadata/generic.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement from collections import OrderedDict import datetime diff --git a/sickgear/metadata/kodi.py b/sickgear/metadata/kodi.py index f0787a36..2efee242 100644 --- a/sickgear/metadata/kodi.py +++ b/sickgear/metadata/kodi.py @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import absolute_import - import datetime import io import os diff --git a/sickgear/metadata/mede8er.py b/sickgear/metadata/mede8er.py index 62c0ec9b..46905273 100644 --- a/sickgear/metadata/mede8er.py +++ b/sickgear/metadata/mede8er.py @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import absolute_import - import datetime from . import mediabrowser diff --git a/sickgear/metadata/mediabrowser.py b/sickgear/metadata/mediabrowser.py index 5ae2cd60..2746bbb8 100644 --- a/sickgear/metadata/mediabrowser.py +++ b/sickgear/metadata/mediabrowser.py @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import absolute_import - import datetime import os import re diff --git a/sickgear/metadata/tivo.py b/sickgear/metadata/tivo.py index 9d749bee..412ea0aa 100644 --- a/sickgear/metadata/tivo.py +++ b/sickgear/metadata/tivo.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement, absolute_import - import datetime import os diff --git a/sickgear/metadata/wdtv.py b/sickgear/metadata/wdtv.py index b0c87c92..e342d963 100644 --- a/sickgear/metadata/wdtv.py +++ b/sickgear/metadata/wdtv.py @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import absolute_import import datetime import os diff --git a/sickgear/metadata/xbmc.py b/sickgear/metadata/xbmc.py index ae5de5a3..7628b3ef 100644 --- a/sickgear/metadata/xbmc.py +++ b/sickgear/metadata/xbmc.py @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import absolute_import import os diff --git a/sickgear/metadata/xbmc_12plus.py b/sickgear/metadata/xbmc_12plus.py index 2721d291..545cbcc0 100644 --- a/sickgear/metadata/xbmc_12plus.py +++ b/sickgear/metadata/xbmc_12plus.py @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import absolute_import import datetime diff --git a/sickgear/name_parser/parser.py b/sickgear/name_parser/parser.py index 9c0bd936..1e242202 100644 --- a/sickgear/name_parser/parser.py +++ b/sickgear/name_parser/parser.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement, division - import datetime import os import os.path diff --git a/sickgear/notifiers/growl.py b/sickgear/notifiers/growl.py index ffc51c9a..a50516ce 100644 --- a/sickgear/notifiers/growl.py +++ b/sickgear/notifiers/growl.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function import re from .generic import Notifier, notify_strings diff --git a/sickgear/notifiers/telegram.py b/sickgear/notifiers/telegram.py index 96d86319..3522f3a0 100644 --- a/sickgear/notifiers/telegram.py +++ b/sickgear/notifiers/telegram.py @@ -14,7 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import absolute_import import os import re diff --git a/sickgear/nzbSplitter.py b/sickgear/nzbSplitter.py index c4334544..474212a7 100644 --- a/sickgear/nzbSplitter.py +++ b/sickgear/nzbSplitter.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import os import re diff --git a/sickgear/people_queue.py b/sickgear/people_queue.py index 0e99721c..7441508b 100644 --- a/sickgear/people_queue.py +++ b/sickgear/people_queue.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import traceback # noinspection PyPep8Naming diff --git a/sickgear/postProcessor.py b/sickgear/postProcessor.py index 711e0bf6..050276c4 100644 --- a/sickgear/postProcessor.py +++ b/sickgear/postProcessor.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import glob import os import re diff --git a/sickgear/processTV.py b/sickgear/processTV.py index c3fafbae..45f28304 100644 --- a/sickgear/processTV.py +++ b/sickgear/processTV.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - from functools import partial import datetime import os diff --git a/sickgear/providers/btn.py b/sickgear/providers/btn.py index 6b87bff9..2c953a36 100644 --- a/sickgear/providers/btn.py +++ b/sickgear/providers/btn.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import division - import math import random import re diff --git a/sickgear/providers/generic.py b/sickgear/providers/generic.py index e5ccb797..f3fbcff0 100644 --- a/sickgear/providers/generic.py +++ b/sickgear/providers/generic.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement, division - from base64 import b64decode import codecs import datetime diff --git a/sickgear/providers/newznab.py b/sickgear/providers/newznab.py index 244b92e2..02c425fb 100644 --- a/sickgear/providers/newznab.py +++ b/sickgear/providers/newznab.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import division # from collections import OrderedDict from io import BytesIO from math import ceil diff --git a/sickgear/providers/thepiratebay.py b/sickgear/providers/thepiratebay.py index 1e390aef..4956e181 100644 --- a/sickgear/providers/thepiratebay.py +++ b/sickgear/providers/thepiratebay.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement, division - import re import traceback diff --git a/sickgear/scheduler.py b/sickgear/scheduler.py index 130e5771..ad7dbb8a 100644 --- a/sickgear/scheduler.py +++ b/sickgear/scheduler.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import division - import datetime import time import threading diff --git a/sickgear/search.py b/sickgear/search.py index 67f3b22f..9c80f7d8 100644 --- a/sickgear/search.py +++ b/sickgear/search.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import datetime import os import re diff --git a/sickgear/search_backlog.py b/sickgear/search_backlog.py index 1181581b..170ed818 100644 --- a/sickgear/search_backlog.py +++ b/sickgear/search_backlog.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement, division - import datetime from math import ceil diff --git a/sickgear/search_propers.py b/sickgear/search_propers.py index 87c7ced9..addcde64 100644 --- a/sickgear/search_propers.py +++ b/sickgear/search_propers.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import sickgear from .scheduler import Job diff --git a/sickgear/search_queue.py b/sickgear/search_queue.py index 200910b9..3880dcbe 100644 --- a/sickgear/search_queue.py +++ b/sickgear/search_queue.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import copy import datetime import re diff --git a/sickgear/search_recent.py b/sickgear/search_recent.py index 5ad92515..58497644 100644 --- a/sickgear/search_recent.py +++ b/sickgear/search_recent.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import sickgear from .scheduler import Job diff --git a/sickgear/show_queue.py b/sickgear/show_queue.py index 8f90d4d2..3f16d18a 100644 --- a/sickgear/show_queue.py +++ b/sickgear/show_queue.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import datetime import os import traceback diff --git a/sickgear/tv.py b/sickgear/tv.py index 0587137d..6aa7c28a 100644 --- a/sickgear/tv.py +++ b/sickgear/tv.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import random import weakref from collections import Counter, OrderedDict diff --git a/sickgear/tvcache.py b/sickgear/tvcache.py index d7fbd365..72889e78 100644 --- a/sickgear/tvcache.py +++ b/sickgear/tvcache.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - import datetime import itertools import time diff --git a/sickgear/watchedstate_queue.py b/sickgear/watchedstate_queue.py index c7449506..55119d45 100644 --- a/sickgear/watchedstate_queue.py +++ b/sickgear/watchedstate_queue.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - from . import logger, generic_queue from .webserve import History diff --git a/sickgear/webapi.py b/sickgear/webapi.py index cafec481..ba3ca528 100644 --- a/sickgear/webapi.py +++ b/sickgear/webapi.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement - # noinspection PyProtectedMember from mimetypes import MimeTypes from random import randint diff --git a/sickgear/webserve.py b/sickgear/webserve.py index 4cdeb8f8..c75944f1 100644 --- a/sickgear/webserve.py +++ b/sickgear/webserve.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import with_statement, division - # noinspection PyProtectedMember from mimetypes import MimeTypes from urllib.parse import urljoin diff --git a/tests/all_tests.py b/tests/all_tests.py index 1583dfb8..62334531 100644 --- a/tests/all_tests.py +++ b/tests/all_tests.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function if '__main__' == __name__: import warnings warnings.filterwarnings('ignore', module=r'.*fuz.*', message='.*Sequence.*') diff --git a/tests/db_tests.py b/tests/db_tests.py index 3bf868fc..ae362290 100644 --- a/tests/db_tests.py +++ b/tests/db_tests.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function import unittest import test_lib as test from sickgear import cache_db, mainDB, failed_db diff --git a/tests/migration_tests.py b/tests/migration_tests.py index a03cd27c..ce394261 100644 --- a/tests/migration_tests.py +++ b/tests/migration_tests.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import gc import sys import os.path diff --git a/tests/name_parser_tests.py b/tests/name_parser_tests.py index 05cefe99..c993ea11 100644 --- a/tests/name_parser_tests.py +++ b/tests/name_parser_tests.py @@ -1,4 +1,3 @@ -from __future__ import print_function import datetime import os.path import test_lib as test diff --git a/tests/newznab_tests.py b/tests/newznab_tests.py index a5afea27..59d88c31 100644 --- a/tests/newznab_tests.py +++ b/tests/newznab_tests.py @@ -1,5 +1,4 @@ # coding=utf-8 -from __future__ import print_function import warnings warnings.filterwarnings('ignore', module=r'.*fuz.*', message='.*Sequence.*') warnings.filterwarnings('ignore', module=r'.*dateutil.*', message='.*Unicode.*') diff --git a/tests/pp_tests.py b/tests/pp_tests.py index 58e05bf5..036d2f4d 100644 --- a/tests/pp_tests.py +++ b/tests/pp_tests.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function import warnings warnings.filterwarnings('ignore', module=r'.*fuz.*', message='.*Sequence.*') diff --git a/tests/show_tests.py b/tests/show_tests.py index 82bac9b0..a47c878f 100644 --- a/tests/show_tests.py +++ b/tests/show_tests.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function - import datetime import unittest diff --git a/tests/snatch_tests.py b/tests/snatch_tests.py index 5240becd..a23ef71b 100644 --- a/tests/snatch_tests.py +++ b/tests/snatch_tests.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function # import random import unittest diff --git a/tests/test_lib.py b/tests/test_lib.py index 58b07ac8..8fefd243 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -16,9 +16,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function -from __future__ import with_statement - import gc import glob import os.path diff --git a/tests/tv_tests.py b/tests/tv_tests.py index ab7568a0..32a5f784 100644 --- a/tests/tv_tests.py +++ b/tests/tv_tests.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function import unittest import test_lib as test from random import randint @@ -208,10 +207,10 @@ find_tests = [ 'description': 'simple standard search via main id dict'}, {'para': {'show_id': {TVINFO_TVDB: 12345}}, 'result': None, 'description': 'simple standard search via main id dict, for non-existing show'}, - {'para': {'show_id': {TVINFO_TVDB: 123, TVINFO_TVMAZE: 123}, 'check_multishow': True}, + {'para': {'show_id': {TVINFO_TVDB: 123, TVINFO_TVMAZE: 123}, 'check_multishow': True}, 'result': {'success': False}, 'description': 'search via 2 ids matching multiple shows and multi show check'}, - {'para': {'show_id': {TVINFO_TVDB: 5555, TVINFO_TVMAZE: 123}, 'check_multishow': True}, + {'para': {'show_id': {TVINFO_TVDB: 5555, TVINFO_TVMAZE: 123}, 'check_multishow': True}, 'result': {'tvid': TVINFO_TVMAZE, 'prodid': 123}, 'description': 'search via 2 ids matching only 1 show and multi show check'}, {'para': {'show_id': {TVINFO_TVDB: 123, TVINFO_TVMAZE: 123}}, diff --git a/tests/webapi_tests.py b/tests/webapi_tests.py index 816de55b..6ccac0ae 100644 --- a/tests/webapi_tests.py +++ b/tests/webapi_tests.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function import datetime import itertools import os diff --git a/tests/xem_tests.py b/tests/xem_tests.py index 1d3cb6d6..7490e66b 100644 --- a/tests/xem_tests.py +++ b/tests/xem_tests.py @@ -16,9 +16,6 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . -from __future__ import print_function -from __future__ import with_statement - import os.path import re import sys From 64ec74ed4cdb82db04782a70b6fb38c883e87b63 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sat, 22 Apr 2023 11:25:02 +0100 Subject: [PATCH 15/36] Change allow rapidfuzz update from 2.x.x to 3.x.x --- CHANGES.md | 1 + recommended.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6d1ebde0..d4bae2dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ * Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee) * Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b) * Update urllib3 1.26.14 (a06c05c) to 1.26.15 (25cca389) +* Change allow rapidfuzz update from 2.x.x to 3.x.x * Change remove redundant py2 import futures * Change add jobs to centralise scheduler activities * Change refactor scene_exceptions diff --git a/recommended.txt b/recommended.txt index 726b5ba7..2ff506f6 100644 --- a/recommended.txt +++ b/recommended.txt @@ -11,6 +11,6 @@ orjson; 'Windows' == platform_system orjson; 'Linux' == platform_system and ('x86_64' == platform_machine or 'aarch64' == platform_machine or 'armv7l' == platform_machine) pip Levenshtein -rapidfuzz < 3.0.0 +rapidfuzz < 4.0.0 regex setuptools From 8561e5ac0c3706ee3b36d553fdf07abb40f4804e Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sat, 22 Apr 2023 23:22:11 +0100 Subject: [PATCH 16/36] Change view-show text "invalid timeformat" to "time unknown". --- CHANGES.md | 1 + gui/slick/interfaces/default/displayShow.tmpl | 7 ++----- gui/slick/interfaces/default/episodeView.tmpl | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d4bae2dd..ee558d5d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ * Change add jobs to centralise scheduler activities * Change refactor scene_exceptions * Add config to change media process log message if there is no media to process +* Change view-show text "invalid timeformat" to "time unknown" ### 3.28.0 (2023-04-12 13:05:00 UTC) diff --git a/gui/slick/interfaces/default/displayShow.tmpl b/gui/slick/interfaces/default/displayShow.tmpl index 29d4667d..fc7d97b1 100644 --- a/gui/slick/interfaces/default/displayShow.tmpl +++ b/gui/slick/interfaces/default/displayShow.tmpl @@ -113,10 +113,7 @@
#if $show_message -
- $show_message -
- +
$show_message
#end if
-