Пример #1
0
def test_AttributeConfiguration__iterate__():
    config = AttributeConfiguration()
    length = len(config)
    index = 0
    for key in config:
        index += 1

    assert index == length

    assert len(config.values()) == len(config) == len(config.keys()) == index
Пример #2
0
def test_AttributeConfiguration__get_set__(key, value, expected_result, error):
    config = AttributeConfiguration()
    if not error:
        config[key] = value
        assert key in config
        assert config[key] == expected_result
        assert getattr(config, key) == expected_result
    else:
        with pytest.raises(error):
            config[key] = value
Пример #3
0
def test_AttributeConfiguration__init__(kwargs):
    if kwargs is None:
        result = AttributeConfiguration()
        kwargs = {}
    else:
        result = AttributeConfiguration(**kwargs)

    assert result.name == kwargs.get('name', None)
    assert result.supports_csv == bool_to_tuple(kwargs.get('supports_csv',
                                                           (False, False)))
    assert result.csv_sequence == kwargs.get('csv_sequence', None)
    assert result.supports_json == bool_to_tuple(kwargs.get('supports_json',
                                                            (False, False)))
    assert result.supports_yaml == bool_to_tuple(kwargs.get('supports_yaml',
                                                            (False, False)))
    assert result.supports_dict == bool_to_tuple(kwargs.get('supports_dict',
                                                            (False, False)))

    assert result.on_serialize is not None
    assert isinstance(result.on_serialize, dict)
    input_on_serialize = callable_to_dict(
        kwargs.get('on_serialize', BLANK_ON_SERIALIZE) or BLANK_ON_SERIALIZE
    )
    for key in result.on_serialize:
        assert key in input_on_serialize
        assert result.on_serialize[key] == input_on_serialize[key]

    assert result.on_deserialize is not None
    assert isinstance(result.on_deserialize, dict)

    input_on_deserialize = callable_to_dict(
        kwargs.get('on_deserialize', BLANK_ON_SERIALIZE) or BLANK_ON_SERIALIZE
    )

    for key in result.on_deserialize:
        assert key in input_on_deserialize
        assert result.on_deserialize[key] == input_on_deserialize[key]
Пример #4
0
def test_AttributeConfiguration_keys():
    config = AttributeConfiguration()
    keys = config.keys()
    assert keys is not None
    assert len(keys) == len(config)
Пример #5
0
def test_AttributeConfiguration__missing_key():
    config = AttributeConfiguration()
    with pytest.raises(KeyError):
        assert config['key'] is not None
Пример #6
0
def test_AttributeConfiguration__contains__(key, expected_result):
    config = AttributeConfiguration()
    assert (key in config) is expected_result
Пример #7
0
    assert keys is not None
    assert len(keys) == len(config)

def test_AttributeConfiguration__iterate__():
    config = AttributeConfiguration()
    length = len(config)
    index = 0
    for key in config:
        index += 1

    assert index == length

    assert len(config.values()) == len(config) == len(config.keys()) == index


@pytest.mark.parametrize('config, expected_length', [
    ([], 0),
    (None, 0),
    (AttributeConfiguration(), 1),
    ([AttributeConfiguration()], 1),
    ([AttributeConfiguration(), AttributeConfiguration()], 1),
    ({ 'name': 'test_1' }, 1),
    ([{ 'name': 'test_2' }, {'name': 'test_3'}], 2),
])
def test_validate_serialization_config(config, expected_length):
    result = validate_serialization_config(config)
    assert len(result) == expected_length
    if len(result) > 0:
        for item in result:
            assert isinstance(item, AttributeConfiguration)
