Source code for sqlathanor.declarative._csv_support

# -*- coding: utf-8 -*-

# The lack of a module docstring for this module is **INTENTIONAL**.
# The module is imported into the documentation using Sphinx's autodoc
# extension, and its member function documentation is automatically incorporated
# there as needed.

import csv

from validator_collection import checkers, validators

from sqlathanor._compat import StringIO, dict as dict_
from sqlathanor.utilities import read_csv_data, get_attribute_names
from sqlathanor.errors import SerializableAttributeError, \
    UnsupportedSerializationError, CSVStructureError, DeserializationError


class CSVSupportMixin(object):
    """Mixin that provides CSV serialization/de-serialization support."""

    @classmethod
    def get_csv_column_names(cls,
                             deserialize = True,
                             serialize = True,
                             config_set = None):
        """Retrieve a list of CSV column names.

        :param deserialize: If ``True``, returns columns that support
          :term:`de-serialization`. If ``False``, returns columns that do *not*
          support deserialization. If :obj:`None <python:None>`, does not take
          deserialization into account. Defaults to ``True``.
        :type deserialize: :class:`bool <python:bool>`

        :param serialize: If ``True``, returns columns that support
          :term:`serialization`. If ``False``, returns columns that do *not*
          support serialization. If :obj:`None <python:None>`, does not take
          serialization into account. Defaults to ``True``.
        :type serialize: :class:`bool <python:bool>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :returns: List of CSV column names, sorted according to their configuration.
        :rtype: :class:`list <python:list>` of :class:`str <python:str>`
        """
        config = cls.get_csv_serialization_config(deserialize = deserialize,
                                                  serialize = serialize,
                                                  config_set = config_set)
        attribute_names = [x.name for x in config]
        display_names = [x.display_name for x in config]

        return [x[0] or x[1] for x in zip(display_names, attribute_names)]

    @classmethod
    def _get_csv_attribute_names(cls,
                                 deserialize = True,
                                 serialize = True,
                                 config_set = None):
        """Retrieve a list of the attribute names that are to be serialized to CSV.

        :param deserialize: If ``True``, returns columns that support
          :term:`de-serialization`. If ``False``, returns columns that do *not*
          support deserialization. If :obj:`None <python:None>`, does not take
          deserialization into account. Defaults to ``True``.
        :type deserialize: :class:`bool <python:bool>`

        :param serialize: If ``True``, returns columns that support
          :term:`serialization`. If ``False``, returns columns that do *not*
          support serialization. If :obj:`None <python:None>`, does not take
          serialization into account. Defaults to ``True``.
        :type serialize: :class:`bool <python:bool>`

        :returns: List of attribute names, sorted according to their configuration.
        :rtype: :class:`list <python:list>` of :class:`str <python:str>`
        """
        config = cls.get_csv_serialization_config(deserialize = deserialize,
                                                  serialize = serialize,
                                                  config_set = config_set)
        return [x.name for x in config]

    @classmethod
    def _get_attribute_csv_header(cls,
                                  attributes,
                                  delimiter = '|',
                                  wrap_all_strings = False,
                                  wrapper_character = "'",
                                  double_wrapper_character_when_nested = False,
                                  escape_character = "\\",
                                  line_terminator = '\r\n'):
        r"""Retrieve a header string for a CSV representation of the model.

        :param attributes: List of :term:`model attributes <model attribute>` to
          include.
        :type attributes: :class:`list <python:list>` of :class:`str <python:str>`

        :param delimiter: The character(s) to utilize between columns. Defaults to
          a pipe (``|``).
        :type delimiter: :class:`str <python:str>`

        :param wrap_all_strings: If ``True``, wraps any string data in the
          ``wrapper_character``. If ``None``, only wraps string data if it contains
          the ``delimiter``. Defaults to ``False``.
        :type wrap_all_strings: :class:`bool <python:bool>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is necessary. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param double_wrapper_character_when_nested: If ``True``, will double the
          ``wrapper_character`` when it is found inside a column value. If ``False``,
          will precede the ``wrapper_character`` by the ``escape_character`` when
          it is found inside a column value. Defaults to ``False``.
        :type double_wrapper_character_when_nested: :class:`bool <python:bool>`

        :param escape_character: The character to use when escaping nested wrapper
          characters. Defaults to ``\``.
        :type escape_character: :class:`str <python:str>`

        :param line_terminator: The character used to mark the end of a line.
          Defaults to ``\r\n``.
        :type line_terminator: :class:`str <python:str>`

        :returns: A string ending in ``line_terminator`` with the model's CSV column names
          listed, separated by the ``delimiter``.
        :rtype: :class:`str <python:str>`
        """
        if not wrapper_character:
            wrapper_character = '\''

        if wrap_all_strings:
            quoting = csv.QUOTE_NONNUMERIC
        else:
            quoting = csv.QUOTE_MINIMAL

        if 'sqlathanor' in csv.list_dialects():
            csv.unregister_dialect('sqlathanor')

        csv.register_dialect('sqlathanor',
                             delimiter = delimiter,
                             doublequote = double_wrapper_character_when_nested,
                             escapechar = escape_character,
                             quotechar = wrapper_character,
                             quoting = quoting,
                             lineterminator = line_terminator)

        output = StringIO()
        csv_writer = csv.DictWriter(output,
                                    fieldnames = attributes,
                                    dialect = 'sqlathanor')

        csv_writer.writeheader()

        header_string = output.getvalue()
        output.close()

        csv.unregister_dialect('sqlathanor')

        return header_string

    def _get_attribute_csv_data(self,
                                attributes,
                                is_dumping = False,
                                delimiter = '|',
                                wrap_all_strings = False,
                                null_text = 'None',
                                wrapper_character = "'",
                                double_wrapper_character_when_nested = False,
                                escape_character = "\\",
                                line_terminator = '\r\n',
                                config_set = None):
        r"""Return the CSV representation of ``attributes`` extracted from the
        model instance (record).

        :param attributes: Names of :term:`model attributes <model attribute>` to
          include in the CSV output.
        :type attributes: :class:`list <python:list>` of :class:`str <python:str>`

        :param is_dumping: If ``True``, then allow
          :exc:`UnsupportedSerializationError <sqlathanor.errors.UnsupportedSerializationError>`.
          Defaults to ``False``.
        :type is_dumping: :class:`bool <python:bool>`

        :param delimiter: The delimiter used between columns. Defaults to ``|``.
        :type delimiter: :class:`str <python:str>`

        :param wrap_all_strings: If ``True``, wraps any string data in the
          ``wrapper_character``. If ``None``, only wraps string data if it contains
          the ``delimiter``. Defaults to ``False``.
        :type wrap_all_strings: :class:`bool <python:bool>`

        :param null_text: The text value to use in place of empty values. Only
          applies if ``wrap_empty_values`` is ``True``. Defaults to ``'None'``.
        :type null_text: :class:`str <python:str>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is necessary. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param double_wrapper_character_when_nested: If ``True``, will double the
          ``wrapper_character`` when it is found inside a column value. If ``False``,
          will precede the ``wrapper_character`` by the ``escape_character`` when
          it is found inside a column value. Defaults to ``False``.
        :type double_wrapper_character_when_nested: :class:`bool <python:bool>`

        :param escape_character: The character to use when escaping nested wrapper
          characters. Defaults to ``\``.
        :type escape_character: :class:`str <python:str>`

        :param line_terminator: The character used to mark the end of a line.
          Defaults to ``\r\n``.
        :type line_terminator: :class:`str <python:str>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :returns: Data from the object in CSV format ending in ``line_terminator``.
        :rtype: :class:`str <python:str>`
        """
        if not wrapper_character:
            wrapper_character = '\''

        if not attributes:
            raise SerializableAttributeError("attributes cannot be empty")

        if wrap_all_strings:
            quoting = csv.QUOTE_NONNUMERIC
        else:
            quoting = csv.QUOTE_MINIMAL

        if 'sqlathanor' in csv.list_dialects():
            csv.unregister_dialect('sqlathanor')

        csv.register_dialect('sqlathanor',
                             delimiter = delimiter,
                             doublequote = double_wrapper_character_when_nested,
                             escapechar = escape_character,
                             quotechar = wrapper_character,
                             quoting = quoting,
                             lineterminator = line_terminator)

        data = []
        for item in attributes:
            try:
                value = self._get_serialized_value(format = 'csv',
                                                   attribute = item,
                                                   config_set = config_set)
            except UnsupportedSerializationError as error:
                if is_dumping:
                    value = getattr(self, item)
                else:
                    raise error

            data.append(value)

        for index, item in enumerate(data):
            if item == '' or item is None or item == 'None':
                data[index] = null_text
            elif not checkers.is_string(item) and not checkers.is_numeric(item):
                data[index] = str(item)

        data_dict = dict_()
        for index, column_name in enumerate(attributes):
            data_dict[column_name] = data[index]

        output = StringIO()
        csv_writer = csv.DictWriter(output,
                                    fieldnames = attributes,
                                    dialect = 'sqlathanor')


        csv_writer.writerow(data_dict)

        data_row = output.getvalue()
        output.close()

        csv.unregister_dialect('sqlathanor')

        return data_row

    @classmethod
    def get_csv_header(cls,
                       deserialize = None,
                       serialize = True,
                       delimiter = '|',
                       wrap_all_strings = False,
                       wrapper_character = "'",
                       double_wrapper_character_when_nested = False,
                       escape_character = "\\",
                       line_terminator = '\r\n',
                       config_set = None):
        r"""Retrieve a header string for a CSV representation of the model.

        :param attributes: List of :term:`model attributes <model attribute>` to
          include.
        :type attributes: :class:`list <python:list>` of :class:`str <python:str>`

        :param delimiter: The character(s) to utilize between columns. Defaults to
          a pipe (``|``).
        :type delimiter: :class:`str <python:str>`

        :param wrap_all_strings: If ``True``, wraps any string data in the
          ``wrapper_character``. If ``None``, only wraps string data if it contains
          the ``delimiter``. Defaults to ``False``.
        :type wrap_all_strings: :class:`bool <python:bool>`

        :param null_text: The text value to use in place of empty values. Only
          applies if ``wrap_empty_values`` is ``True``. Defaults to ``'None'``.
        :type null_text: :class:`str <python:str>`

        :param null_text: The text value to use in place of empty values. Only
          applies if ``wrap_empty_values`` is ``True``. Defaults to ``'None'``.
        :type null_text: :class:`str <python:str>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is necessary. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param double_wrapper_character_when_nested: If ``True``, will double the
          ``wrapper_character`` when it is found inside a column value. If ``False``,
          will precede the ``wrapper_character`` by the ``escape_character`` when
          it is found inside a column value. Defaults to ``False``.
        :type double_wrapper_character_when_nested: :class:`bool <python:bool>`

        :param escape_character: The character to use when escaping nested wrapper
          characters. Defaults to ``\``.
        :type escape_character: :class:`str <python:str>`

        :param line_terminator: The character used to mark the end of a line.
          Defaults to ``\r\n``.
        :type line_terminator: :class:`str <python:str>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :returns: A string ending in ``line_terminator`` with the model's CSV column names
          listed, separated by the ``delimiter``.
        :rtype: :class:`str <python:str>`
        """
        # pylint: disable=line-too-long

        column_names = cls.get_csv_column_names(deserialize = deserialize,
                                                serialize = serialize,
                                                config_set = config_set)

        header_string = cls._get_attribute_csv_header(column_names,
                                                      delimiter = delimiter,
                                                      wrap_all_strings = wrap_all_strings,
                                                      wrapper_character = wrapper_character,
                                                      double_wrapper_character_when_nested = double_wrapper_character_when_nested,
                                                      escape_character = escape_character,
                                                      line_terminator = line_terminator)
        return header_string

    def get_csv_data(self,
                     delimiter = '|',
                     wrap_all_strings = False,
                     null_text = 'None',
                     wrapper_character = "'",
                     double_wrapper_character_when_nested = False,
                     escape_character = "\\",
                     line_terminator = '\r\n',
                     config_set = None):
        r"""Return the CSV representation of the model instance (record).

        :param delimiter: The delimiter used between columns. Defaults to ``|``.
        :type delimiter: :class:`str <python:str>`

        :param wrap_all_strings: If ``True``, wraps any string data in the
          ``wrapper_character``. If ``None``, only wraps string data if it contains
          the ``delimiter``. Defaults to ``False``.
        :type wrap_all_strings: :class:`bool <python:bool>`

        :param null_text: The text value to use in place of empty values. Only
          applies if ``wrap_empty_values`` is ``True``. Defaults to ``'None'``.
        :type null_text: :class:`str <python:str>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is necessary. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param double_wrapper_character_when_nested: If ``True``, will double the
          ``wrapper_character`` when it is found inside a column value. If ``False``,
          will precede the ``wrapper_character`` by the ``escape_character`` when
          it is found inside a column value. Defaults to ``False``.
        :type double_wrapper_character_when_nested: :class:`bool <python:bool>`

        :param escape_character: The character to use when escaping nested wrapper
          characters. Defaults to ``\``.
        :type escape_character: :class:`str <python:str>`

        :param line_terminator: The character used to mark the end of a line.
          Defaults to ``\r\n``.
        :type line_terminator: :class:`str <python:str>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :returns: Data from the object in CSV format ending in ``line_terminator``.
        :rtype: :class:`str <python:str>`
        """
        # pylint: disable=line-too-long
        csv_attribute_names = [x
                               for x in self._get_csv_attribute_names(deserialize = None,
                                                                      serialize = True,
                                                                      config_set = config_set)
                               if hasattr(self, x)]

        if not csv_attribute_names:
            raise SerializableAttributeError("no 'csv' serializable attributes found")

        data_row = self._get_attribute_csv_data(csv_attribute_names,
                                                is_dumping = False,
                                                delimiter = delimiter,
                                                wrap_all_strings = wrap_all_strings,
                                                null_text = null_text,
                                                wrapper_character = wrapper_character,
                                                double_wrapper_character_when_nested = double_wrapper_character_when_nested,
                                                escape_character = escape_character,
                                                line_terminator = line_terminator,
                                                config_set = config_set)

        return data_row

    def to_csv(self,
               include_header = False,
               delimiter = '|',
               wrap_all_strings = False,
               null_text = 'None',
               wrapper_character = "'",
               double_wrapper_character_when_nested = False,
               escape_character = "\\",
               line_terminator = '\r\n',
               config_set = None):
        r"""Retrieve a CSV string with the object's data.

        :param include_header: If ``True``, will include a header row with column
          labels. If ``False``, will not include a header row. Defaults to ``True``.
        :type include_header: :class:`bool <python:bool>`

        :param delimiter: The delimiter used between columns. Defaults to ``|``.
        :type delimiter: :class:`str <python:str>`

        :param wrap_all_strings: If ``True``, wraps any string data in the
          ``wrapper_character``. If ``None``, only wraps string data if it contains
          the ``delimiter``. Defaults to ``False``.
        :type wrap_all_strings: :class:`bool <python:bool>`

        :param null_text: The text value to use in place of empty values. Only
          applies if ``wrap_empty_values`` is ``True``. Defaults to ``'None'``.
        :type null_text: :class:`str <python:str>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is necessary. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param double_wrapper_character_when_nested: If ``True``, will double the
          ``wrapper_character`` when it is found inside a column value. If ``False``,
          will precede the ``wrapper_character`` by the ``escape_character`` when
          it is found inside a column value. Defaults to ``False``.
        :type double_wrapper_character_when_nested: :class:`bool <python:bool>`

        :param escape_character: The character to use when escaping nested wrapper
          characters. Defaults to ``\``.
        :type escape_character: :class:`str <python:str>`

        :param line_terminator: The character used to mark the end of a line.
          Defaults to ``\r\n``.
        :type line_terminator: :class:`str <python:str>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :returns: Data from the object in CSV format ending in a newline (``\n``).
        :rtype: :class:`str <python:str>`
        """
        if include_header:
            return self.get_csv_header(delimiter = delimiter,
                                       config_set = config_set) + \
                   self.get_csv_data(delimiter = delimiter,
                                     wrap_all_strings = wrap_all_strings,
                                     null_text = null_text,
                                     wrapper_character = wrapper_character,
                                     double_wrapper_character_when_nested = double_wrapper_character_when_nested,     # pylint: disable=line-too-long
                                     escape_character = escape_character,
                                     line_terminator = line_terminator,
                                     config_set = config_set)


        return self.get_csv_data(delimiter = delimiter,
                                 wrap_all_strings = wrap_all_strings,
                                 null_text = null_text,
                                 wrapper_character = wrapper_character,
                                 double_wrapper_character_when_nested = double_wrapper_character_when_nested,     # pylint: disable=line-too-long
                                 escape_character = escape_character,
                                 line_terminator = line_terminator,
                                 config_set = config_set)

    @classmethod
    def _parse_csv(cls,
                   csv_data,
                   delimiter = '|',
                   wrap_all_strings = False,
                   null_text = 'None',
                   wrapper_character = "'",
                   double_wrapper_character_when_nested = False,
                   escape_character = "\\",
                   line_terminator = '\r\n',
                   config_set = None):
        """Generate a :class:`dict <python:dict>` from a CSV record.

        .. tip::

          Unwrapped empty column values are automatically interpreted as null
          (:obj:`None <python:None>`).

        :param csv_data: The CSV record. Should be a single row and should **not**
          include column headers.
        :type csv_data: :class:`str <python:str>`

        :param delimiter: The delimiter used between columns. Defaults to ``|``.
        :type delimiter: :class:`str <python:str>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is applied. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param null_text: The string used to indicate an empty value if empty
          values are wrapped. Defaults to `None`.
        :type null_text: :class:`str <python:str>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :returns: A :class:`dict <python:dict>` representation of the CSV record.
        :rtype: :class:`dict <python:dict>`

        :raises DeserializationError: if ``csv_data`` is not a valid
          :class:`str <python:str>`
        :raises CSVStructureError: if the columns in ``csv_data`` do not match
          the expected columns returned by
          :func:`get_csv_column_names() <BaseModel.get_csv_column_names>`
        :raises ValueDeserializationError: if a value extracted from the CSV
          failed when executing its :term:`de-serialization function`.

        """
        try:
            csv_data = validators.string(csv_data, allow_empty = False)
        except (ValueError, TypeError):
            raise DeserializationError("csv_data expects a 'str', received '%s'" \
                                       % type(csv_data))

        if not wrapper_character:
            wrapper_character = '\''

        if wrap_all_strings:
            quoting = csv.QUOTE_NONNUMERIC
        else:
            quoting = csv.QUOTE_MINIMAL

        if 'sqlathanor' in csv.list_dialects():
            csv.unregister_dialect('sqlathanor')

        csv.register_dialect('sqlathanor',
                             delimiter = delimiter,
                             doublequote = double_wrapper_character_when_nested,
                             escapechar = escape_character,
                             quotechar = wrapper_character,
                             quoting = quoting,
                             lineterminator = line_terminator)

        csv_column_names = [x
                            for x in cls.get_csv_column_names(deserialize = True,
                                                              serialize = None,
                                                              config_set = config_set)]

        csv_reader = csv.DictReader([csv_data],
                                    fieldnames = csv_column_names,
                                    dialect = 'sqlathanor',
                                    restkey = None,
                                    restval = None)

        rows = [x for x in csv_reader]

        if len(rows) > 1:
            raise CSVStructureError('expected 1 row of data, received %s' % len(csv_reader))
        elif len(rows) == 0:
            data = dict_()
            for column_name in csv_column_names:
                data[column_name] = None
        else:
            data = rows[0]

        if data.get(None, None) is not None:
            raise CSVStructureError('expected %s fields, found %s' % (len(csv_column_names),
                                                                      len(data.keys())))

        deserialized_data = dict_()
        for key in data:
            if data[key] == null_text:
                deserialized_data[key] = None
                continue

            attribute_name = cls._get_attribute_name(key)
            deserialized_value = cls._get_deserialized_value(data[key],
                                                             'csv',
                                                             key,
                                                             config_set = config_set)

            deserialized_data[attribute_name] = deserialized_value

        csv.unregister_dialect('sqlathanor')

        return deserialized_data

    def update_from_csv(self,
                        csv_data,
                        delimiter = '|',
                        wrap_all_strings = False,
                        null_text = 'None',
                        wrapper_character = "'",
                        double_wrapper_character_when_nested = False,
                        escape_character = "\\",
                        line_terminator = '\r\n',
                        config_set = None):
        """Update the model instance from a CSV record.

        .. tip::

          Unwrapped empty column values are automatically interpreted as null
          (:obj:`None <python:None>`).

        :param csv_data: The CSV data. If a Path-like object, will read the first
          record from a file that is assumed to include a header row. If a
          :class:`str <python:str>` and has more than one record (line), will assume
          the first line is a header row.
        :type csv_data: :class:`str <python:str>` / Path-like object

        :param delimiter: The delimiter used between columns. Defaults to ``|``.
        :type delimiter: :class:`str <python:str>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is applied. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param null_text: The string used to indicate an empty value if empty
          values are wrapped. Defaults to `None`.
        :type null_text: :class:`str <python:str>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :raises DeserializationError: if ``csv_data`` is not a valid
          :class:`str <python:str>`
        :raises CSVStructureError: if the columns in ``csv_data`` do not match
          the expected columns returned by
          :func:`get_csv_column_names() <BaseModel.get_csv_column_names>`
        :raises ValueDeserializationError: if a value extracted from the CSV
          failed when executing its :term:`de-serialization function`.

        """
        csv_data = read_csv_data(csv_data,
                                 single_record = True)

        data = self._parse_csv(csv_data,
                               delimiter = delimiter,
                               wrap_all_strings = wrap_all_strings,
                               null_text = null_text,
                               wrapper_character = wrapper_character,
                               double_wrapper_character_when_nested = double_wrapper_character_when_nested,
                               escape_character = escape_character,
                               line_terminator = line_terminator,
                               config_set = config_set)

        for key in data:
            setattr(self, key, data[key])

    @classmethod
    def new_from_csv(cls,
                     csv_data,
                     delimiter = '|',
                     wrap_all_strings = False,
                     null_text = 'None',
                     wrapper_character = "'",
                     double_wrapper_character_when_nested = False,
                     escape_character = "\\",
                     line_terminator = '\r\n',
                     config_set = None):
        """Create a new model instance from a CSV record.

        .. tip::

          Unwrapped empty column values are automatically interpreted as null
          (:obj:`None <python:None>`).

        :param csv_data: The CSV data. If a Path-like object, will read the first
          record from a file that is assumed to include a header row. If a
          :class:`str <python:str>` and has more than one record (line), will assume
          the first line is a header row.
        :type csv_data: :class:`str <python:str>` / Path-like object

        :param delimiter: The delimiter used between columns. Defaults to ``|``.
        :type delimiter: :class:`str <python:str>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is applied. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param null_text: The string used to indicate an empty value if empty
          values are wrapped. Defaults to `None`.
        :type null_text: :class:`str <python:str>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :returns: A :term:`model instance` created from the record.
        :rtype: model instance

        :raises DeserializationError: if ``csv_data`` is not a valid
          :class:`str <python:str>`
        :raises CSVStructureError: if the columns in ``csv_data`` do not match
          the expected columns returned by
          :func:`get_csv_column_names() <BaseModel.get_csv_column_names>`
        :raises ValueDeserializationError: if a value extracted from the CSV
          failed when executing its :term:`de-serialization function`.

        """
        csv_data = read_csv_data(csv_data,
                                 single_record = True)

        data = cls._parse_csv(csv_data,
                              delimiter = delimiter,
                              wrap_all_strings = wrap_all_strings,
                              null_text = null_text,
                              wrapper_character = wrapper_character,
                              double_wrapper_character_when_nested = double_wrapper_character_when_nested,
                              escape_character = escape_character,
                              line_terminator = line_terminator,
                              config_set = config_set)

        return cls(**data)

    def dump_to_csv(self,
                    include_header = False,
                    delimiter = '|',
                    wrap_all_strings = False,
                    null_text = 'None',
                    wrapper_character = "'",
                    double_wrapper_character_when_nested = False,
                    escape_character = "\\",
                    line_terminator = '\r\n',
                    config_set = None):
        r"""Retrieve a :term:`CSV <Comma-Separated Value (CSV)>` representation of
        the object, *with all attributes* serialized regardless of configuration.

        .. caution::

          Nested objects (such as :term:`relationships <relationship>` or
          :term:`association proxies <association proxy>`) will **not**
          be serialized.

        .. note::

          This method ignores any ``display_name`` contributed on the
          :class:`AttributeConfiguration`.

        :param include_header: If ``True``, will include a header row with column
          labels. If ``False``, will not include a header row. Defaults to ``True``.
        :type include_header: :class:`bool <python:bool>`

        :param delimiter: The delimiter used between columns. Defaults to ``|``.
        :type delimiter: :class:`str <python:str>`

        :param wrap_all_strings: If ``True``, wraps any string data in the
          ``wrapper_character``. If ``None``, only wraps string data if it contains
          the ``delimiter``. Defaults to ``False``.
        :type wrap_all_strings: :class:`bool <python:bool>`

        :param null_text: The text value to use in place of empty values. Only
          applies if ``wrap_empty_values`` is ``True``. Defaults to ``'None'``.
        :type null_text: :class:`str <python:str>`

        :param wrapper_character: The string used to wrap string values when
          wrapping is necessary. Defaults to ``'``.
        :type wrapper_character: :class:`str <python:str>`

        :param double_wrapper_character_when_nested: If ``True``, will double the
          ``wrapper_character`` when it is found inside a column value. If ``False``,
          will precede the ``wrapper_character`` by the ``escape_character`` when
          it is found inside a column value. Defaults to ``False``.
        :type double_wrapper_character_when_nested: :class:`bool <python:bool>`

        :param escape_character: The character to use when escaping nested wrapper
          characters. Defaults to ``\``.
        :type escape_character: :class:`str <python:str>`

        :param line_terminator: The character used to mark the end of a line.
          Defaults to ``\r\n``.
        :type line_terminator: :class:`str <python:str>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use. Defaults to :obj:`None <python:None>`.
        :type config_set: :class:`str <python:str>` / :obj:`None <python:None>`

        :returns: Data from the object in CSV format ending in a newline (``\n``).
        :rtype: :class:`str <python:str>`
        """
        # pylint: disable=line-too-long

        attributes = [x for x in get_attribute_names(self,
                                                     include_callable = False,
                                                     include_nested = False,
                                                     include_private = True,
                                                     include_special = False,
                                                     include_utilities = False)
                      if x[0:2] != '__']

        if include_header:
            return self._get_attribute_csv_header(attributes,
                                                  delimiter = delimiter) + \
                   self._get_attribute_csv_data(attributes,
                                                is_dumping = True,
                                                delimiter = delimiter,
                                                wrap_all_strings = wrap_all_strings,
                                                null_text = null_text,
                                                wrapper_character = wrapper_character,
                                                double_wrapper_character_when_nested = double_wrapper_character_when_nested,
                                                escape_character = escape_character,
                                                line_terminator = line_terminator,
                                                config_set = config_set)


        return self._get_attribute_csv_data(attributes,
                                            is_dumping = True,
                                            delimiter = delimiter,
                                            wrap_all_strings = wrap_all_strings,
                                            null_text = null_text,
                                            wrapper_character = wrapper_character,
                                            double_wrapper_character_when_nested = double_wrapper_character_when_nested,
                                            escape_character = escape_character,
                                            line_terminator = line_terminator,
                                            config_set = config_set)