diff --git a/CHANGES.rst b/CHANGES.rst index 09337d4..a28f29c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,24 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Utils release. +0.14.7 (2013-07-22) +^^^^^^^^^^^^^^^^^^^ + +- Lazy import for ipaddress package + + +0.14.6 (2013-07-22) +^^^^^^^^^^^^^^^^^^^ + +- Fixed UUID import issues + + +0.14.5 (2013-07-22) +^^^^^^^^^^^^^^^^^^^ + +- Added UUID type + + 0.14.4 (2013-07-03) ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index a7584bf..495ee72 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -173,6 +173,24 @@ NumberRange supports some arithmetic operators: # '30-140' +UUIDType +-------- + +UUIDType will store a UUID in the database in a native format, if available, +or a 16-byte BINARY column or a 32-character CHAR column if not. + +:: + + from sqlalchemy_utils import UUIDType + import uuid + + class User(Base): + __tablename__ = 'user' + + # Pass `binary=False` to fallback to CHAR instead of BINARY + id = sa.Column(UUIDType(binary=False), primary_key=True) + + API Documentation ----------------- diff --git a/setup.py b/setup.py index 93ec010..b97f09f 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class PyTest(Command): setup( name='SQLAlchemy-Utils', - version='0.14.4', + version='0.14.7', url='https://github.com/kvesteri/sqlalchemy-utils', license='BSD', author='Konsta Vesterinen', @@ -33,7 +33,7 @@ setup( 'Various utility functions for SQLAlchemy.' ), long_description=__doc__, - packages=['sqlalchemy_utils'], + packages=['sqlalchemy_utils', 'sqlalchemy_utils.types'], zip_safe=False, include_package_data=True, platforms='any', @@ -55,8 +55,8 @@ setup( 'flexmock>=0.9.7', ], 'phone': ['phonenumbers3k==5.6b1'], - 'color': ['colour>=0.0.3'], - 'password': ['passlib >= 1.6, < 2.0'] + 'password': ['passlib >= 1.6, < 2.0'], + 'color': ['colour>=0.0.4'] }, cmdclass={'test': PyTest}, classifiers=[ diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index f57977a..e2869ab 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -21,11 +21,12 @@ from .types import ( NumberRangeType, ScalarListType, ScalarListException, - TSVectorType + TSVectorType, + UUIDType, ) -__version__ = '0.14.4' +__version__ = '0.14.7' __all__ = ( @@ -48,11 +49,13 @@ __all__ = ( NumberRangeException, NumberRangeRawType, NumberRangeType, + Password, PasswordType, PhoneNumber, PhoneNumberType, ProxyDict, ScalarListType, ScalarListException, - TSVectorType + TSVectorType, + UUIDType, ) diff --git a/sqlalchemy_utils/types/__init__.py b/sqlalchemy_utils/types/__init__.py index 33cc4f4..c8c60ae 100644 --- a/sqlalchemy_utils/types/__init__.py +++ b/sqlalchemy_utils/types/__init__.py @@ -14,6 +14,7 @@ from .number_range import ( from .password import Password, PasswordType from .phone_number import PhoneNumber, PhoneNumberType from .scalar_list import ScalarListException, ScalarListType +from .uuid import UUIDType __all__ = ( @@ -30,6 +31,7 @@ __all__ = ( PhoneNumberType, ScalarListException, ScalarListType, + UUIDType, ) diff --git a/sqlalchemy_utils/types/color.py b/sqlalchemy_utils/types/color.py index 6f77816..689ce9a 100644 --- a/sqlalchemy_utils/types/color.py +++ b/sqlalchemy_utils/types/color.py @@ -24,7 +24,8 @@ class ColorType(types.TypeDecorator): # Bail if colour is not found. if colour is None: raise ImproperlyConfigured( - "'colour' is required to use 'ColorType'") + "'colour' package is required to use 'ColorType'" + ) super(ColorType, self).__init__(*args, **kwargs) self.impl = types.Unicode(max_length) diff --git a/sqlalchemy_utils/types/ip_address.py b/sqlalchemy_utils/types/ip_address.py index 457d2ca..8065762 100644 --- a/sqlalchemy_utils/types/ip_address.py +++ b/sqlalchemy_utils/types/ip_address.py @@ -1,6 +1,12 @@ import six -import ipaddress + +ipaddress = None +try: + import ipaddress +except: + pass from sqlalchemy import types +from sqlalchemy_utils import ImproperlyConfigured class IPAddressType(types.TypeDecorator): @@ -11,6 +17,11 @@ class IPAddressType(types.TypeDecorator): impl = types.Unicode(50) def __init__(self, max_length=50, *args, **kwargs): + if not ipaddress: + raise ImproperlyConfigured( + "'ipaddress' package is required to use 'IPAddressType'" + ) + super(IPAddressType, self).__init__(*args, **kwargs) self.impl = types.Unicode(max_length) diff --git a/sqlalchemy_utils/types/uuid.py b/sqlalchemy_utils/types/uuid.py new file mode 100644 index 0000000..71368b7 --- /dev/null +++ b/sqlalchemy_utils/types/uuid.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import +import uuid +from sqlalchemy import types +from sqlalchemy.dialects import postgresql + + +class UUIDType(types.TypeDecorator): + """ + Stores a UUID in the database natively when it can and falls back to + a BINARY(16) or a CHAR(32) when it can't. + """ + + impl = types.BINARY(16) + + python_type = uuid.UUID + + def __init__(self, binary=True): + """ + :param binary: Whether to use a BINARY(16) or CHAR(32) fallback. + """ + self.binary = binary + + def load_dialect_impl(self, dialect): + if dialect.name == 'postgresql': + # Use the native UUID type. + return dialect.type_descriptor(postgresql.UUID()) + + else: + # Fallback to either a BINARY or a CHAR. + kind = self.impl if self.binary else types.CHAR(32) + return dialect.type_descriptor(kind) + + @staticmethod + def _coerce(value): + if value and not isinstance(value, uuid.UUID): + try: + value = uuid.UUID(value) + + except (TypeError, ValueError): + value = uuid.UUID(bytes=value) + + return value + + def process_bind_param(self, value, dialect): + if value is None: + return value + + if not isinstance(value, uuid.UUID): + value = self._coerce(value) + + if dialect == 'postgresql': + return str(value) + + return value.bytes if self.binary else value.hex + + def process_result_value(self, value, dialect): + if value is None: + return value + + if dialect == 'postgresql': + return uuid.UUID(value) + + return uuid.UUID(bytes=value) if self.binary else uuid.UUID(value) + + def coercion_listener(self, target, value, oldvalue, initiator): + return self._coerce(value) diff --git a/tests/test_uuid.py b/tests/test_uuid.py new file mode 100644 index 0000000..e07465e --- /dev/null +++ b/tests/test_uuid.py @@ -0,0 +1,41 @@ +import sqlalchemy as sa +from tests import TestCase +from sqlalchemy_utils import UUIDType, coercion_listener +import uuid + + +class TestUUIDType(TestCase): + + def create_models(self): + class User(self.Base): + __tablename__ = 'user' + id = sa.Column(UUIDType, default=uuid.uuid4, primary_key=True) + + def __repr__(self): + return 'User(%r)' % self.id + + self.User = User + sa.event.listen(sa.orm.mapper, 'mapper_configured', coercion_listener) + + def test_commit(self): + obj = self.User() + obj.id = uuid.uuid4().hex + + self.session.add(obj) + self.session.commit() + + u = self.session.query(self.User).one() + + assert u.id == obj.id + + def test_coerce(self): + obj = self.User() + obj.id = identifier = uuid.uuid4().hex + + assert isinstance(obj.id, uuid.UUID) + assert obj.id.hex == identifier + + obj.id = identifier = uuid.uuid4().bytes + + assert isinstance(obj.id, uuid.UUID) + assert obj.id.bytes == identifier