def generate_model_from_dict(serialized_dict,
                             tablename,
                             primary_key,
                             cls = BaseModel,
                             serialization_config = None,
                             skip_nested = True,
                             default_to_str = False,
                             type_mapping = None,
                             base_model_attrs = None,
                             **kwargs):
    """Generate a :term:`model class` from a serialized :class:`dict <python:dict>`.

    .. versionadded: 0.3.0

    .. note::

      This function *cannot* programmatically create
      :term:`relationships <relationship>`, :term:`hybrid properties <hybrid property>`,
      or :term:`association proxies <association proxy>`.

    :param serialized_dict: The :class:`dict <python:dict>` that has been de-serialized
      from a given string. Keys will be treated as column names, while value data
      types will determine :term:`model attribute` data types.
    :type serialized_dict: :class:`dict <python:dict>`

    :param tablename: The name of the SQL table to which the model corresponds.
    :type tablename: :class:`str <python:str>`

    :param primary_key: The name of the column/key that should be used as the table's
      primary key.
    :type primary_key: :class:`str <python:str>`

    :param cls: The base class to use when generating a new :term:`model class`.
      Defaults to :class:`BaseModel` to provide serialization/de-serialization
      support.

      If a :class:`tuple <python:tuple>` of classes, will include :class:`BaseModel`
      in that list of classes to mixin serialization/de-serialization support.

      If not :obj:`None <python:None>` and not a :class:`tuple <python:tuple>`, will mixin
      :class:`BaseModel` with the value passed to provide
      serialization/de-serialization support.
    :type cls: :obj:`None <python:None>` / :class:`tuple <python:tuple>` of classes / class object

    :param serialization_config: Collection of
      :class:`AttributeConfiguration <sqlathanor.attributes.AttributeConfiguration>`
      that determine the generated model's
      :term:`serialization`/:term:`de-serialization`
      :ref:`configuration <configuration>`. If :obj:`None <python:None>`, will
      support serialization and de-serialization across all keys in
      ``serialized_dict``. Defaults to :obj:`None <python:None>`.
    :type serialization_config: Iterable of
      :class:`AttributeConfiguration <sqlathanor.attributes.AttributeConfiguration>`
      or coercable :class:`dict <python:dict>` objects / :obj:`None <python:None>`

    :param skip_nested: If ``True`` then any keys in ``serialized_dict`` that
      feature nested items (e.g. iterables, :class:`dict <python:dict>` objects,
      etc.) will be ignored. If ``False``, will treat serialized items as
      :class:`str <python:str>`. Defaults to ``True``.
    :type skip_nested: :class:`bool <python:bool>`

    :param default_to_str: If ``True``, will automatically set a key/column whose
      value type cannot be determined to ``str``
      (:class:`Text <sqlalchemy:sqlalchemy.types.Text>`). If ``False``, will
      use the value type's ``__name__`` attribute and attempt to find a mapping.
      Defaults to ``False``.
    :type default_to_str: :class:`bool <python:bool>`

    :param type_mapping: Determines how value types in ``serialized_dict`` map to
      SQL column data types. To add a new mapping or override a default, set a
      key to the name of the value type in Python, and set the value to a
      :doc:`SQLAlchemy Data Type <sqlalchemy:core/types>`. The following are the
      default mappings applied:

      .. list-table::
         :widths: 30 30
         :header-rows: 1

         * - Python Literal
           - SQL Column Type
         * - ``bool``
           - :class:`Boolean <sqlalchemy:sqlalchemy.types.Boolean>`
         * - ``str``
           - :class:`Text <sqlalchemy:sqlalchemy.types.Text>`
         * - ``int``
           - :class:`Integer <sqlalchemy:sqlalchemy.types.Integer>`
         * - ``float``
           - :class:`Float <sqlalchemy:sqlalchemy.types.Float>`
         * - ``date``
           - :class:`Date <sqlalchemy:sqlalchemy.types.Date>`
         * - ``datetime``
           - :class:`DateTime <sqlalchemy:sqlalchemy.types.DateTime>`
         * - ``time``
           - :class:`Time <sqlalchemy:sqlalchemy.types.Time>`

    :type type_mapping: :class:`dict <python:dict>` with type names as keys and
      column data types as values.

    :param base_model_attrs: Optional :class:`dict <python:dict>` of special
      attributes that will be applied to the generated
      :class:`BaseModel <sqlathanor.declarative.BaseModel>` (e.g.
      ``__table_args__``). Keys will correspond to the attribute name, while the
      value is the value that will be applied. Defaults to :obj:`None <python:None>`.
    :type base_model_attrs: :class:`dict <python:dict>` / :obj:`None <python:None>`

    :param kwargs: Any additional keyword arguments will be passed to
      :func:`declarative_base() <sqlathanor.declarative.declarative_base>` when
      generating the programmatic :class:`BaseModel <sqlathanor.declarative.BaseModel>`.

    :returns: :term:`Model class` whose structure matches ``serialized_dict``.
    :rtype: :class:`BaseModel`

    :raises UnsupportedValueTypeError: when a value in ``serialized_dict`` does not
      have a corresponding key in ``type_mapping``
    :raises ValueError: if ``serialized_dict`` is not a :class:`dict <python:dict>`
      or is empty
    :raises ValueError: if ``tablename`` is empty
    :raises ValueError: if ``primary_key`` is empty

    """
    # pylint: disable=too-many-branches

    if not isinstance(serialized_dict, dict):
        raise ValueError('serialized_dict must be a dict')

    if not serialized_dict:
        raise ValueError('serialized_dict cannot be empty')

    if not tablename:
        raise ValueError('tablename cannot be empty')

    if not primary_key:
        raise ValueError('primary_key cannot be empty')

    serialization_config = validate_serialization_config(serialization_config)

    GeneratedBaseModel = declarative_base(cls = cls, **kwargs)

    class InterimGeneratedModel(object):
        # pylint: disable=too-few-public-methods,missing-docstring,invalid-variable-name
        __tablename__ = tablename

    prospective_serialization_config = []

    for key in serialized_dict:
        value = serialized_dict[key]
        column_type = get_type_mapping(value,
                                       type_mapping = type_mapping,
                                       skip_nested = skip_nested,
                                       default_to_str = default_to_str)
        if column_type is None:
            continue

        if key == primary_key:
            column = Column(name = key, type_ = column_type, primary_key = True)
        else:
            column = Column(name = key, type_ = column_type)

        setattr(InterimGeneratedModel, key, column)

        attribute_config = AttributeConfiguration(name = key,
                                                  supports_csv = True,
                                                  supports_json = True,
                                                  supports_yaml = True,
                                                  supports_dict = True,
                                                  on_serialize = None,
                                                  on_deserialize = None)
        prospective_serialization_config.append(attribute_config)

    if not serialization_config:
        serialization_config = prospective_serialization_config

    if base_model_attrs:
        for key in base_model_attrs:
            setattr(InterimGeneratedModel, key, base_model_attrs[key])

    class GeneratedModel(GeneratedBaseModel, InterimGeneratedModel):
        # pylint: disable=missing-docstring,too-few-public-methods
        pass

    GeneratedModel.configure_serialization(configs = serialization_config)

    return GeneratedModel
