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
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
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]
def test_AttributeConfiguration_keys(): config = AttributeConfiguration() keys = config.keys() assert keys is not None assert len(keys) == len(config)
def test_AttributeConfiguration__missing_key(): config = AttributeConfiguration() with pytest.raises(KeyError): assert config['key'] is not None
def test_AttributeConfiguration__contains__(key, expected_result): config = AttributeConfiguration() assert (key in config) is expected_result
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
'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
}, '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']
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