Hello community,

here is the log from the commit of package python-SQLAlchemy-Utils for 
openSUSE:Factory checked in at 2018-02-20 17:55:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-SQLAlchemy-Utils (Old)
 and      /work/SRC/openSUSE:Factory/.python-SQLAlchemy-Utils.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-SQLAlchemy-Utils"

Tue Feb 20 17:55:43 2018 rev:8 rq:578202 version:0.33.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-SQLAlchemy-Utils/python-SQLAlchemy-Utils.changes
  2017-11-15 16:58:15.107040572 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-SQLAlchemy-Utils.new/python-SQLAlchemy-Utils.changes
     2018-02-20 17:55:52.476737512 +0100
@@ -1,0 +2,14 @@
+Mon Feb 19 17:16:09 UTC 2018 - [email protected]
+
+- specfile:
+  * update copyright year
+
+- update to version 0.33.0:
+  * Added support for materialized views in PostgreSQL
+  * Added Ltree.descendant_of and Ltree.ancestor_of (#311, pull
+    request courtesy of kageurufu)
+  * Dropped Python 3.3 support
+  * Fixed EncryptedType padding (#301, pull request courtesy of
+    konstantinoskostis)
+
+-------------------------------------------------------------------

Old:
----
  SQLAlchemy-Utils-0.32.21.tar.gz

New:
----
  SQLAlchemy-Utils-0.33.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-SQLAlchemy-Utils.spec ++++++
--- /var/tmp/diff_new_pack.24qcZ9/_old  2018-02-20 17:55:54.904650091 +0100
+++ /var/tmp/diff_new_pack.24qcZ9/_new  2018-02-20 17:55:54.904650091 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-SQLAlchemy-Utils
 #
-# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany.
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-SQLAlchemy-Utils
-Version:        0.32.21
+Version:        0.33.0
 Release:        0
 Summary:        Various utility functions for SQLAlchemy
 License:        BSD-3-Clause

++++++ SQLAlchemy-Utils-0.32.21.tar.gz -> SQLAlchemy-Utils-0.33.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/SQLAlchemy-Utils-0.32.21/CHANGES.rst 
new/SQLAlchemy-Utils-0.33.0/CHANGES.rst
--- old/SQLAlchemy-Utils-0.32.21/CHANGES.rst    2017-11-11 18:53:06.000000000 
+0100
+++ new/SQLAlchemy-Utils-0.33.0/CHANGES.rst     2018-02-18 15:42:06.000000000 
+0100
@@ -4,6 +4,15 @@
 Here you can see the full list of changes between each SQLAlchemy-Utils 
release.
 
 
+0.33.0 (2018-02-18)
+^^^^^^^^^^^^^^^^^^^
+
+- Added support for materialized views in PostgreSQL
+- Added Ltree.descendant_of and Ltree.ancestor_of (#311, pull request courtesy 
of kageurufu)
+- Dropped Python 3.3 support
+- Fixed EncryptedType padding (#301, pull request courtesy of 
konstantinoskostis)
+
+
 0.32.21 (2017-11-11)
 ^^^^^^^^^^^^^^^^^^^^
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/SQLAlchemy-Utils-0.32.21/PKG-INFO 
new/SQLAlchemy-Utils-0.33.0/PKG-INFO
--- old/SQLAlchemy-Utils-0.32.21/PKG-INFO       2017-11-11 19:41:23.000000000 
+0100
+++ new/SQLAlchemy-Utils-0.33.0/PKG-INFO        2018-02-18 15:47:12.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: SQLAlchemy-Utils
-Version: 0.32.21
+Version: 0.33.0
 Summary: Various utility functions for SQLAlchemy.
 Home-page: https://github.com/kvesteri/sqlalchemy-utils
 Author: Konsta Vesterinen, Ryan Leckey, Janne Vanhala, Vesa Uimonen
@@ -21,7 +21,6 @@
 Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/SQLAlchemy_Utils.egg-info/PKG-INFO 
new/SQLAlchemy-Utils-0.33.0/SQLAlchemy_Utils.egg-info/PKG-INFO
--- old/SQLAlchemy-Utils-0.32.21/SQLAlchemy_Utils.egg-info/PKG-INFO     
2017-11-11 19:41:23.000000000 +0100
+++ new/SQLAlchemy-Utils-0.33.0/SQLAlchemy_Utils.egg-info/PKG-INFO      
2018-02-18 15:47:12.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: SQLAlchemy-Utils
-Version: 0.32.21
+Version: 0.33.0
 Summary: Various utility functions for SQLAlchemy.
 Home-page: https://github.com/kvesteri/sqlalchemy-utils
 Author: Konsta Vesterinen, Ryan Leckey, Janne Vanhala, Vesa Uimonen
@@ -21,7 +21,6 @@
 Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/SQLAlchemy_Utils.egg-info/SOURCES.txt 
new/SQLAlchemy-Utils-0.33.0/SQLAlchemy_Utils.egg-info/SOURCES.txt
--- old/SQLAlchemy-Utils-0.32.21/SQLAlchemy_Utils.egg-info/SOURCES.txt  
2017-11-11 19:41:23.000000000 +0100
+++ new/SQLAlchemy-Utils-0.33.0/SQLAlchemy_Utils.egg-info/SOURCES.txt   
2018-02-18 15:47:12.000000000 +0100
@@ -40,6 +40,7 @@
 sqlalchemy_utils/generic.py
 sqlalchemy_utils/i18n.py
 sqlalchemy_utils/listeners.py
+sqlalchemy_utils/materialized_view.py
 sqlalchemy_utils/models.py
 sqlalchemy_utils/observer.py
 sqlalchemy_utils/operators.py
@@ -70,7 +71,6 @@
 sqlalchemy_utils/types/country.py
 sqlalchemy_utils/types/currency.py
 sqlalchemy_utils/types/email.py
-sqlalchemy_utils/types/encrypted.py
 sqlalchemy_utils/types/ip_address.py
 sqlalchemy_utils/types/json.py
 sqlalchemy_utils/types/locale.py
