From 861f69ca2e5cf821555d8809292b68ecd9819302 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 13 Nov 2023 17:13:25 -0500 Subject: [PATCH 1/3] Improved YAML group support --- apprise/config/ConfigBase.py | 53 +++++++++++++++++++++++++++--------- test/test_apprise_config.py | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/apprise/config/ConfigBase.py b/apprise/config/ConfigBase.py index adddc4f56c..1c60a850ae 100644 --- a/apprise/config/ConfigBase.py +++ b/apprise/config/ConfigBase.py @@ -898,19 +898,11 @@ def config_parse_yaml(content, asset=None): # groups root directive # groups = result.get('groups', None) - if not isinstance(groups, (list, tuple)): - # Not a problem; we simply have no group entry - groups = list() - - # Iterate over each group defined and store it - for no, entry in enumerate(groups): - if not isinstance(entry, dict): - ConfigBase.logger.warning( - 'No assignment for group {}, entry #{}'.format( - entry, no + 1)) - continue - - for _groups, tags in entry.items(): + if isinstance(groups, dict): + # + # Dictionary + # + for no, (_groups, tags) in enumerate(groups.items()): for group in parse_list(_groups, cast=str): if isinstance(tags, (list, tuple)): _tags = set() @@ -932,6 +924,41 @@ def config_parse_yaml(content, asset=None): else: group_tags[group] |= tags + elif isinstance(groups, (list, tuple)): + # + # List of Dictionaries + # + + # Iterate over each group defined and store it + for no, entry in enumerate(groups): + if not isinstance(entry, dict): + ConfigBase.logger.warning( + 'No assignment for group {}, entry #{}'.format( + entry, no + 1)) + continue + + for _groups, tags in entry.items(): + for group in parse_list(_groups, cast=str): + if isinstance(tags, (list, tuple)): + _tags = set() + for e in tags: + if isinstance(e, dict): + _tags |= set(e.keys()) + else: + _tags |= set(parse_list(e, cast=str)) + + # Final assignment + tags = _tags + + else: + tags = set(parse_list(tags, cast=str)) + + if group not in group_tags: + group_tags[group] = tags + + else: + group_tags[group] |= tags + # # include root directive # diff --git a/test/test_apprise_config.py b/test/test_apprise_config.py index a78300b494..a2910775b1 100644 --- a/test/test_apprise_config.py +++ b/test/test_apprise_config.py @@ -1250,6 +1250,57 @@ def test_config_base_parse_yaml_file04(tmpdir): assert sum(1 for _ in a.find('test1, test3')) == 2 +def test_config_base_parse_yaml_file05(tmpdir): + """ + API: ConfigBase.parse_yaml_file (#5) + + Test groups + + """ + t = tmpdir.mkdir("always-keyword").join("apprise.yml") + t.write(""" + version: 1 + groups: + mygroup: user1, user2 + + urls: + - json:///user:pass@localhost: + - to: user1@example.com + tag: user1 + - to: user2@example.com + tag: user2 + """) + + # Create ourselves a config object + ac = AppriseConfig(paths=str(t)) + + # The number of configuration files that exist + assert len(ac) == 1 + + # no notifications are loaded + assert len(ac.servers()) == 2 + + # Test our ability to add Config objects to our apprise object + a = Apprise() + + # Add our configuration object + assert a.add(servers=ac) is True + + # Detect our 3 entry as they should have loaded successfully + assert len(a) == 2 + + # No match + assert sum(1 for _ in a.find('no-match')) == 0 + # Match everything + assert sum(1 for _ in a.find('all')) == 2 + # Match user1 entry + assert sum(1 for _ in a.find('user1')) == 1 + # Match user2 entry + assert sum(1 for _ in a.find('user2')) == 1 + # Match mygroup + assert sum(1 for _ in a.find('mygroup')) == 2 + + def test_apprise_config_template_parse(tmpdir): """ API: AppriseConfig parsing of templates From 493f4dba8f6cccc55c0a30ec544c5af5e7534a80 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 13 Nov 2023 19:13:27 -0500 Subject: [PATCH 2/3] Added dict support --- apprise/config/ConfigBase.py | 3 +- test/test_apprise_config.py | 51 ------------- test/test_config_base.py | 142 ++++++++++++++++++++++++++++++++--- 3 files changed, 132 insertions(+), 64 deletions(-) diff --git a/apprise/config/ConfigBase.py b/apprise/config/ConfigBase.py index 1c60a850ae..d907d92c06 100644 --- a/apprise/config/ConfigBase.py +++ b/apprise/config/ConfigBase.py @@ -902,7 +902,7 @@ def config_parse_yaml(content, asset=None): # # Dictionary # - for no, (_groups, tags) in enumerate(groups.items()): + for _groups, tags in groups.items(): for group in parse_list(_groups, cast=str): if isinstance(tags, (list, tuple)): _tags = set() @@ -959,7 +959,6 @@ def config_parse_yaml(content, asset=None): else: group_tags[group] |= tags - # # include root directive # includes = result.get('include', None) diff --git a/test/test_apprise_config.py b/test/test_apprise_config.py index a2910775b1..a78300b494 100644 --- a/test/test_apprise_config.py +++ b/test/test_apprise_config.py @@ -1250,57 +1250,6 @@ def test_config_base_parse_yaml_file04(tmpdir): assert sum(1 for _ in a.find('test1, test3')) == 2 -def test_config_base_parse_yaml_file05(tmpdir): - """ - API: ConfigBase.parse_yaml_file (#5) - - Test groups - - """ - t = tmpdir.mkdir("always-keyword").join("apprise.yml") - t.write(""" - version: 1 - groups: - mygroup: user1, user2 - - urls: - - json:///user:pass@localhost: - - to: user1@example.com - tag: user1 - - to: user2@example.com - tag: user2 - """) - - # Create ourselves a config object - ac = AppriseConfig(paths=str(t)) - - # The number of configuration files that exist - assert len(ac) == 1 - - # no notifications are loaded - assert len(ac.servers()) == 2 - - # Test our ability to add Config objects to our apprise object - a = Apprise() - - # Add our configuration object - assert a.add(servers=ac) is True - - # Detect our 3 entry as they should have loaded successfully - assert len(a) == 2 - - # No match - assert sum(1 for _ in a.find('no-match')) == 0 - # Match everything - assert sum(1 for _ in a.find('all')) == 2 - # Match user1 entry - assert sum(1 for _ in a.find('user1')) == 1 - # Match user2 entry - assert sum(1 for _ in a.find('user2')) == 1 - # Match mygroup - assert sum(1 for _ in a.find('mygroup')) == 2 - - def test_apprise_config_template_parse(tmpdir): """ API: AppriseConfig parsing of templates diff --git a/test/test_config_base.py b/test/test_config_base.py index 819c16cdb6..2b234dbdab 100644 --- a/test/test_config_base.py +++ b/test/test_config_base.py @@ -191,7 +191,7 @@ def test_config_base_config_parse(): - tag: devops, admin """, asset=AppriseAsset()) - # We expect to parse 3 entries from the above + # We expect to parse 2 entries from the above assert isinstance(result, tuple) assert len(result) == 2 assert isinstance(result[0], list) @@ -263,7 +263,7 @@ def test_config_base_config_parse_text(): # A relative include statement (with trailing spaces) include apprise.cfg """, asset=AppriseAsset()) - # We expect to parse 3 entries from the above + # We expect to parse 4 entries from the above assert isinstance(result, list) assert isinstance(config, list) assert len(result) == 4 @@ -409,7 +409,7 @@ def test_config_base_config_tag_groups_text(): """, asset=AppriseAsset()) - # We expect to parse 3 entries from the above + # We expect to parse 4 entries from the above assert isinstance(result, list) assert isinstance(config, list) assert len(result) == 4 @@ -442,7 +442,7 @@ def test_config_base_config_tag_groups_text(): groupD=form://localhost """) - # We expect to parse 3 entries from the above + # We expect to parse 0 entries from the above assert isinstance(result, list) assert isinstance(config, list) assert len(result) == 0 @@ -827,7 +827,7 @@ def test_config_base_config_parse_yaml(): - dbus:// """, asset=asset) - # We expect to parse 3 entries from the above + # We expect to parse 2 entries from the above assert isinstance(result, list) assert len(result) == 2 @@ -861,7 +861,7 @@ def test_config_base_config_parse_yaml(): assert 'admin' in entry.tags assert 'devops' in entry.tags - # We expect to parse 3 entries from the above + # We expect to parse 2 entries from the above assert isinstance(result, list) assert len(result) == 2 @@ -886,7 +886,7 @@ def test_config_base_config_parse_yaml(): - entry """, asset=asset) - # We expect to parse 3 entries from the above + # We expect to parse 0 entries from the above assert isinstance(result, list) assert len(result) == 0 @@ -932,7 +932,7 @@ def test_config_base_config_parse_yaml(): - json://localhost: """, asset=asset) - # We expect to parse 3 entries from the above + # We expect to parse 1 entries from the above assert isinstance(result, list) assert len(result) == 1 @@ -1155,9 +1155,9 @@ def test_yaml_vs_text_tagging(): assert 'mytag' in yaml_result[0] -def test_config_base_config_tag_groups_yaml(): +def test_config_base_config_tag_groups_yaml_01(): """ - API: ConfigBase.config_tag_groups_yaml object + API: ConfigBase.config_tag_groups_yaml #1 object """ @@ -1233,7 +1233,7 @@ def test_config_base_config_tag_groups_yaml(): """, asset=asset) - # We expect to parse 3 entries from the above + # We expect to parse 4 entries from the above assert isinstance(result, list) assert isinstance(config, list) assert len(result) == 4 @@ -1261,11 +1261,131 @@ def test_config_base_config_tag_groups_yaml(): assert len([x for x in apobj.find('group3')]) == 2 assert len([x for x in apobj.find('group4')]) == 0 assert len([x for x in apobj.find('group5')]) == 0 + # json:// -- group6 -> 4 -> TagA + # xml:// -- group6 -> TagC assert len([x for x in apobj.find('group6')]) == 2 assert len([x for x in apobj.find('4')]) == 1 assert len([x for x in apobj.find('groupN')]) == 1 +def test_config_base_config_tag_groups_yaml_02(): + """ + API: ConfigBase.config_tag_groups_yaml #2 object + + """ + + # general reference used below + asset = AppriseAsset() + + # Valid Configuration + result, config = ConfigBase.config_parse_yaml(""" +# if no version is specified then version 1 is presumed +version: 1 + +groups: + group1: tagB, tagC, tagNotAssigned + group2: + - tagA + - tagC + group3: + - tagD: optional comment + - tagA: optional comment #2 + + # No assignment type 2 + group5: + + # Integer assignment (since it's not a list, the last element prevails + # and replaces the above); '4' does not get appended as it would in + # the event this was a list instead + group6: 3 + group6: 3, 4, 5, test + group6: 3.5, tagC + + # Recursion + groupA: groupB + groupB: groupA + # And Again... (just because) + groupA: groupB + groupB: groupA + + # Self assignment + groupX: groupX + + # Set up a larger recursive loop + groupG: groupH + groupH: groupI, groupJ + groupI: groupJ, groupG + groupJ: groupK, groupH, groupI + groupK: groupG + + # No tags assigned + groupK: ",, , ," + " , ": ",, , ," + + # Multi Assignments + groupL, groupM: tagD, tagA + 4, groupN: + - tagD + - tagE, TagA + + # Add one more tag to groupL making it different then GroupM by 1 + groupL: tagB +# +# Define your notification urls: +# +urls: + - form://localhost: + - tag: tagA + - mailto://test:password@gmail.com: + - tag: tagB + - xml://localhost: + - tag: tagC + - json://localhost: + - tag: tagD, tagA + +""", asset=asset) + + # We expect to parse 4 entries from the above + assert isinstance(result, list) + assert isinstance(config, list) + assert len(result) == 4 + + # Our first element is our group tags + assert len(result[0].tags) == 5 + assert 'group2' in result[0].tags + assert 'group3' in result[0].tags + assert 'groupL' in result[0].tags + assert 'groupM' in result[0].tags + assert 'tagA' in result[0].tags + + # No additional configuration is loaded + assert len(config) == 0 + + apobj = Apprise() + assert apobj.add(result) + # We match against 1 entry + assert len([x for x in apobj.find('tagA')]) == 2 + assert len([x for x in apobj.find('tagB')]) == 1 + assert len([x for x in apobj.find('tagC')]) == 1 + assert len([x for x in apobj.find('tagD')]) == 1 + assert len([x for x in apobj.find('group1')]) == 2 + assert len([x for x in apobj.find('group2')]) == 3 + assert len([x for x in apobj.find('group3')]) == 2 + assert len([x for x in apobj.find('group4')]) == 0 + assert len([x for x in apobj.find('group5')]) == 0 + # NOT json:// -- group6 -> 4 -> TagA (not appended because dict storage) + # ^ + # | + # See: test_config_base_config_tag_groups_yaml_01 (above) + # dict storage (as this tests for) causes last entry to + # prevail; previous assignments are lost + # + # xml:// -- group6 -> TagC + assert len([x for x in apobj.find('group6')]) == 1 + assert len([x for x in apobj.find('4')]) == 1 + assert len([x for x in apobj.find('groupN')]) == 1 + + def test_config_base_config_parse_yaml_globals(): """ API: ConfigBase.config_parse_yaml globals From 35d5a8f20493f48c2752d0163f9a43b0ce5927a0 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 13 Nov 2023 19:40:30 -0500 Subject: [PATCH 3/3] bulletproofing --- apprise/config/ConfigBase.py | 6 +++++- test/test_config_base.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apprise/config/ConfigBase.py b/apprise/config/ConfigBase.py index d907d92c06..77c5ce9085 100644 --- a/apprise/config/ConfigBase.py +++ b/apprise/config/ConfigBase.py @@ -393,7 +393,11 @@ def _expand(tags, ignore=None): # Track our groups groups.add(tag) - # Store what we know is worth keping + # Store what we know is worth keeping + if tag not in group_tags: # pragma: no cover + # handle cases where the tag doesn't exist + group_tags[tag] = set() + results |= group_tags[tag] - tag_groups # Get simple tag assignments diff --git a/test/test_config_base.py b/test/test_config_base.py index 2b234dbdab..3004abd0eb 100644 --- a/test/test_config_base.py +++ b/test/test_config_base.py @@ -1384,6 +1384,7 @@ def test_config_base_config_tag_groups_yaml_02(): assert len([x for x in apobj.find('group6')]) == 1 assert len([x for x in apobj.find('4')]) == 1 assert len([x for x in apobj.find('groupN')]) == 1 + assert len([x for x in apobj.find('groupK')]) == 0 def test_config_base_config_parse_yaml_globals():