mirror of https://github.com/caronc/apprise
URLBase() supports calls to url() for generic responses (#973)
parent
902f39cd58
commit
480d0e0bbc
|
@ -228,6 +228,11 @@ class URLBase:
|
||||||
# Always unquote the password if it exists
|
# Always unquote the password if it exists
|
||||||
self.password = URLBase.unquote(self.password)
|
self.password = URLBase.unquote(self.password)
|
||||||
|
|
||||||
|
# Store our full path consistently ensuring it ends with a `/'
|
||||||
|
self.fullpath = URLBase.unquote(kwargs.get('fullpath'))
|
||||||
|
if not isinstance(self.fullpath, str) or not self.fullpath:
|
||||||
|
self.fullpath = '/'
|
||||||
|
|
||||||
# Store our Timeout Variables
|
# Store our Timeout Variables
|
||||||
if 'rto' in kwargs:
|
if 'rto' in kwargs:
|
||||||
try:
|
try:
|
||||||
|
@ -307,7 +312,36 @@ class URLBase:
|
||||||
arguments provied.
|
arguments provied.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("url() is implimented by the child class.")
|
|
||||||
|
# Our default parameters
|
||||||
|
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||||
|
|
||||||
|
# Determine Authentication
|
||||||
|
auth = ''
|
||||||
|
if self.user and self.password:
|
||||||
|
auth = '{user}:{password}@'.format(
|
||||||
|
user=URLBase.quote(self.user, safe=''),
|
||||||
|
password=self.pprint(
|
||||||
|
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||||
|
)
|
||||||
|
elif self.user:
|
||||||
|
auth = '{user}@'.format(
|
||||||
|
user=URLBase.quote(self.user, safe=''),
|
||||||
|
)
|
||||||
|
|
||||||
|
default_port = 443 if self.secure else 80
|
||||||
|
|
||||||
|
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||||
|
schema='https' if self.secure else 'http',
|
||||||
|
auth=auth,
|
||||||
|
# never encode hostname since we're expecting it to be a valid one
|
||||||
|
hostname=self.host,
|
||||||
|
port='' if self.port is None or self.port == default_port
|
||||||
|
else ':{}'.format(self.port),
|
||||||
|
fullpath=URLBase.quote(self.fullpath, safe='/')
|
||||||
|
if self.fullpath else '/',
|
||||||
|
params=URLBase.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
def __contains__(self, tags):
|
def __contains__(self, tags):
|
||||||
"""
|
"""
|
||||||
|
@ -583,6 +617,33 @@ class URLBase:
|
||||||
"""
|
"""
|
||||||
return (self.socket_connect_timeout, self.socket_read_timeout)
|
return (self.socket_connect_timeout, self.socket_read_timeout)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request_auth(self):
|
||||||
|
"""This is primarily used to fullfill the `auth` keyword argument
|
||||||
|
that is used by requests.get() and requests.put() calls.
|
||||||
|
"""
|
||||||
|
return (self.user, self.password) if self.user else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request_url(self):
|
||||||
|
"""
|
||||||
|
Assemble a simple URL that can be used by the requests library
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Acquire our schema
|
||||||
|
schema = 'https' if self.secure else 'http'
|
||||||
|
|
||||||
|
# Prepare our URL
|
||||||
|
url = '%s://%s' % (schema, self.host)
|
||||||
|
|
||||||
|
# Apply Port information if present
|
||||||
|
if isinstance(self.port, int):
|
||||||
|
url += ':%d' % self.port
|
||||||
|
|
||||||
|
# Append our full path
|
||||||
|
return url + self.fullpath
|
||||||
|
|
||||||
def url_parameters(self, *args, **kwargs):
|
def url_parameters(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Provides a default set of args to work with. This can greatly
|
Provides a default set of args to work with. This can greatly
|
||||||
|
|
|
@ -167,10 +167,6 @@ class NotifyAppriseAPI(NotifyBase):
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
|
||||||
if not isinstance(self.fullpath, str):
|
|
||||||
self.fullpath = '/'
|
|
||||||
|
|
||||||
self.token = validate_regex(
|
self.token = validate_regex(
|
||||||
token, *self.template_tokens['token']['regex'])
|
token, *self.template_tokens['token']['regex'])
|
||||||
if not self.token:
|
if not self.token:
|
||||||
|
@ -334,8 +330,8 @@ class NotifyAppriseAPI(NotifyBase):
|
||||||
url += ':%d' % self.port
|
url += ':%d' % self.port
|
||||||
|
|
||||||
fullpath = self.fullpath.strip('/')
|
fullpath = self.fullpath.strip('/')
|
||||||
url += '/{}/'.format(fullpath) if fullpath else '/'
|
url += '{}'.format('/' + fullpath) if fullpath else ''
|
||||||
url += 'notify/{}'.format(self.token)
|
url += '/notify/{}'.format(self.token)
|
||||||
|
|
||||||
# Some entries can not be over-ridden
|
# Some entries can not be over-ridden
|
||||||
headers.update({
|
headers.update({
|
||||||
|
|
|
@ -741,6 +741,49 @@ def test_apprise_schemas(tmpdir):
|
||||||
assert len(schemas) == 0
|
assert len(schemas) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_apprise_urlbase_object():
|
||||||
|
"""
|
||||||
|
API: Apprise() URLBase object testing
|
||||||
|
|
||||||
|
"""
|
||||||
|
results = URLBase.parse_url('https://localhost/path/?cto=3.0&verify=no')
|
||||||
|
assert results.get('user') is None
|
||||||
|
assert results.get('password') is None
|
||||||
|
assert results.get('path') == '/path/'
|
||||||
|
assert results.get('secure') is True
|
||||||
|
assert results.get('verify') is False
|
||||||
|
base = URLBase(**results)
|
||||||
|
assert base.request_timeout == (3.0, 4.0)
|
||||||
|
assert base.request_auth is None
|
||||||
|
assert base.request_url == 'https://localhost/path/'
|
||||||
|
assert base.url().startswith('https://localhost/')
|
||||||
|
|
||||||
|
results = URLBase.parse_url(
|
||||||
|
'http://user:pass@localhost:34/path/here?rto=3.0&verify=yes')
|
||||||
|
assert results.get('user') == 'user'
|
||||||
|
assert results.get('password') == 'pass'
|
||||||
|
assert results.get('fullpath') == '/path/here'
|
||||||
|
assert results.get('secure') is False
|
||||||
|
assert results.get('verify') is True
|
||||||
|
base = URLBase(**results)
|
||||||
|
assert base.request_timeout == (4.0, 3.0)
|
||||||
|
assert base.request_auth == ('user', 'pass')
|
||||||
|
assert base.request_url == 'http://localhost:34/path/here'
|
||||||
|
assert base.url().startswith('http://user:pass@localhost:34/path/here')
|
||||||
|
|
||||||
|
results = URLBase.parse_url('http://user@127.0.0.1/path/')
|
||||||
|
assert results.get('user') == 'user'
|
||||||
|
assert results.get('password') is None
|
||||||
|
assert results.get('fullpath') == '/path/'
|
||||||
|
assert results.get('secure') is False
|
||||||
|
assert results.get('verify') is True
|
||||||
|
base = URLBase(**results)
|
||||||
|
assert base.request_timeout == (4.0, 4.0)
|
||||||
|
assert base.request_auth == ('user', None)
|
||||||
|
assert base.request_url == 'http://127.0.0.1/path/'
|
||||||
|
assert base.url().startswith('http://user@127.0.0.1/path/')
|
||||||
|
|
||||||
|
|
||||||
def test_apprise_notify_formats(tmpdir):
|
def test_apprise_notify_formats(tmpdir):
|
||||||
"""
|
"""
|
||||||
API: Apprise() Input Formats tests
|
API: Apprise() Input Formats tests
|
||||||
|
|
|
@ -70,9 +70,8 @@ def test_attach_base():
|
||||||
# Create an object with no mimetype over-ride
|
# Create an object with no mimetype over-ride
|
||||||
obj = AttachBase()
|
obj = AttachBase()
|
||||||
|
|
||||||
# Get our string object
|
# Get our url object
|
||||||
with pytest.raises(NotImplementedError):
|
str(obj)
|
||||||
str(obj)
|
|
||||||
|
|
||||||
# We can not process name/path/mimetype at a Base level
|
# We can not process name/path/mimetype at a Base level
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
|
|
@ -65,15 +65,8 @@ def test_notify_base():
|
||||||
nb = NotifyBase(port=10)
|
nb = NotifyBase(port=10)
|
||||||
assert nb.port == 10
|
assert nb.port == 10
|
||||||
|
|
||||||
try:
|
assert isinstance(nb.url(), str)
|
||||||
nb.url()
|
assert str(nb) == nb.url()
|
||||||
assert False
|
|
||||||
|
|
||||||
except NotImplementedError:
|
|
||||||
# Each sub-module is that inherits this as a parent is required to
|
|
||||||
# over-ride this function. So direct calls to this throws a not
|
|
||||||
# implemented error intentionally
|
|
||||||
assert True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
nb.send('test message')
|
nb.send('test message')
|
||||||
|
|
|
@ -265,4 +265,10 @@ def test_notify_apprise_api_attachments(mock_post):
|
||||||
body='body', title='title', notify_type=NotifyType.INFO,
|
body='body', title='title', notify_type=NotifyType.INFO,
|
||||||
attach=attach) is True
|
attach=attach) is True
|
||||||
assert mock_post.call_count == 1
|
assert mock_post.call_count == 1
|
||||||
|
|
||||||
|
details = mock_post.call_args_list[0]
|
||||||
|
assert details[0][0] == 'http://localhost/notify/mytoken1'
|
||||||
|
assert obj.url(privacy=False).startswith(
|
||||||
|
'apprise://user@localhost/mytoken1/')
|
||||||
|
|
||||||
mock_post.reset_mock()
|
mock_post.reset_mock()
|
||||||
|
|
|
@ -769,6 +769,9 @@ def test_plugin_matrix_rooms(mock_post, mock_get):
|
||||||
obj._room_cache = {}
|
obj._room_cache = {}
|
||||||
assert obj._room_id('#abc123:localhost') is None
|
assert obj._room_id('#abc123:localhost') is None
|
||||||
|
|
||||||
|
# Force a object removal (thus a logout call)
|
||||||
|
del obj
|
||||||
|
|
||||||
|
|
||||||
def test_plugin_matrix_url_parsing():
|
def test_plugin_matrix_url_parsing():
|
||||||
"""
|
"""
|
||||||
|
@ -840,6 +843,9 @@ def test_plugin_matrix_image_errors(mock_post, mock_get):
|
||||||
# post was okay
|
# post was okay
|
||||||
assert obj.notify('test', 'test') is True
|
assert obj.notify('test', 'test') is True
|
||||||
|
|
||||||
|
# Force a object removal (thus a logout call)
|
||||||
|
del obj
|
||||||
|
|
||||||
def mock_function_handing(url, data, **kwargs):
|
def mock_function_handing(url, data, **kwargs):
|
||||||
"""
|
"""
|
||||||
dummy function for handling image posts (successfully)
|
dummy function for handling image posts (successfully)
|
||||||
|
@ -873,6 +879,9 @@ def test_plugin_matrix_image_errors(mock_post, mock_get):
|
||||||
|
|
||||||
assert obj.notify('test', 'test') is True
|
assert obj.notify('test', 'test') is True
|
||||||
|
|
||||||
|
# Force a object removal (thus a logout call)
|
||||||
|
del obj
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('requests.get')
|
@mock.patch('requests.get')
|
||||||
@mock.patch('requests.post')
|
@mock.patch('requests.post')
|
||||||
|
@ -957,11 +966,14 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get):
|
||||||
# handle a bad response
|
# handle a bad response
|
||||||
bad_response = mock.Mock()
|
bad_response = mock.Mock()
|
||||||
bad_response.status_code = requests.codes.internal_server_error
|
bad_response.status_code = requests.codes.internal_server_error
|
||||||
mock_post.side_effect = [response, bad_response]
|
mock_post.side_effect = [response, bad_response, response]
|
||||||
|
|
||||||
# We'll fail now because of an internal exception
|
# We'll fail now because of an internal exception
|
||||||
assert obj.send(body="test", attach=attach) is False
|
assert obj.send(body="test", attach=attach) is False
|
||||||
|
|
||||||
|
# Force a object removal (thus a logout call)
|
||||||
|
del obj
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('requests.get')
|
@mock.patch('requests.get')
|
||||||
@mock.patch('requests.post')
|
@mock.patch('requests.post')
|
||||||
|
@ -1053,15 +1065,23 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get):
|
||||||
|
|
||||||
# Throw an exception on the first call to requests.post()
|
# Throw an exception on the first call to requests.post()
|
||||||
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
||||||
mock_post.side_effect = [side_effect]
|
# Reset our value
|
||||||
mock_get.side_effect = [side_effect]
|
mock_post.reset_mock()
|
||||||
|
mock_get.reset_mock()
|
||||||
|
|
||||||
|
mock_post.side_effect = [side_effect, response]
|
||||||
|
mock_get.side_effect = [side_effect, response]
|
||||||
|
|
||||||
assert obj.send(body="test", attach=attach) is False
|
assert obj.send(body="test", attach=attach) is False
|
||||||
|
|
||||||
# Throw an exception on the second call to requests.post()
|
# Throw an exception on the second call to requests.post()
|
||||||
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
for side_effect in (requests.RequestException(), OSError(), bad_response):
|
||||||
mock_post.side_effect = [response, side_effect, side_effect]
|
# Reset our value
|
||||||
mock_get.side_effect = [side_effect, side_effect]
|
mock_post.reset_mock()
|
||||||
|
mock_get.reset_mock()
|
||||||
|
|
||||||
|
mock_post.side_effect = [response, side_effect, side_effect, response]
|
||||||
|
mock_get.side_effect = [side_effect, side_effect, response]
|
||||||
|
|
||||||
# We'll fail now because of our error handling
|
# We'll fail now because of our error handling
|
||||||
assert obj.send(body="test", attach=attach) is False
|
assert obj.send(body="test", attach=attach) is False
|
||||||
|
@ -1070,9 +1090,12 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get):
|
||||||
bad_response = mock.Mock()
|
bad_response = mock.Mock()
|
||||||
bad_response.status_code = requests.codes.internal_server_error
|
bad_response.status_code = requests.codes.internal_server_error
|
||||||
mock_post.side_effect = \
|
mock_post.side_effect = \
|
||||||
[response, bad_response, response, response, response]
|
[response, bad_response, response, response, response, response]
|
||||||
mock_get.side_effect = \
|
mock_get.side_effect = \
|
||||||
[response, bad_response, response, response, response]
|
[response, bad_response, response, response, response, response]
|
||||||
|
|
||||||
# We'll fail now because of an internal exception
|
# We'll fail now because of an internal exception
|
||||||
assert obj.send(body="test", attach=attach) is False
|
assert obj.send(body="test", attach=attach) is False
|
||||||
|
|
||||||
|
# Force __del__() call
|
||||||
|
del obj
|
||||||
|
|
Loading…
Reference in New Issue