@@ -86,6 +86,9 @@
 sqlalchemy_utils/types/url.py
 sqlalchemy_utils/types/uuid.py
 sqlalchemy_utils/types/weekdays.py
+sqlalchemy_utils/types/encrypted/__init__.py
+sqlalchemy_utils/types/encrypted/encrypted_type.py
+sqlalchemy_utils/types/encrypted/padding.py
 tests/.DS_Store
 tests/__init__.py
 tests/mixins.py
@@ -95,6 +98,7 @@
 tests/test_expressions.py
 tests/test_instant_defaults_listener.py
 tests/test_instrumented_list.py
+tests/test_materialized_view.py
 tests/test_models.py
 tests/test_path.py
 tests/test_proxy_dict.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/SQLAlchemy-Utils-0.32.21/docs/data_types.rst 
new/SQLAlchemy-Utils-0.33.0/docs/data_types.rst
--- old/SQLAlchemy-Utils-0.32.21/docs/data_types.rst    2016-10-20 
08:55:26.000000000 +0200
+++ new/SQLAlchemy-Utils-0.33.0/docs/data_types.rst     2018-02-18 
15:38:57.000000000 +0100
@@ -74,7 +74,7 @@
 EncryptedType
 -------------
 
-.. module:: sqlalchemy_utils.types.encrypted
+.. module:: sqlalchemy_utils.types.encrypted.encrypted_type
 
 .. autoclass:: EncryptedType
 
@@ -183,4 +183,3 @@
 .. module:: sqlalchemy_utils.types.weekdays
 
 .. autoclass:: WeekDaysType
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/SQLAlchemy-Utils-0.32.21/setup.py 
new/SQLAlchemy-Utils-0.33.0/setup.py
--- old/SQLAlchemy-Utils-0.32.21/setup.py       2017-03-27 15:50:45.000000000 
+0200
+++ new/SQLAlchemy-Utils-0.33.0/setup.py        2018-02-18 15:35:53.000000000 
+0100
@@ -88,7 +88,6 @@
         'Programming Language :: Python :: 2',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/__init__.py 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/__init__.py
--- old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/__init__.py   2017-11-11 
18:53:06.000000000 +0100
+++ new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/__init__.py    2018-02-18 
15:42:27.000000000 +0100
@@ -53,6 +53,10 @@
     force_auto_coercion,
     force_instant_defaults
 )
+from .materialized_view import (  # noqa
+    create_materialized_view,
+    refresh_materialized_view
+)
 from .models import generic_repr, Timestamp  # noqa
 from .observer import observes  # noqa
 from .primitives import Country, Currency, Ltree, WeekDay, WeekDays  # noqa
@@ -95,4 +99,4 @@
     WeekDaysType
 )
 
-__version__ = '0.32.21'
+__version__ = '0.33.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/materialized_view.py 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/materialized_view.py
--- old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/materialized_view.py  
1970-01-01 01:00:00.000000000 +0100
+++ new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/materialized_view.py   
2018-02-18 15:30:37.000000000 +0100
@@ -0,0 +1,92 @@
+import sqlalchemy as sa
+from sqlalchemy.ext import compiler
+from sqlalchemy.schema import DDLElement, PrimaryKeyConstraint
+
+
+class CreateMaterializedView(DDLElement):
+    def __init__(self, name, selectable):
+        self.name = name
+        self.selectable = selectable
+
+
[email protected](CreateMaterializedView)
+def compile_create_materialized_view(element, compiler, **kw):
+    return 'CREATE MATERIALIZED VIEW %s AS %s' % (
+        element.name,
+        compiler.sql_compiler.process(element.selectable, literal_binds=True),
+    )
+
+
+class DropMateralizedView(DDLElement):
+    def __init__(self, name):
+        self.name = name
+
+
[email protected](DropMateralizedView)
+def compile_drop_materialized_view(element, compiler, **kw):
+    return 'DROP MATERIALIZED VIEW IF EXISTS {} CASCADE'.format(
+        element.name
+    )
+
+
+def create_table_from_selectable(
+    name,
+    selectable,
+    indexes=None,
+    metadata=None
+):
+    if indexes is None:
+        indexes = []
+    if metadata is None:
+        metadata = sa.MetaData()
+    args = [
+        sa.Column(c.name, c.type, primary_key=c.primary_key)
+        for c in selectable.c
+    ] + indexes
+    table = sa.Table(name, metadata, *args)
+
+    if not any([c.primary_key for c in selectable.c]):
+        table.append_constraint(
+            PrimaryKeyConstraint(*[c.name for c in selectable.c])
+        )
+    return table
+
+
+def create_materialized_view(
+    name,
+    selectable,
+    metadata,
+    indexes=None
+):
+    table = create_table_from_selectable(
+        name=name,
+        selectable=selectable,
+        indexes=indexes,
+        metadata=None
+    )
+
+    sa.event.listen(
+        metadata,
+        'after_create',
+        CreateMaterializedView(name, selectable)
+    )
+
+    @sa.event.listens_for(metadata, 'after_create')
+    def create_indexes(target, connection, **kw):
+        for idx in table.indexes:
+            idx.create(connection)
+
+    sa.event.listen(metadata, 'before_drop', DropMateralizedView(name))
+    return table
+
+
+def refresh_materialized_view(session, name, concurrently=False):
+    # Since session.execute() bypasses autoflush, we must manually flush in
+    # order to include newly-created/modified objects in the refresh.
+    session.flush()
+    session.execute(
+        'REFRESH MATERIALIZED VIEW {}{}'.format(
+            'CONCURRENTLY ' if concurrently else '',
+            name
+        )
+    )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/primitives/ltree.py 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/primitives/ltree.py
--- old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/primitives/ltree.py   
2016-04-25 20:23:11.000000000 +0200
+++ new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/primitives/ltree.py    
2018-02-18 15:34:52.000000000 +0100
@@ -128,6 +128,28 @@
                 return index
         raise ValueError('subpath not found')
 
