From 721ef9da709a07a3bd690c8bcf999fdd3bd913ca Mon Sep 17 00:00:00 2001 From: Vladimir Botka Date: Thu, 30 May 2024 14:57:25 +0200 Subject: [PATCH] Add filter keep_keys. Feature #8438. --- .../fragments/8438-feature-keep_keys.yml | 2 + plugins/filter/keep_keys.py | 154 ++++++++++++++++++ .../targets/filter_keep_keys/aliases | 5 + .../filter_keep_keys/tasks/keep_keys.yml | 61 +++++++ .../targets/filter_keep_keys/tasks/main.yml | 7 + .../targets/filter_keep_keys/vars/main.yml | 18 ++ 6 files changed, 247 insertions(+) create mode 100644 changelogs/fragments/8438-feature-keep_keys.yml create mode 100644 plugins/filter/keep_keys.py create mode 100644 tests/integration/targets/filter_keep_keys/aliases create mode 100644 tests/integration/targets/filter_keep_keys/tasks/keep_keys.yml create mode 100644 tests/integration/targets/filter_keep_keys/tasks/main.yml create mode 100644 tests/integration/targets/filter_keep_keys/vars/main.yml diff --git a/changelogs/fragments/8438-feature-keep_keys.yml b/changelogs/fragments/8438-feature-keep_keys.yml new file mode 100644 index 00000000000..ea4e72c4da9 --- /dev/null +++ b/changelogs/fragments/8438-feature-keep_keys.yml @@ -0,0 +1,2 @@ +major_changes: + - Filter keep_keys. diff --git a/plugins/filter/keep_keys.py b/plugins/filter/keep_keys.py new file mode 100644 index 00000000000..14f2e525548 --- /dev/null +++ b/plugins/filter/keep_keys.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Vladimir Botka +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: keep_keys + short_description: Keep specific keys from dictionaries in a list. + version_added: "2.17" + author: Vladimir Botka (@vbotka) + description: This filter keeps only specified keys from a provided list of dictionaries. + options: + _input: + description: A list of dictionaries. + type: list + elements: dictionary + required: true + target: + description: + - A list of keys or keys patterns to keep. + - The interpretation of the list items depends on the option C(matching_parameter) + - For matching_parameter C(regex) only the first item is taken. + type: list + elements: str + required: true + matching_parameter: + description: Specify the matching option of target keys. + type: str + default: equal + choices: + - equal + - starts_with + - ends_with + - regex +''' + +EXAMPLES = ''' + l: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + + # By default match equal keys. + t: [k0_x0, k1_x1] + r: "{{ l | keep_keys(target=t) }}" + + # Match keys that starts with any of the items in the target. + t: [k0, k1] + r: "{{ l | keep_keys(target=t, matching_parameter='starts_with') }}" + + # Match keys that ends with any of the items in target. + t: [x0, x1] + r: "{{ l | keep_keys(target=t, matching_parameter='ends_with') }}" + + # Match keys by the regex. + t: ['^.*[01]_x.*$'] + r: "{{ l | keep_keys(target=t, matching_parameter='regex') }}" + + # Match keys by the regex. The regex does not have to be in list. + t: '^.*[01]_x.*$' + r: "{{ l | keep_keys(target=t, matching_parameter='regex') }}" + + # The results of all examples are all the same. + r: + - {k0_x0: A0, k1_x1: B0} + - {k0_x0: A1, k1_x1: B1} +''' + +RETURN = ''' + _value: + description: The list of dictionaries with selected keys. + type: list + elements: dictionary +''' + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common._collections_compat import Mapping, Sequence + +import re + + +def keep_keys(data, target=[], matching_parameter='equal'): + """keep specific keys from dictionaries in a list""" + + ld = data + tt = target + mp = matching_parameter + ml = ['equal', 'starts_with', 'ends_with', 'regex'] + + if not isinstance(ld, Sequence): + msg = "First argument for keep_keys must be a list. %s is %s" + raise AnsibleFilterError(msg % (ld, type(ld))) + + for elem in ld: + if not isinstance(elem, Mapping): + msg = "Elements of the data list for keep_keys must be dictionaries. %s is %s" + raise AnsibleFilterError(msg % (elem, type(elem))) + + if mp not in ml: + msg = ("The matching_parameter for keep_keys must be one of %s. matching_parameter is %s") + raise AnsibleFilterError(msg % (ml, mp)) + + if mp == 'regex': + if isinstance(target, string_types): + tt = target + elif isinstance(target, Sequence): + tt = target[0] + else: + msg = ("The target for keep_keys must be a string or a list if matching_parameter is regex." + "target is %s.") + raise AnsibleFilterError(msg % target) + try: + re.compile(tt) + is_valid = True + except re.error: + is_valid = False + if not is_valid: + msg = ("The target for keep_keys must be a valid regex if matching_parameter is regex." + "target is %s") + raise AnsibleFilterError(msg % tt) + else: + if not isinstance(tt, Sequence): + msg = ("The target for keep_keys must be a list if matching_parameter is %s. %s is %s") + raise AnsibleFilterError(msg % (mp, tt, type(tt))) + for elem in tt: + if not isinstance(elem, string_types): + msg = "Elements of the targets for keep_keys must be strings. %s is %s" + raise AnsibleFilterError(msg % (elem, type(elem))) + + if mp == 'equal': + my_keys = [[k for k in i.keys() if k in tt] for i in ld] + elif mp == 'starts_with': + my_keys = [[k for k in i.keys() if k.startswith(tuple(tt))] for i in ld] + elif mp == 'ends_with': + my_keys = [[k for k in i.keys() if k.endswith(tuple(tt))] for i in ld] + elif mp == 'regex': + if isinstance(target, string_types): + tt = target + else: + tt = target[0] + my_keys = [[k for k in i.keys() if re.match(tt, k)] for i in ld] + + return [dict([(k, ld[i][k]) for k in j]) for i, j in enumerate(my_keys)] + + +class FilterModule(object): + + def filters(self): + return { + 'keep_keys': keep_keys, + } diff --git a/tests/integration/targets/filter_keep_keys/aliases b/tests/integration/targets/filter_keep_keys/aliases new file mode 100644 index 00000000000..12d1d6617eb --- /dev/null +++ b/tests/integration/targets/filter_keep_keys/aliases @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 diff --git a/tests/integration/targets/filter_keep_keys/tasks/keep_keys.yml b/tests/integration/targets/filter_keep_keys/tasks/keep_keys.yml new file mode 100644 index 00000000000..a959e516a1e --- /dev/null +++ b/tests/integration/targets/filter_keep_keys/tasks/keep_keys.yml @@ -0,0 +1,61 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Debug ansible_version + ansible.builtin.debug: + var: ansible_version + when: not quite_test | d(true) | bool + tags: ansible_version + +- name: Test keep keys equal (default) + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quite_test | d(true) | bool }}" + vars: + rr: "{{ list1 | community.general.keep_keys(target=tt) }}" + tt: [k0_x0, k1_x1] + tags: equal_default + +- name: Test keep keys regex string + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quite_test | d(true) | bool }}" + vars: + rr: "{{ list1 | community.general.keep_keys(target=tt, matching_parameter=mp) }}" + mp: regex + tt: '^.*[01]_x.*$' + tags: regex_string + +- name: Test keep keys matching_parameter list + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quite_test | d(true) | bool }}" + loop: "{{ targets }}" + loop_control: + label: "{{ item.mp }}: {{ item.tt }}" + vars: + rr: "{{ list1 | community.general.keep_keys(target=item.tt, matching_parameter=item.mp) }}" + tags: matching_parameter lists diff --git a/tests/integration/targets/filter_keep_keys/tasks/main.yml b/tests/integration/targets/filter_keep_keys/tasks/main.yml new file mode 100644 index 00000000000..23457d1e118 --- /dev/null +++ b/tests/integration/targets/filter_keep_keys/tasks/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Test keep_keys + import_tasks: keep_keys.yml diff --git a/tests/integration/targets/filter_keep_keys/vars/main.yml b/tests/integration/targets/filter_keep_keys/vars/main.yml new file mode 100644 index 00000000000..b3e91a943f7 --- /dev/null +++ b/tests/integration/targets/filter_keep_keys/vars/main.yml @@ -0,0 +1,18 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +targets: + - {mp: equal, tt: [k0_x0, k1_x1]} + - {mp: starts_with, tt: [k0, k1]} + - {mp: ends_with, tt: [x0, x1]} + - {mp: regex, tt: ['^.*[01]_x.*$']} + +list1: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + +result1: + - {k0_x0: A0, k1_x1: B0} + - {k0_x0: A1, k1_x1: B1}