Beispiel #1
0
    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
Beispiel #2
0
    def _parse_dict(cls,
                    input_data,
                    format,
                    error_on_extra_keys=True,
                    drop_extra_keys=False,
                    config_set=None):
        """Generate a processed :class:`dict <python:dict>` object from
        in-bound :class:`dict <python:dict>` data.

        :param input_data: An inbound :class:`dict <python:dict>` object which
          can be processed for de-serialization.
        :type input_data: :class:`dict <python:dict>`

        :param format: The format from which ``input_data`` was received. Accepts:
          ``'csv'``, ``'json'``, ``'yaml'``, and ``'dict'``.
        :type format: :class:`str <python:str>`

        :param error_on_extra_keys: If ``True``, will raise an error if an
          unrecognized key is found in ``dict_data``. If ``False``, will
          either drop or include the extra key in the result, as configured in
          the ``drop_extra_keys`` parameter. Defaults to ``True``.
        :type error_on_extra_keys: :class:`bool <python:bool>`

        :param drop_extra_keys: If ``True``, will omit unrecognized top-level keys
          from the resulting :class:`dict <python:dict>`. If ``False``, will
          include unrecognized keys or raise an error based on the configuration of
          the ``error_on_extra_keys`` parameter. Defaults to ``False``.
        :type drop_extra_keys: :class:`bool <python:bool>`

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

        :returns: A processed :class:`dict <python:dict>` object that has had
          :term:`deserializer functions` applied to it.
        :rtype: :class:`dict <python:dict>`

        :raises ExtraKeyError: if ``error_on_extra_keys`` is ``True`` and
          ``input_data`` contains top-level keys that are not recognized as
          attributes for the instance model.
        :raises DeserializationError: if ``input_data`` is
          not a :class:`dict <python:dict>` or JSON object serializable to a
          :class:`dict <python:dict>` or if ``input_data`` is empty.
        :raises InvalidFormatError: if ``format`` is not a supported value
        """
        if format not in ['csv', 'json', 'yaml', 'dict']:
            raise InvalidFormatError("format '%s' not supported" % format)

        try:
            input_data = validators.dict(input_data,
                                         allow_empty=True,
                                         json_serializer=json)
        except ValueError:
            raise DeserializationError('input_data is not a dict')

        if not input_data or len(input_data.keys()) == 0:
            raise DeserializationError("input_data is empty")

        dict_object = dict_()

        if format == 'csv':
            attribute_getter = cls.get_csv_serialization_config
        elif format == 'json':
            attribute_getter = cls.get_json_serialization_config
        elif format == 'yaml':
            attribute_getter = cls.get_yaml_serialization_config
        elif format == 'dict':
            attribute_getter = cls.get_dict_serialization_config

        attributes = [
            x for x in attribute_getter(
                deserialize=True, serialize=None, config_set=config_set)
            if hasattr(cls, x.name)
        ]

        if not attributes:
            raise DeserializableAttributeError(
                "'%s' has no '%s' de-serializable attributes" %
                (type(cls.__name__), format))

        attribute_names = [x.display_name or x.name for x in attributes]
        extra_keys = [x for x in input_data.keys() if x not in attribute_names]
        if extra_keys and error_on_extra_keys:
            raise ExtraKeyError("input data had extra keys: %s" % extra_keys)

        for attribute in attributes:
            key = attribute.display_name or attribute.name
            try:
                value = input_data.pop(key)
            except KeyError:
                continue

            value = cls._get_deserialized_value(
                value,
                format,
                key,
                error_on_extra_keys=error_on_extra_keys,
                drop_extra_keys=drop_extra_keys,
                config_set=config_set)

            dict_object[attribute.name] = value

        if input_data and not drop_extra_keys:
            for key in input_data:
                dict_object[key] = input_data[key]

        return dict_object
Beispiel #3
0
    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