+    def descendant_of(self, other):
+        """
+        is left argument a descendant of right (or equal)?
+
+        ::
+
+            assert Ltree('1.2.3.4.5').descendant_of('1.2.3')
+        """
+        subpath = self[:len(Ltree(other))]
+        return subpath == other
+
+    def ancestor_of(self, other):
+        """
+        is left argument an ancestor of right (or equal)?
+
+        ::
+
+            assert Ltree('1.2.3').ancestor_of('1.2.3.4.5')
+        """
+        subpath = Ltree(other)[:len(self)]
+        return subpath == self
+
     def __getitem__(self, key):
         if isinstance(key, int):
             return Ltree(self.path.split('.')[key])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/__init__.py 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/__init__.py
--- old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/__init__.py     
2016-05-20 08:58:31.000000000 +0200
+++ new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/__init__.py      
2018-02-18 15:38:57.000000000 +0100
@@ -8,7 +8,7 @@
 from .country import CountryType  # noqa
 from .currency import CurrencyType  # noqa
 from .email import EmailType  # noqa
-from .encrypted import EncryptedType  # noqa
+from .encrypted.encrypted_type import EncryptedType  # noqa
 from .ip_address import IPAddressType  # noqa
 from .json import JSONType  # noqa
 from .locale import LocaleType  # noqa
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/encrypted/__init__.py 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/encrypted/__init__.py
--- old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/encrypted/__init__.py   
1970-01-01 01:00:00.000000000 +0100
+++ new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/encrypted/__init__.py    
2018-02-18 15:38:57.000000000 +0100
@@ -0,0 +1 @@
+# Module for encrypted type
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/encrypted/encrypted_type.py 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/encrypted/encrypted_type.py
--- 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/encrypted/encrypted_type.py 
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/encrypted/encrypted_type.py  
    2018-02-18 15:38:57.000000000 +0100
