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):