Fixed argument parsing within YAML files (#404)

This commit is contained in:
Chris Caron 2021-07-28 13:17:16 -04:00 committed by GitHub
parent 8a455695ba
commit ab6b6b51c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 268 additions and 8 deletions

View File

@ -848,7 +848,7 @@ class ConfigBase(URLBase):
# support our special tokens (if they're present)
if schema in plugins.SCHEMA_MAP:
entries = ConfigBase.__extract_special_tokens(
entries = ConfigBase._special_token_handler(
schema, entries)
# Extend our dictionary with our new entries
@ -860,7 +860,7 @@ class ConfigBase(URLBase):
elif isinstance(tokens, dict):
# support our special tokens (if they're present)
if schema in plugins.SCHEMA_MAP:
tokens = ConfigBase.__extract_special_tokens(
tokens = ConfigBase._special_token_handler(
schema, tokens)
# Copy ourselves a template of our parsed URL as a base to
@ -962,7 +962,7 @@ class ConfigBase(URLBase):
return self._cached_servers.pop(index)
@staticmethod
def __extract_special_tokens(schema, tokens):
def _special_token_handler(schema, tokens):
"""
This function takes a list of tokens and updates them to no longer
include any special tokens such as +,-, and :
@ -994,7 +994,7 @@ class ConfigBase(URLBase):
# we're done with this entry
continue
if not isinstance(tokens.get(kw, None), dict):
if not isinstance(tokens.get(kw), dict):
# Invalid; correct it
tokens[kw] = dict()
@ -1005,6 +1005,88 @@ class ConfigBase(URLBase):
# Update our entries
tokens[kw].update(matches)
# Now map our tokens accordingly to the class templates defined by
# each service.
#
# This is specifically used for YAML file parsing. It allows a user to
# define an entry such as:
#
# urls:
# - mailto://user:pass@domain:
# - to: user1@hotmail.com
# - to: user2@hotmail.com
#
# Under the hood, the NotifyEmail() class does not parse the `to`
# argument. It's contents needs to be mapped to `targets`. This is
# defined in the class via the `template_args` and template_tokens`
# section.
#
# This function here allows these mappings to take place within the
# YAML file as independant arguments.
class_templates = \
plugins.details(plugins.SCHEMA_MAP[schema])
for key in list(tokens.keys()):
if key not in class_templates['args']:
# No need to handle non-arg entries
continue
# get our `map_to` and/or 'alias_of' value (if it exists)
map_to = class_templates['args'][key].get(
'alias_of', class_templates['args'][key].get('map_to', ''))
if map_to == key:
# We're already good as we are now
continue
if map_to in class_templates['tokens']:
meta = class_templates['tokens'][map_to]
else:
meta = class_templates['args'].get(
map_to, class_templates['args'][key])
# Perform a translation/mapping if our code reaches here
value = tokens[key]
del tokens[key]
# Detect if we're dealign with a list or not
is_list = re.search(
r'^(list|choice):.*',
meta.get('type'),
re.IGNORECASE)
if map_to not in tokens:
tokens[map_to] = [] if is_list \
else meta.get('default')
elif is_list and not isinstance(tokens.get(map_to), list):
# Convert ourselves to a list if we aren't already
tokens[map_to] = [tokens[map_to]]
# Type Conversion
if re.search(
r'^(choice:)?string',
meta.get('type'),
re.IGNORECASE) \
and not isinstance(value, six.string_types):
# Ensure our format is as expected
value = str(value)
# Apply any further translations if required (absolute map)
# This is the case when an arg maps to a token which further
# maps to a different function arg on the class constructor
abs_map = meta.get('map_to', map_to)
# Set our token as how it was provided by the configuration
if isinstance(tokens.get(map_to), list):
tokens[abs_map].append(value)
else:
tokens[abs_map] = value
# Return our tokens
return tokens

View File

@ -363,10 +363,6 @@ class NotifyEmail(NotifyBase):
'type': 'string',
'map_to': 'from_name',
},
'smtp_host': {
'name': _('SMTP Server'),
'type': 'string',
},
'cc': {
'name': _('Carbon Copy'),
'type': 'list:string',
@ -375,6 +371,11 @@ class NotifyEmail(NotifyBase):
'name': _('Blind Carbon Copy'),
'type': 'list:string',
},
'smtp': {
'name': _('SMTP Server'),
'type': 'string',
'map_to': 'smtp_host',
},
'mode': {
'name': _('Secure Mode'),
'type': 'choice:string',

View File

@ -1171,3 +1171,180 @@ def test_config_base_parse_yaml_file03(tmpdir):
assert sum(1 for _ in a.find('test3')) == 1
# Match test1 or test3 entry; (only matches test3)
assert sum(1 for _ in a.find('test1, test3')) == 1
def test_apprise_config_template_parse(tmpdir):
"""
API: AppriseConfig parsing of templates
"""
# Create ourselves a config object
ac = AppriseConfig()
t = tmpdir.mkdir("template-testing").join("apprise.yml")
t.write("""
tag:
- company
# A comment line over top of a URL
urls:
- mailto://user:pass@example.com:
- to: user1@gmail.com
cc: test@hotmail.com
- to: user2@gmail.com
tag: co-worker
""")
# Create ourselves a config object
ac = AppriseConfig(paths=str(t))
# 2 emails to be sent
assert len(ac.servers()) == 2
# The below checks are very customized for NotifyMail but just
# test that the content got passed correctly
assert (False, 'user1@gmail.com') in ac[0][0].targets
assert 'test@hotmail.com' in ac[0][0].cc
assert 'company' in ac[0][1].tags
assert (False, 'user2@gmail.com') in ac[0][1].targets
assert 'company' in ac[0][1].tags
assert 'co-worker' in ac[0][1].tags
#
# Specifically test _special_token_handler()
#
tokens = {
# This maps to itself (bcc); no change here
'bcc': 'user@test.com',
# This should get mapped to 'targets'
'to': 'user1@abc.com',
# white space and tab is intentionally added to the end to verify we
# do not play/tamper with information
'targets': 'user2@abc.com, user3@abc.com \t',
# If the end user provides a configuration for data we simply don't use
# this isn't a proble... we simply don't touch it either; we leave it
# as is.
'ignore': 'not-used'
}
result = ConfigBase._special_token_handler('mailto', tokens)
# to gets mapped to targets
assert 'to' not in result
# bcc is allowed here
assert 'bcc' in result
assert 'targets' in result
# Not used, but also not touched; this entry should still be in our result
# set
assert 'ignore' in result
# We'll concatinate all of our targets together
assert len(result['targets']) == 2
assert 'user1@abc.com' in result['targets']
# Content is passed as is
assert 'user2@abc.com, user3@abc.com \t' in result['targets']
# We re-do the simmiar test above. The very key difference is the
# `targets` is a list already (it's expected type) so `to` can properly be
# concatinated into the list vs the above (which tries to correct the
# situation)
tokens = {
# This maps to itself (bcc); no change here
'bcc': 'user@test.com',
# This should get mapped to 'targets'
'to': 'user1@abc.com',
# similar to the above test except targets is now a proper
# dictionary allowing the `to` (when translated to `targets`) to get
# appended to it
'targets': ['user2@abc.com', 'user3@abc.com'],
# If the end user provides a configuration for data we simply don't use
# this isn't a proble... we simply don't touch it either; we leave it
# as is.
'ignore': 'not-used'
}
result = ConfigBase._special_token_handler('mailto', tokens)
# to gets mapped to targets
assert 'to' not in result
# bcc is allowed here
assert 'bcc' in result
assert 'targets' in result
# Not used, but also not touched; this entry should still be in our result
# set
assert 'ignore' in result
# Now we'll see the new user added as expected (concatinated into our list)
assert len(result['targets']) == 3
assert 'user1@abc.com' in result['targets']
assert 'user2@abc.com' in result['targets']
assert 'user3@abc.com' in result['targets']
# Test providing a list
t.write("""
# A comment line over top of a URL
urls:
- mailtos://user:pass@example.com:
- smtp: smtp3-dev.google.gmail.com
to:
- John Smith <user1@gmail.com>
- Jason Tater <user2@gmail.com>
- user3@gmail.com
- to: Henry Fisher <user4@gmail.com>, Jason Archie <user5@gmail.com>
smtp_host: smtp5-dev.google.gmail.com
tag: drinking-buddy
# provide case where the URL includes some input too
# In both of these cases, the cc and targets (to) get over-ridden
# by values below
- mailtos://user:pass@example.com/arnold@imdb.com/?cc=bill@micro.com/:
to:
- override01@gmail.com
cc:
- override02@gmail.com
- sinch://:
- spi: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
token: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
# Test a case where we expect a string, but yaml reads it in as
# a number
from: 10005243890
to: +1(123)555-1234
""")
# Create ourselves a config object
ac = AppriseConfig(paths=str(t))
# 2 emails to be sent and 1 Sinch service call
assert len(ac.servers()) == 4
# Verify our users got placed into the to
assert len(ac[0][0].targets) == 3
assert ("John Smith", 'user1@gmail.com') in ac[0][0].targets
assert ("Jason Tater", 'user2@gmail.com') in ac[0][0].targets
assert (False, 'user3@gmail.com') in ac[0][0].targets
assert ac[0][0].smtp_host == 'smtp3-dev.google.gmail.com'
assert len(ac[0][1].targets) == 2
assert ("Henry Fisher", 'user4@gmail.com') in ac[0][1].targets
assert ("Jason Archie", 'user5@gmail.com') in ac[0][1].targets
assert 'drinking-buddy' in ac[0][1].tags
assert ac[0][1].smtp_host == 'smtp5-dev.google.gmail.com'
# Our third test tests cases where some variables are defined inline
# and additional ones are defined below that share the same token space
assert len(ac[0][2].targets) == 1
assert len(ac[0][2].cc) == 1
assert (False, 'override01@gmail.com') in ac[0][2].targets
assert 'override02@gmail.com' in ac[0][2].cc
# Test our Since configuration now:
assert len(ac[0][3].targets) == 1
assert ac[0][3].service_plan_id == 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
assert ac[0][3].source == '+10005243890'
assert ac[0][3].targets[0] == '+11235551234'