Beispiel #4
0
    def _to_dict(self,
                 format,
                 max_nesting=0,
                 current_nesting=0,
                 is_dumping=False,
                 config_set=None):
        """Return a :class:`dict <python:dict>` representation of the object.

        .. warning::

          This method is an **intermediate** step that is used to produce the
          contents for certain public JSON, YAML, and :class:`dict <python:dict>`
          serialization methods. It should not be called directly.

        :param format: The format to which the :class:`dict <python:dict>` will
          ultimately be serialized. Accepts: ``'csv'``, ``'json'``, ``'yaml'``, and
          ``'dict'``.
        :type format: :class:`str <python:str>`

        :param max_nesting: The maximum number of levels that the resulting
          :class:`dict <python:dict>` object can be nested. If set to ``0``, will
          not nest other serializable objects. Defaults to ``0``.
        :type max_nesting: :class:`int <python:int>`

        :param current_nesting: The current nesting level at which the
          :class:`dict <python:dict>` representation will reside. Defaults to ``0``.
        :type current_nesting: :class:`int <python:int>`

        :param is_dumping: If ``True``, retrieves all attributes except callables,
          utilities, and specials (``__<name>``). If ``False``, only retrieves
          those that have JSON serialization enabled. Defaults to ``False``.
        :type is_dumping: :class:`bool <python:bool>`

        :param config_set: If not :obj:`None <python:None>`, the named configuration set
          to use when processing the input. 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 object.
        :rtype: :class:`dict <python:dict>`

        :raises InvalidFormatError: if ``format`` is not recognized
        :raises SerializableAttributeError: if attributes is empty
        :raises UnsupportedSerializationError: if unable to serialize a value
        :raises MaximumNestingExceededError: if ``current_nesting`` is greater
          than ``max_nesting``
        :raises MaximumNestingExceededWarning: if an attribute requires nesting
          beyond ``max_nesting``
        """
        # pylint: disable=too-many-branches

        next_nesting = current_nesting + 1

        if format not in ['csv', 'json', 'yaml', 'dict']:
            raise InvalidFormatError("format '%s' not supported" % format)

        if current_nesting > max_nesting:
            raise MaximumNestingExceededError(
                'current nesting level (%s) exceeds maximum %s' %
                (current_nesting, max_nesting))

        dict_object = dict_()

        if format == 'csv':
            attribute_getter = self.get_csv_serialization_config
        elif format == 'json':
            attribute_getter = self.get_json_serialization_config
        elif format == 'yaml':
            attribute_getter = self.get_yaml_serialization_config
        elif format == 'dict':
            attribute_getter = self.get_dict_serialization_config

        if not is_dumping:
            attributes = [
                x for x in attribute_getter(
                    deserialize=None, serialize=True, config_set=config_set)
                if hasattr(self, x.name)
            ]
        else:
            attribute_names = [
                x for x in get_attribute_names(self,
                                               include_callable=False,
                                               include_nested=False,
                                               include_private=True,
                                               include_special=False,
                                               include_utilities=False)
            ]
            attributes = []
            for item in attribute_names:
                attribute_config = self.get_attribute_serialization_config(
                    item, config_set=config_set)
                if attribute_config is not None:
                    on_serialize_function = attribute_config.on_serialize.get(
                        format, None)
                else:
                    on_serialize_function = None

                attribute = AttributeConfiguration(
                    name=item,
                    supports_json=True,
                    supports_yaml=True,
                    supports_dict=True,
                    on_serialize=on_serialize_function)
                attributes.append(attribute)

        if not attributes:
            raise SerializableAttributeError(
                "'%s' has no '%s' serializable attributes" %
                (type(self.__class__), format))

        for attribute in attributes:
            item = getattr(self, attribute.name, None)
            if hasattr(item, '_to_dict'):
                try:
                    value = item._to_dict(
                        format,  # pylint: disable=protected-access
                        max_nesting=max_nesting,
                        current_nesting=next_nesting,
                        is_dumping=is_dumping,
                        config_set=config_set)
                except MaximumNestingExceededError:
                    warnings.warn(
                        "skipping key '%s' because maximum nesting has been exceeded" \
                            % attribute.name,
                        MaximumNestingExceededWarning
                    )
                    continue
            else:
                if attribute.on_serialize[format]:
                    on_serialize_function = attribute.on_serialize[format]
                    item = on_serialize_function(item)

                if checkers.is_iterable(item,
                                        forbid_literals=(str, bytes, dict)):
                    try:
                        value = iterable__to_dict(item,
                                                  format,
                                                  max_nesting=max_nesting,
                                                  current_nesting=next_nesting,
                                                  is_dumping=is_dumping,
                                                  config_set=config_set)
                    except MaximumNestingExceededError:
                        warnings.warn(
                            "skipping key '%s' because maximum nesting has been exceeded" \
                                % attribute.name,
                            MaximumNestingExceededWarning
                        )
                        continue
                    except NotAnIterableError:
                        try:
                            value = self._get_serialized_value(
                                format, attribute.name, config_set=config_set)
                        except UnsupportedSerializationError as error:
                            if is_dumping:
                                value = getattr(self, attribute.name)
                            else:
                                raise error
                else:
                    try:
                        value = self._get_serialized_value(
                            format, attribute.name, config_set=config_set)
                    except UnsupportedSerializationError as error:
                        if is_dumping:
                            value = getattr(self, attribute.name)
                        else:
                            raise error

            serialized_key = attribute.display_name or attribute.name

            dict_object[str(serialized_key)] = value

        return dict_object