Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Filter plugin from_csv #2037

Merged
merged 12 commits into from
Mar 21, 2021
4 changes: 4 additions & 0 deletions changelogs/fragments/2037-add-from-csv-filter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
add plugin.filter:
- name: from_csv
description: Converts csv text input into list of dicts
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions changelogs/fragments/XXXX-add-from-csv-filter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
add plugin.filter:
- name: from_csv
description: Converts csv text input into list of dicts
77 changes: 77 additions & 0 deletions plugins/filter/from_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-

# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type

import csv
from io import BytesIO, StringIO

from ansible.errors import AnsibleFilterError
from ansible.module_utils._text import to_native
from ansible.module_utils.six import PY3


# Add Unix dialect from Python 3
Ajpantuso marked this conversation as resolved.
Show resolved Hide resolved
class unix_dialect(csv.Dialect):
"""Describe the usual properties of Unix-generated CSV files."""
delimiter = ','
quotechar = '"'
doublequote = True
skipinitialspace = False
lineterminator = '\n'
quoting = csv.QUOTE_ALL


csv.register_dialect("unix", unix_dialect)


def from_csv(data, dialect='excel', fieldnames=None, delimiter=None, skipinitialspace=None, strict=None):

if dialect not in csv.list_dialects():
raise AnsibleFilterError("Dialect '%s' is not supported by your version of python." % dialect)

dialect_options = dict(
delimiter=delimiter,
skipinitialspace=skipinitialspace,
strict=strict,
)

# Create a dictionary from only set options
dialect_params = dict((k, v) for k, v in dialect_options.items() if v is not None)
if dialect_params:
try:
csv.register_dialect('custom', dialect, **dialect_params)
except TypeError as e:
raise AnsibleFilterError("Unable to create custom dialect: %s" % to_native(e))
dialect = 'custom'

data = to_native(data, errors='surrogate_or_strict')

if PY3:
fake_fh = StringIO(data)
else:
fake_fh = BytesIO(data)

reader = csv.DictReader(fake_fh, fieldnames=fieldnames, dialect=dialect)

data_list = []

try:
for row in reader:
data_list.append(row)
except csv.Error as e:
raise AnsibleFilterError("Unable to process file: %s" % to_native(e))

return data_list


class FilterModule(object):

def filters(self):
return {
'from_csv': from_csv
}
2 changes: 2 additions & 0 deletions tests/integration/targets/filter_from_csv/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
shippable/posix/group2
skip/python2.6 # filters are controller only, and we no longer support Python 2.6 on the controller
49 changes: 49 additions & 0 deletions tests/integration/targets/filter_from_csv/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################

- name: Parse valid csv input
assert:
that:
- "valid_comma_separated | community.general.from_csv == expected_result"

- name: Parse valid csv input containing spaces with/without skipinitialspace=True
assert:
that:
- "valid_comma_separated_spaces | community.general.from_csv(skipinitialspace=True) == expected_result"
- "valid_comma_separated_spaces | community.general.from_csv != expected_result"

- name: Parse valid csv input with no headers with/without specifiying fieldnames
assert:
that:
- "valid_comma_separated_no_headers | community.general.from_csv(fieldnames=['id','name','role']) == expected_result"
- "valid_comma_separated_no_headers | community.general.from_csv != expected_result"

- name: Parse valid pipe-delimited csv input with/without delimiter=|
assert:
that:
- "valid_pipe_separated | community.general.from_csv(delimiter='|') == expected_result"
- "valid_pipe_separated | community.general.from_csv != expected_result"

- name: Register result of invalid csv input when strict=False
debug:
var: "invalid_comma_separated | community.general.from_csv"
register: _invalid_csv_strict_false

- name: Test invalid csv input when strict=False is successful
assert:
that:
- _invalid_csv_strict_false is success

- name: Register result of invalid csv input when strict=True
debug:
var: "invalid_comma_separated | community.general.from_csv(strict=True)"
register: _invalid_csv_strict_true
ignore_errors: True

- name: Test invalid csv input when strict=True is failed
assert:
that:
- _invalid_csv_strict_true is failed
- _invalid_csv_strict_true.msg is match('Unable to process file:.*')
26 changes: 26 additions & 0 deletions tests/integration/targets/filter_from_csv/vars/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
valid_comma_separated: |
id,name,role
1,foo,bar
2,bar,baz
valid_comma_separated_spaces: |
id,name,role
1, foo, bar
2, bar, baz
valid_comma_separated_no_headers: |
1,foo,bar
2,bar,baz
valid_pipe_separated: |
id|name|role
1|foo|bar
2|bar|baz
invalid_comma_separated: |
id,name,role
1,foo,bar
2,"b"ar",baz
expected_result:
- id: '1'
name: foo
role: bar
- id: '2'
name: bar
role: baz