@@ -0,0 +1,382 @@
+# -*- coding: utf-8 -*-
+import base64
+import datetime
+
+import six
+from sqlalchemy.types import LargeBinary, String, TypeDecorator
+
+from sqlalchemy_utils.exceptions import ImproperlyConfigured
+from sqlalchemy_utils.types.encrypted.padding import PADDING_MECHANISM
+from sqlalchemy_utils.types.scalar_coercible import ScalarCoercible
+
+cryptography = None
+try:
+    import cryptography
+    from cryptography.hazmat.backends import default_backend
+    from cryptography.hazmat.primitives import hashes
+    from cryptography.hazmat.primitives.ciphers import (
+        Cipher, algorithms, modes
+    )
+    from cryptography.fernet import Fernet
+except ImportError:
+    pass
+
+
+dateutil = None
+try:
+    import dateutil
+    from dateutil.parser import parse as datetime_parse
+except ImportError:
+    pass
+
+
+class EncryptionDecryptionBaseEngine(object):
+    """A base encryption and decryption engine.
+
+    This class must be sub-classed in order to create
+    new engines.
+    """
+
+    def _update_key(self, key):
+        if isinstance(key, six.string_types):
+            key = key.encode()
+        digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
+        digest.update(key)
+        engine_key = digest.finalize()
+
+        self._initialize_engine(engine_key)
+
+    def encrypt(self, value):
+        raise NotImplementedError('Subclasses must implement this!')
+
+    def decrypt(self, value):
+        raise NotImplementedError('Subclasses must implement this!')
+
+
+class AesEngine(EncryptionDecryptionBaseEngine):
+    """Provide AES encryption and decryption methods."""
+
+    BLOCK_SIZE = 16
+
+    def _initialize_engine(self, parent_class_key):
+        self.secret_key = parent_class_key
+        self.iv = self.secret_key[:16]
+        self.cipher = Cipher(
+            algorithms.AES(self.secret_key),
+            modes.CBC(self.iv),
+            backend=default_backend()
+        )
+
+    def _set_padding_mechanism(self, padding_mechanism=None):
+        """Set the padding mechanism."""
+        if isinstance(padding_mechanism, six.string_types):
+            if padding_mechanism not in PADDING_MECHANISM.keys():
+                raise ImproperlyConfigured(
+                    "There is not padding mechanism with name {}".format(
+                        padding_mechanism
+                    )
+                )
+
+        if padding_mechanism is None:
+            padding_mechanism = 'naive'
+
+        padding_class = PADDING_MECHANISM[padding_mechanism]
+        self.padding_engine = padding_class(self.BLOCK_SIZE)
+
+    def encrypt(self, value):
+        if not isinstance(value, six.string_types):
+            value = repr(value)
+        if isinstance(value, six.text_type):
+            value = str(value)
+        value = value.encode()
+        value = self.padding_engine.pad(value)
+        encryptor = self.cipher.encryptor()
+        encrypted = encryptor.update(value) + encryptor.finalize()
+        encrypted = base64.b64encode(encrypted)
+        return encrypted
+
+    def decrypt(self, value):
+        if isinstance(value, six.text_type):
+            value = str(value)
+        decryptor = self.cipher.decryptor()
+        decrypted = base64.b64decode(value)
+        decrypted = decryptor.update(decrypted) + decryptor.finalize()
+        decrypted = self.padding_engine.unpad(decrypted)
+        if not isinstance(decrypted, six.string_types):
+            try:
+                decrypted = decrypted.decode('utf-8')
+            except UnicodeDecodeError:
+                raise ValueError('Invalid decryption key')
+        return decrypted
+
+
+class FernetEngine(EncryptionDecryptionBaseEngine):
+    """Provide Fernet encryption and decryption methods."""
+
+    def _initialize_engine(self, parent_class_key):
+        self.secret_key = base64.urlsafe_b64encode(parent_class_key)
+        self.fernet = Fernet(self.secret_key)
+
+    def encrypt(self, value):
+        if not isinstance(value, six.string_types):
+            value = repr(value)
+        if isinstance(value, six.text_type):
+            value = str(value)
+        value = value.encode()
+        encrypted = self.fernet.encrypt(value)
+        return encrypted
+
+    def decrypt(self, value):
+        if isinstance(value, six.text_type):
+            value = str(value)
+        decrypted = self.fernet.decrypt(value)
+        if not isinstance(decrypted, six.string_types):
+            decrypted = decrypted.decode('utf-8')
+        return decrypted
+
+
+class EncryptedType(TypeDecorator, ScalarCoercible):
+    """
+    EncryptedType provides a way to encrypt and decrypt values,
+    to and from databases, that their type is a basic SQLAlchemy type.
+    For example Unicode, String or even Boolean.
+    On the way in, the value is encrypted and on the way out the stored value
+    is decrypted.
+
+    EncryptedType needs Cryptography_ library in order to work.
+
+    When declaring a column which will be of type EncryptedType
+    it is better to be as precise as possible and follow the pattern
+    below.
+
+    .. _Cryptography: https://cryptography.io/en/latest/
+
+    ::
+
+
+        a_column = sa.Column(EncryptedType(sa.Unicode,
+                                           secret_key,
+                                           FernetEngine))
+
+        another_column = sa.Column(EncryptedType(sa.Unicode,
+                                           secret_key,
+                                           AesEngine,
+                                           'pkcs5'))
+
+
+    A more complete example is given below.
+
+    ::
+
+
+        import sqlalchemy as sa
+        from sqlalchemy import create_engine
+        from sqlalchemy.ext.declarative import declarative_base
+        from sqlalchemy.orm import sessionmaker
+
+        from sqlalchemy_utils import EncryptedType
+        from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine
+
+        secret_key = 'secretkey1234'
+        # setup
+        engine = create_engine('sqlite:///:memory:')
+        connection = engine.connect()
+        Base = declarative_base()
+
+
+        class User(Base):
+            __tablename__ = "user"
+            id = sa.Column(sa.Integer, primary_key=True)
+            username = sa.Column(EncryptedType(sa.Unicode,
+                                               secret_key,
+                                               AesEngine,
+                                               'pkcs5'))
+            access_token = sa.Column(EncryptedType(sa.String,
+                                                   secret_key,
+                                                   AesEngine,
+                                                   'pkcs5'))
+            is_active = sa.Column(EncryptedType(sa.Boolean,
+                                                secret_key,
+                                                AesEngine,
+                                                'zeroes'))
+            number_of_accounts = sa.Column(EncryptedType(sa.Integer,
+                                                         secret_key,
+                                                         AesEngine,
+                                                         'oneandzeroes'))
+
+
+        sa.orm.configure_mappers()
+        Base.metadata.create_all(connection)
+
+        # create a configured "Session" class
+        Session = sessionmaker(bind=connection)
+
+        # create a Session
+        session = Session()
+
+        # example
+        user_name = u'secret_user'
+        test_token = 'atesttoken'
+        active = True
+        num_of_accounts = 2
+
+        user = User(username=user_name, access_token=test_token,
+                    is_active=active, number_of_accounts=num_of_accounts)
+        session.add(user)
+        session.commit()
+
+        user_id = user.id
+
+        session.expunge_all()
+
+        user_instance = session.query(User).get(user_id)
+
+        print('id: {}'.format(user_instance.id))
+        print('username: {}'.format(user_instance.username))
+        print('token: {}'.format(user_instance.access_token))
+        print('active: {}'.format(user_instance.is_active))
+        print('accounts: {}'.format(user_instance.number_of_accounts))
+
+        # teardown
+        session.close_all()
+        Base.metadata.drop_all(connection)
+        connection.close()
+        engine.dispose()
+
+    The key parameter accepts a callable to allow for the key to change
+    per-row instead of being fixed for the whole table.
+
+    ::
+
+
+        def get_key():
+            return 'dynamic-key'
+
+        class User(Base):
+            __tablename__ = 'user'
+            id = sa.Column(sa.Integer, primary_key=True)
+            username = sa.Column(EncryptedType(
+                sa.Unicode, get_key))
+
+    """
+
+    impl = LargeBinary
+
+    def __init__(self, type_in=None, key=None,
+                 engine=None, padding=None, **kwargs):
+        """Initialization."""
+        if not cryptography:
+            raise ImproperlyConfigured(
+                "'cryptography' is required to use EncryptedType"
+            )
+        super(EncryptedType, self).__init__(**kwargs)
+        # set the underlying type
+        if type_in is None:
+            type_in = String()
+        elif isinstance(type_in, type):
+            type_in = type_in()
+        self.underlying_type = type_in
+        self._key = key
+        if not engine:
+            engine = AesEngine
+        self.engine = engine()
+        if isinstance(self.engine, AesEngine):
+            self.engine._set_padding_mechanism(padding)
+
+    @property
+    def key(self):
+        return self._key
+
+    @key.setter
+    def key(self, value):
+        self._key = value
+
+    def _update_key(self):
+        key = self._key() if callable(self._key) else self._key
+        self.engine._update_key(key)
+
+    def process_bind_param(self, value, dialect):
+        """Encrypt a value on the way in."""
+        if value is not None:
+            self._update_key()
+
+            try:
+                value = self.underlying_type.process_bind_param(
+                    value, dialect
+                )
+
+            except AttributeError:
+                # Doesn't have 'process_bind_param'
+
+                # Handle 'boolean' and 'dates'
+                type_ = self.underlying_type.python_type
+                if issubclass(type_, bool):
+                    value = 'true' if value else 'false'
+
+                elif issubclass(type_, (datetime.date, datetime.time)):
+                    value = value.isoformat()
+
+            return self.engine.encrypt(value)
+
+    def process_result_value(self, value, dialect):
+        """Decrypt value on the way out."""
+        if value is not None:
+            self._update_key()
+            decrypted_value = self.engine.decrypt(value)
+
+            try:
+                return self.underlying_type.process_result_value(
+                    decrypted_value, dialect
+                )
+
+            except AttributeError:
+                # Doesn't have 'process_result_value'
+
+                # Handle 'boolean' and 'dates'
+                type_ = self.underlying_type.python_type
+                date_types = [datetime.datetime, datetime.time, datetime.date]
+
+                if issubclass(type_, bool):
+                    return decrypted_value == 'true'
+
+                elif type_ in date_types:
+                    return DatetimeHandler.process_value(
+                        decrypted_value, type_
+                    )
+
+                # Handle all others
+                return self.underlying_type.python_type(decrypted_value)
+
+    def _coerce(self, value):
+        if isinstance(self.underlying_type, ScalarCoercible):
+            return self.underlying_type._coerce(value)
+
+        return value
+
+
+class DatetimeHandler(object):
+    """
+    DatetimeHandler is responsible for parsing strings and
+    returning the appropriate date, datetime or time objects.
+    """
+
+    @classmethod
+    def process_value(cls, value, python_type):
+        """
+        process_value returns a datetime, date
+        or time object according to a given string
+        value and a python type.
+        """
+        if not dateutil:
+            raise ImproperlyConfigured(
+                "'python-dateutil' is required to process datetimes"
+            )
+
+        return_value = datetime_parse(value)
+
+        if issubclass(python_type, datetime.datetime):
+            return return_value
+        elif issubclass(python_type, datetime.time):
+            return return_value.time()
+        elif issubclass(python_type, datetime.date):
+            return return_value.date()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/encrypted/padding.py 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/encrypted/padding.py
--- old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/encrypted/padding.py    
1970-01-01 01:00:00.000000000 +0100
+++ new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/encrypted/padding.py     
2018-02-18 15:38:57.000000000 +0100
@@ -0,0 +1,124 @@
+import six
+
+
+class Padding(object):
+    """Base class for padding and unpadding."""
+
+    def __init__(self, block_size):
+        self.block_size = block_size
+
+    def pad(value):
+        raise NotImplementedError('Subclasses must implement this!')
+
+    def unpad(value):
+        raise NotImplementedError('Subclasses must implement this!')
+
+
+class PKCS5Padding(Padding):
+    """Provide PKCS5 padding and unpadding."""
+
+    def pad(self, value):
+        if not isinstance(value, six.binary_type):
+            value = value.encode()
+        padding_length = (self.block_size - len(value) % self.block_size)
+        padding_sequence = padding_length * six.b(chr(padding_length))
+        value_with_padding = value + padding_sequence
+
+        return value_with_padding
+
+    def unpad(self, value):
+        if isinstance(value, six.binary_type):
+            padding_length = value[-1]
+        if isinstance(value, six.string_types):
+            padding_length = ord(value[-1])
+        value_without_padding = value[0:-padding_length]
+
+        return value_without_padding
+
+
+class OneAndZeroesPadding(Padding):
+    """Provide the one and zeroes padding and unpadding.
+
+    This mechanism pads with 0x80 followed by zero bytes.
+    For unpadding it strips off all trailing zero bytes and the 0x80 byte.
+    """
+
+    BYTE_80 = 0x80
+    BYTE_00 = 0x00
+
+    def pad(self, value):
+        if not isinstance(value, six.binary_type):
+            value = value.encode()
+        padding_length = (self.block_size - len(value) % self.block_size)
+        one_part_bytes = six.b(chr(self.BYTE_80))
+        zeroes_part_bytes = (padding_length - 1) * six.b(chr(self.BYTE_00))
+        padding_sequence = one_part_bytes + zeroes_part_bytes
+        value_with_padding = value + padding_sequence
+
+        return value_with_padding
+
+    def unpad(self, value):
+        value_without_padding = value.rstrip(six.b(chr(self.BYTE_00)))
+        value_without_padding = value_without_padding.rstrip(
+            six.b(chr(self.BYTE_80)))
+
+        return value_without_padding
+
+
+class ZeroesPadding(Padding):
+    """Provide zeroes padding and unpadding.
+
+    This mechanism pads with 0x00 except the last byte equals
+    to the padding length. For unpadding it reads the last byte
+    and strips off that many bytes.
+    """
+
+    BYTE_00 = 0x00
+
+    def pad(self, value):
+        if not isinstance(value, six.binary_type):
+            value = value.encode()
+        padding_length = (self.block_size - len(value) % self.block_size)
+        zeroes_part_bytes = (padding_length - 1) * six.b(chr(self.BYTE_00))
+        last_part_bytes = six.b(chr(padding_length))
+        padding_sequence = zeroes_part_bytes + last_part_bytes
+        value_with_padding = value + padding_sequence
+
+        return value_with_padding
+
+    def unpad(self, value):
+        if isinstance(value, six.binary_type):
+            padding_length = value[-1]
+        if isinstance(value, six.string_types):
+            padding_length = ord(value[-1])
+        value_without_padding = value[0:-padding_length]
+
+        return value_without_padding
+
+
+class NaivePadding(Padding):
+    """Naive padding and unpadding using '*'.
+
+    The class is provided only for backwards compatibility.
+    """
+
+    CHARACTER = six.b('*')
+
+    def pad(self, value):
+        num_of_bytes = (self.block_size - len(value) % self.block_size)
+        value_with_padding = value + num_of_bytes * self.CHARACTER
+
+        return value_with_padding
+
+    def unpad(self, value):
+        value_without_padding = value.rstrip(self.CHARACTER)
+
+        return value_without_padding
+
+
+PADDING_MECHANISM = {
+    'pkcs5': PKCS5Padding,
+    'oneandzeroes': OneAndZeroesPadding,
+    'zeroes': ZeroesPadding,
+    'naive': NaivePadding
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/encrypted.py 
new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/encrypted.py
--- old/SQLAlchemy-Utils-0.32.21/sqlalchemy_utils/types/encrypted.py    
2017-09-01 08:58:42.000000000 +0200
+++ new/SQLAlchemy-Utils-0.33.0/sqlalchemy_utils/types/encrypted.py     
1970-01-01 01:00:00.000000000 +0100
@@ -1,329 +0,0 @@
-# -*- coding: utf-8 -*-
-import base64
-import datetime
-
-import six
-from sqlalchemy.types import LargeBinary, String, TypeDecorator
-
-from ..exceptions import ImproperlyConfigured
-from .scalar_coercible import ScalarCoercible
-
-cryptography = None
-try:
-    import cryptography
-    from cryptography.hazmat.backends import default_backend
-    from cryptography.hazmat.primitives import hashes
-    from cryptography.hazmat.primitives.ciphers import (
-        Cipher, algorithms, modes
-    )
-    from cryptography.fernet import Fernet
-except ImportError:
-    pass
-
-
-dateutil = None
-try:
-    import dateutil
-    from dateutil.parser import parse as datetime_parse
-except ImportError:
-    pass
-
-
-class EncryptionDecryptionBaseEngine(object):
-    """A base encryption and decryption engine.
-
-    This class must be sub-classed in order to create
-    new engines.
-    """
-
-    def _update_key(self, key):
-        if isinstance(key, six.string_types):
-            key = key.encode()
-        digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
-        digest.update(key)
-        engine_key = digest.finalize()
-
-        self._initialize_engine(engine_key)
-
-    def encrypt(self, value):
-        raise NotImplementedError('Subclasses must implement this!')
-
-    def decrypt(self, value):
-        raise NotImplementedError('Subclasses must implement this!')
-
-
-class AesEngine(EncryptionDecryptionBaseEngine):
-    """Provide AES encryption and decryption methods."""
-
-    BLOCK_SIZE = 16
-    PADDING = six.b('*')
-
-    def _initialize_engine(self, parent_class_key):
-        self.secret_key = parent_class_key
-        self.iv = self.secret_key[:16]
-        self.cipher = Cipher(
-            algorithms.AES(self.secret_key),
-            modes.CBC(self.iv),
-            backend=default_backend()
-        )
-
-    def _pad(self, value):
-        """Pad the message to be encrypted, if needed."""
-        BS = self.BLOCK_SIZE
-        P = self.PADDING
-        padded = (value + (BS - len(value) % BS) * P)
-        return padded
-
-    def encrypt(self, value):
-        if not isinstance(value, six.string_types):
-            value = repr(value)
-        if isinstance(value, six.text_type):
-            value = str(value)
-        value = value.encode()
-        value = self._pad(value)
-        encryptor = self.cipher.encryptor()
-        encrypted = encryptor.update(value) + encryptor.finalize()
-        encrypted = base64.b64encode(encrypted)
-        return encrypted
-
-    def decrypt(self, value):
-        if isinstance(value, six.text_type):
-            value = str(value)
-        decryptor = self.cipher.decryptor()
-        decrypted = base64.b64decode(value)
-        decrypted = decryptor.update(decrypted) + decryptor.finalize()
-        decrypted = decrypted.rstrip(self.PADDING)
-        if not isinstance(decrypted, six.string_types):
-            try:
-                decrypted = decrypted.decode('utf-8')
-            except UnicodeDecodeError:
-                raise ValueError('Invalid decryption key')
-        return decrypted
-
-
-class FernetEngine(EncryptionDecryptionBaseEngine):
-    """Provide Fernet encryption and decryption methods."""
-
-    def _initialize_engine(self, parent_class_key):
-        self.secret_key = base64.urlsafe_b64encode(parent_class_key)
-        self.fernet = Fernet(self.secret_key)
-
-    def encrypt(self, value):
-        if not isinstance(value, six.string_types):
-            value = repr(value)
-        if isinstance(value, six.text_type):
-            value = str(value)
-        value = value.encode()
-        encrypted = self.fernet.encrypt(value)
-        return encrypted
-
-    def decrypt(self, value):
-        if isinstance(value, six.text_type):
-            value = str(value)
-        decrypted = self.fernet.decrypt(value)
-        if not isinstance(decrypted, six.string_types):
-            decrypted = decrypted.decode('utf-8')
-        return decrypted
-
-
-class EncryptedType(TypeDecorator, ScalarCoercible):
-    """
-    EncryptedType provides a way to encrypt and decrypt values,
-    to and from databases, that their type is a basic SQLAlchemy type.
-    For example Unicode, String or even Boolean.
-    On the way in, the value is encrypted and on the way out the stored value
-    is decrypted.
-
-    EncryptedType needs Cryptography_ library in order to work.
-    A simple example is given below.
-
-    .. _Cryptography: https://cryptography.io/en/latest/
-
-    ::
-
-        import sqlalchemy as sa
-        from sqlalchemy.ext.declarative import declarative_base
-        from sqlalchemy import create_engine
-        from sqlalchemy.orm import sessionmaker
-        from sqlalchemy_utils import EncryptedType
-
-
-        secret_key = 'secretkey1234'
-        # setup
-        engine = create_engine('sqlite:///:memory:')
-        connection = engine.connect()
-        Base = declarative_base()
-
-        class User(Base):
-            __tablename__ = "user"
-            id = sa.Column(sa.Integer, primary_key=True)
-            username = sa.Column(EncryptedType(sa.Unicode, secret_key))
-            access_token = sa.Column(EncryptedType(sa.String, secret_key))
-            is_active = sa.Column(EncryptedType(sa.Boolean, secret_key))
-            number_of_accounts = sa.Column(EncryptedType(sa.Integer,
-                                                         secret_key))
-
-        sa.orm.configure_mappers()
-        Base.metadata.create_all(connection)
-
-        # create a configured "Session" class
-        Session = sessionmaker(bind=connection)
-
-        # create a Session
-        session = Session()
-
-        # example
-        user_name = u'secret_user'
-        test_token = 'atesttoken'
-        active = True
-        num_of_accounts = 2
-
-        user = User(username=user_name, access_token=test_token,
-                    is_active=active, accounts_num=accounts)
-        session.add(user)
-        session.commit()
-
-        print('id: {}'.format(user.id))
-        print('username: {}'.format(user.username))
-        print('token: {}'.format(user.access_token))
-        print('active: {}'.format(user.is_active))
-        print('accounts: {}'.format(user.accounts_num))
-
-        # teardown
-        session.close_all()
-        Base.metadata.drop_all(connection)
-        connection.close()
-        engine.dispose()
-
-    The key parameter accepts a callable to allow for the key to change
-    per-row instead of be fixed for the whole table.
-
-    ::
-        def get_key():
-            return 'dynamic-key'
-
-        class User(Base):
-            __tablename__ = 'user'
-            id = sa.Column(sa.Integer, primary_key=True)
-            username = sa.Column(EncryptedType(
-                sa.Unicode, get_key))
-
-    """
-
-    impl = LargeBinary
-
-    def __init__(self, type_in=None, key=None, engine=None, **kwargs):
-        """Initialization."""
-        if not cryptography:
-            raise ImproperlyConfigured(
-                "'cryptography' is required to use EncryptedType"
-            )
-        super(EncryptedType, self).__init__(**kwargs)
-        # set the underlying type
-        if type_in is None:
-            type_in = String()
-        elif isinstance(type_in, type):
-            type_in = type_in()
-        self.underlying_type = type_in
-        self._key = key
-        if not engine:
-            engine = AesEngine
-        self.engine = engine()
-
-    @property
-    def key(self):
-        return self._key
-
-    @key.setter
-    def key(self, value):
-        self._key = value
-
-    def _update_key(self):
-        key = self._key() if callable(self._key) else self._key
-        self.engine._update_key(key)
-
-    def process_bind_param(self, value, dialect):
-        """Encrypt a value on the way in."""
-        if value is not None:
-            self._update_key()
-
-            try:
-                value = self.underlying_type.process_bind_param(
-                    value, dialect
-                )
-
-            except AttributeError:
-                # Doesn't have 'process_bind_param'
-
-                # Handle 'boolean' and 'dates'
-                type_ = self.underlying_type.python_type
-                if issubclass(type_, bool):
-                    value = 'true' if value else 'false'
-
-                elif issubclass(type_, (datetime.date, datetime.time)):
-                    value = value.isoformat()
-
-            return self.engine.encrypt(value)
-
-    def process_result_value(self, value, dialect):
-        """Decrypt value on the way out."""
-        if value is not None:
-            self._update_key()
-            decrypted_value = self.engine.decrypt(value)
-
-            try:
-                return self.underlying_type.process_result_value(
-                    decrypted_value, dialect
-                )
-
-            except AttributeError:
-                # Doesn't have 'process_result_value'
-
-                # Handle 'boolean' and 'dates'
-                type_ = self.underlying_type.python_type
-                date_types = [datetime.datetime, datetime.time, datetime.date]
-
-                if issubclass(type_, bool):
-                    return decrypted_value == 'true'
-
-                elif type_ in date_types:
-                    return DatetimeHandler.process_value(
-                        decrypted_value, type_
-                    )
-
-                # Handle all others
-                return self.underlying_type.python_type(decrypted_value)
-
-    def _coerce(self, value):
-        if isinstance(self.underlying_type, ScalarCoercible):
-            return self.underlying_type._coerce(value)
-
-        return value
-
-
-class DatetimeHandler(object):
-    """
-    DatetimeHandler is responsible for parsing strings and
-    returning the appropriate date, datetime or time objects.
-    """
-
-    @classmethod
-    def process_value(cls, value, python_type):
-        """
-        process_value returns a datetime, date
-        or time object according to a given string
-        value and a python type.
-        """
-        if not dateutil:
-            raise ImproperlyConfigured(
-                "'python-dateutil' is required to process datetimes"
-            )
-
-        return_value = datetime_parse(value)
-
-        if issubclass(python_type, datetime.datetime):
-            return return_value
-        elif issubclass(python_type, datetime.time):
-            return return_value.time()
-        elif issubclass(python_type, datetime.date):
-            return return_value.date()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/tests/primitives/test_ltree.py 
new/SQLAlchemy-Utils-0.33.0/tests/primitives/test_ltree.py
--- old/SQLAlchemy-Utils-0.32.21/tests/primitives/test_ltree.py 2016-04-25 
20:10:56.000000000 +0200
+++ new/SQLAlchemy-Utils-0.33.0/tests/primitives/test_ltree.py  2018-02-18 
15:34:52.000000000 +0100
@@ -141,6 +141,37 @@
     def test_contains(self, path, other, result):
         assert (other in Ltree(path)) == result
 
+    @pytest.mark.parametrize(
+        ('path', 'other', 'result'),
+        (
+            ('1', '1.2.3', True),
+            ('1.2', '1.2.3', True),
+            ('1.2.3', '1.2.3', True),
+            ('1.2.3', '1', False),
+            ('1.2.3', '1.2', False),
+            ('1', '1', True),
+            ('1', '2', False),
+        )
+    )
+    def test_ancestor_of(self, path, other, result):
+        assert Ltree(path).ancestor_of(other) == result
+
+    @pytest.mark.parametrize(
+        ('path', 'other', 'result'),
+        (
+            ('1', '1.2.3', False),
+            ('1.2', '1.2.3', False),
+            ('1.2', '1.2.3', False),
+            ('1.2.3', '1', True),
+            ('1.2.3', '1.2', True),
+            ('1.2.3', '1.2.3', True),
+            ('1', '1', True),
+            ('1', '2', False),
+        )
+    )
+    def test_descendant_of(self, path, other, result):
+        assert Ltree(path).descendant_of(other) == result
+
     def test_getitem_with_other_than_slice_or_in(self):
         with pytest.raises(TypeError):
             Ltree('1.2')['something']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/tests/test_materialized_view.py 
new/SQLAlchemy-Utils-0.33.0/tests/test_materialized_view.py
--- old/SQLAlchemy-Utils-0.32.21/tests/test_materialized_view.py        
1970-01-01 01:00:00.000000000 +0100
+++ new/SQLAlchemy-Utils-0.33.0/tests/test_materialized_view.py 2018-02-18 
15:32:21.000000000 +0100
@@ -0,0 +1,76 @@
+import pytest
+import sqlalchemy as sa
+
+from sqlalchemy_utils import (
+    create_materialized_view,
+    refresh_materialized_view
+)
+
+
[email protected]
+def Article(Base, User):
+    class Article(Base):
+        __tablename__ = 'article'
+        id = sa.Column(sa.Integer, primary_key=True)
+        name = sa.Column(sa.String)
+        author_id = sa.Column(sa.Integer, sa.ForeignKey(User.id))
+        author = sa.orm.relationship(User)
+    return Article
+
+
[email protected]
+def User(Base):
+    class User(Base):
+        __tablename__ = 'user'
+        id = sa.Column(sa.Integer, primary_key=True)
+        name = sa.Column(sa.String)
+    return User
+
+
[email protected]
+def ArticleMV(Base, Article, User):
+    class ArticleMV(Base):
+        __table__ = create_materialized_view(
+            name='article_mv',
+            selectable=sa.select(
+                [
+                    Article.id,
+                    Article.name,
+                    User.id.label('author_id'),
+                    User.name.label('author_name')
+                ],
+                from_obj=(
+                    Article.__table__
+                    .join(User, Article.author_id == User.id)
+                )
+            ),
+            metadata=Base.metadata,
+            indexes=[sa.Index('article_mv_id_idx', 'id')]
+        )
+    return ArticleMV
+
+
[email protected]
+def init_models(ArticleMV):
+    pass
+
+
[email protected]('postgresql_dsn')
+class TestMaterializedViews:
+    def test_refresh_materialized_view(
+        self,
+        session,
+        Article,
+        User,
+        ArticleMV
+    ):
+        article = Article(
+            name='Some article',
+            author=User(name='Some user')
+        )
+        session.add(article)
+        session.commit()
+        refresh_materialized_view(session, 'article_mv')
+        materialized = session.query(ArticleMV).first()
+        assert materialized.name == 'Some article'
+        assert materialized.author_name == 'Some user'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/SQLAlchemy-Utils-0.32.21/tests/types/test_encrypted.py 
new/SQLAlchemy-Utils-0.33.0/tests/types/test_encrypted.py
--- old/SQLAlchemy-Utils-0.32.21/tests/types/test_encrypted.py  2017-09-01 
08:58:42.000000000 +0200
+++ new/SQLAlchemy-Utils-0.33.0/tests/types/test_encrypted.py   2018-02-18 
15:38:57.000000000 +0100
@@ -4,7 +4,7 @@
 import sqlalchemy as sa
 
 from sqlalchemy_utils import ColorType, EncryptedType, PhoneNumberType
-from sqlalchemy_utils.types.encrypted import (
+from sqlalchemy_utils.types.encrypted.encrypted_type import (
     AesEngine,
     DatetimeHandler,
     FernetEngine
@@ -18,7 +18,7 @@
 
 
 @pytest.fixture
-def User(Base, encryption_engine, test_key):
+def User(Base, encryption_engine, test_key, padding_mechanism):
     class User(Base):
         __tablename__ = 'user'
         id = sa.Column(sa.Integer, primary_key=True)
@@ -26,61 +26,71 @@
         username = sa.Column(EncryptedType(
             sa.Unicode,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         access_token = sa.Column(EncryptedType(
             sa.String,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         is_active = sa.Column(EncryptedType(
             sa.Boolean,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         accounts_num = sa.Column(EncryptedType(
             sa.Integer,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         phone = sa.Column(EncryptedType(
             PhoneNumberType,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         color = sa.Column(EncryptedType(
             ColorType,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         date = sa.Column(EncryptedType(
             sa.Date,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         time = sa.Column(EncryptedType(
             sa.Time,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         datetime = sa.Column(EncryptedType(
             sa.DateTime,
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
         enum = sa.Column(EncryptedType(
             sa.Enum('One', name='user_enum_t'),
             test_key,
-            encryption_engine)
+            encryption_engine,
+            padding_mechanism)
         )
 
     return User
@@ -217,7 +227,7 @@
 class EncryptedTypeTestCase(object):
 
     @pytest.fixture
-    def Team(self, Base, encryption_engine):
+    def Team(self, Base, encryption_engine, padding_mechanism):
         self._team_key = None
 
         class Team(Base):
@@ -227,7 +237,8 @@
             name = sa.Column(EncryptedType(
                 sa.Unicode,
                 lambda: self._team_key,
-                encryption_engine)
+                encryption_engine,
+                padding_mechanism)
             )
         return Team
 
@@ -289,9 +300,6 @@
 
         assert team.name == u'One'
 
-        with pytest.raises(Exception):
-            session.query(Team).get(team_2_id)
-
         session.expunge_all()
 
         self._team_key = session.query(Team.key).filter_by(
@@ -302,9 +310,6 @@
 
         assert team.name == u'Two'
 
-        with pytest.raises(Exception):
-            session.query(Team).get(team_1_id)
-
         session.expunge_all()
 
         # Remove teams
@@ -312,7 +317,7 @@
         session.commit()
 
 
-class TestAesEncryptedTypeTestcase(EncryptedTypeTestCase):
+class AesEncryptedTypeTestCase(EncryptedTypeTestCase):
 
     @pytest.fixture
     def encryption_engine(self):
@@ -325,6 +330,34 @@
 
         assert test.username == user.username
 
+
+class TestAesEncryptedTypeWithPKCS5Padding(AesEncryptedTypeTestCase):
+
+    @pytest.fixture
+    def padding_mechanism(self):
+        return 'pkcs5'
+
+
+class TestAesEncryptedTypeWithOneAndZeroesPadding(AesEncryptedTypeTestCase):
+
+    @pytest.fixture
+    def padding_mechanism(self):
+        return 'oneandzeroes'
+
+
+class TestAesEncryptedTypeWithZeroesPadding(AesEncryptedTypeTestCase):
+
+    @pytest.fixture
+    def padding_mechanism(self):
+        return 'zeroes'
+
+
+class TestAesEncryptedTypeTestcaseWithNaivePadding(AesEncryptedTypeTestCase):
+
+    @pytest.fixture
+    def padding_mechanism(self):
+        return 'naive'
+
     def test_decrypt_raises_value_error_with_invalid_key(self, session, Team):
         self._team_key = 'one'
         team = Team(key=self._team_key, name=u'One')
@@ -342,6 +375,10 @@
     def encryption_engine(self):
         return FernetEngine
 
+    @pytest.fixture
+    def padding_mechanism(self):
+        return None
+
 
 class TestDatetimeHandler(object):
 


Reply via email to