Пример #9
0
          'yaml': test_func,
          'dict': test_func
      },
      'on_serialize': {
          'csv': None,
          'json': None,
          'yaml': None,
          'dict': None
      }
  }
 ),
 (True, 0, 'hybrid', {
     'config': AttributeConfiguration(name = 'hybrid',
                                      supports_csv = False,
                                      supports_json = True,
                                      supports_yaml = True,
                                      supports_dict = True,
                                      on_deserialize = test_func,
                                      on_serialize = test_func)
     },
  {
      'supports_csv': (False, False),
      'supports_json': (True, True),
      'supports_yaml': (True, True),
      'supports_dict': (True, True),
      'csv_sequence': None,
      'on_deserialize': {
          'csv': test_func,
          'json': test_func,
          'yaml': test_func,
          'dict': test_func
Пример #10
0
  }, 'test_table', 'int1', None, True, False, None, None, [('int1', Integer),
                                                           ('string1', Text),
                                                           ('float1', Float),
                                                           ('bool1', Boolean),
                                                           ('datetime1', DateTime),
                                                           ('date1', Date),
                                                           ('time1', Time)], UnsupportedValueTypeError),
 ({'int1': 123,
   'string1': 'test',
   'float1': 123.45,
   'bool1': True,
   'datetime1': '2018-01-01T00:00:00.00000',
   'date1': '2018-01-01',
   'time1': datetime.utcnow().time(),
   'nested1': ['test', 'test2']
  }, 'test_table', 'int1', [AttributeConfiguration(name = 'bool1', supports_csv = False, supports_json = True, supports_yaml = True, supports_dict = True)],
  True, False, None, None, [('int1', Integer),
                            ('string1', Text),
                            ('float1', Float),
                            ('bool1', Boolean),
                            ('datetime1', DateTime),
                            ('date1', Date),
                            ('time1', Time)], None),
 ({'int1': 123,
   'string1': 'test',
   'float1': 123.45,
   'bool1': True,
   'datetime1': '2018-01-01T00:00:00.00000',
   'date1': '2018-01-01',
   'time1': datetime.utcnow().time(),
   'nested1': ['test', 'test2']
Пример #11
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