Merge pull request #29 from kvesteri/topics/timezone
Add support for a timezone type.
This commit is contained in:
@@ -191,6 +191,24 @@ or a 16-byte BINARY column or a 32-character CHAR column if not.
|
|||||||
id = sa.Column(UUIDType(binary=False), primary_key=True)
|
id = sa.Column(UUIDType(binary=False), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
TimezoneType
|
||||||
|
------------
|
||||||
|
|
||||||
|
TimezoneType provides a way for saving timezones (from either the pytz or the dateutil package) objects into database.
|
||||||
|
TimezoneType saves timezone objects as strings on the way in and converts them back to objects when querying the database.
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from sqlalchemy_utils import UUIDType
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = 'user'
|
||||||
|
|
||||||
|
# Pass backend='pytz' to change it to use pytz (dateutil by default)
|
||||||
|
timezone = sa.Column(TimezoneType(backend='pytz'))
|
||||||
|
|
||||||
|
|
||||||
API Documentation
|
API Documentation
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
3
setup.py
3
setup.py
@@ -42,7 +42,8 @@ extras_require = {
|
|||||||
],
|
],
|
||||||
'password': ['passlib >= 1.6, < 2.0'],
|
'password': ['passlib >= 1.6, < 2.0'],
|
||||||
'color': ['colour>=0.0.4'],
|
'color': ['colour>=0.0.4'],
|
||||||
'ipaddress': ['ipaddr'] if not PY3 else []
|
'ipaddress': ['ipaddr'] if not PY3 else [],
|
||||||
|
'timezone': ['python-dateutil']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ from .types import (
|
|||||||
NumberRangeType,
|
NumberRangeType,
|
||||||
ScalarListType,
|
ScalarListType,
|
||||||
ScalarListException,
|
ScalarListException,
|
||||||
|
TimezoneType,
|
||||||
TSVectorType,
|
TSVectorType,
|
||||||
UUIDType,
|
UUIDType,
|
||||||
)
|
)
|
||||||
@@ -58,6 +59,7 @@ __all__ = (
|
|||||||
ProxyDict,
|
ProxyDict,
|
||||||
ScalarListType,
|
ScalarListType,
|
||||||
ScalarListException,
|
ScalarListException,
|
||||||
|
TimezoneType,
|
||||||
TSVectorType,
|
TSVectorType,
|
||||||
UUIDType,
|
UUIDType,
|
||||||
)
|
)
|
||||||
|
@@ -15,6 +15,7 @@ from .number_range import (
|
|||||||
from .password import Password, PasswordType
|
from .password import Password, PasswordType
|
||||||
from .phone_number import PhoneNumber, PhoneNumberType
|
from .phone_number import PhoneNumber, PhoneNumberType
|
||||||
from .scalar_list import ScalarListException, ScalarListType
|
from .scalar_list import ScalarListException, ScalarListType
|
||||||
|
from .timezone import TimezoneType
|
||||||
from .uuid import UUIDType
|
from .uuid import UUIDType
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ __all__ = (
|
|||||||
PhoneNumberType,
|
PhoneNumberType,
|
||||||
ScalarListException,
|
ScalarListException,
|
||||||
ScalarListType,
|
ScalarListType,
|
||||||
|
TimezoneType,
|
||||||
UUIDType,
|
UUIDType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
74
sqlalchemy_utils/types/timezone.py
Normal file
74
sqlalchemy_utils/types/timezone.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import six
|
||||||
|
from sqlalchemy import types
|
||||||
|
from sqlalchemy_utils import ImproperlyConfigured
|
||||||
|
|
||||||
|
|
||||||
|
class TimezoneType(types.TypeDecorator):
|
||||||
|
"""
|
||||||
|
Changes Timezone objects to a string representation on the way in and
|
||||||
|
changes them back to Timezone objects on the way out.
|
||||||
|
"""
|
||||||
|
|
||||||
|
impl = types.CHAR(50)
|
||||||
|
|
||||||
|
python_type = None
|
||||||
|
|
||||||
|
def __init__(self, backend='dateutil'):
|
||||||
|
"""
|
||||||
|
:param backend: Whether to use 'dateutil' or 'pytz' for timezones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.backend = backend
|
||||||
|
if backend == 'dateutil':
|
||||||
|
try:
|
||||||
|
from dateutil.tz import tzfile
|
||||||
|
from dateutil.zoneinfo import gettz
|
||||||
|
|
||||||
|
self.python_type = tzfile
|
||||||
|
self._to = gettz
|
||||||
|
self._from = lambda x: x._filename
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"'python-dateutil' is required to use the "
|
||||||
|
"'dateutil' backend for 'TimezoneType'"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif backend == 'pytz':
|
||||||
|
try:
|
||||||
|
from pytz import tzfile, timezone
|
||||||
|
|
||||||
|
self.python_type = tzfile.DstTzInfo
|
||||||
|
self._to = timezone
|
||||||
|
self._from = six.text_type
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"'pytz' is required to use the 'pytz' backend "
|
||||||
|
"for 'TimezoneType'"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"'pytz' or 'dateutil' are the backends supported for "
|
||||||
|
"'TimezoneType'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _coerce(self, value):
|
||||||
|
if value and not isinstance(value, self.python_type):
|
||||||
|
obj = self._to(value)
|
||||||
|
if obj is None:
|
||||||
|
raise ValueError("unknown time zone '%s'" % value)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def coercion_listener(self, target, value, oldvalue, initiator):
|
||||||
|
return self._coerce(value)
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
return self._from(self._coerce(value)) if value else None
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
return self._to(value) if value else None
|
39
tests/test_timezone.py
Normal file
39
tests/test_timezone.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from pytest import mark
|
||||||
|
import six
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy_utils.types import timezone
|
||||||
|
from tests import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import dateutil
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
dateutil = None
|
||||||
|
|
||||||
|
|
||||||
|
@mark.skipif('dateutil is None')
|
||||||
|
class TestTimezoneType(TestCase):
|
||||||
|
def create_models(self):
|
||||||
|
class Visitor(self.Base):
|
||||||
|
__tablename__ = 'document'
|
||||||
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
|
timezone = sa.Column(timezone.TimezoneType)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Visitor(%r)' % self.id
|
||||||
|
|
||||||
|
self.Visitor = Visitor
|
||||||
|
|
||||||
|
def test_parameter_processing(self):
|
||||||
|
visitor = self.Visitor(
|
||||||
|
timezone=u'America/Los_Angeles'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.session.add(visitor)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
visitor = self.session.query(self.Visitor).filter_by(
|
||||||
|
timezone='America/Los_Angeles').first()
|
||||||
|
|
||||||
|
assert visitor is not None
|
Reference in New Issue
Block a user