You're thinking of hybrid properties
(see the docs). These can be used to exhibit different behaviour in SQL vs python settings, but can also be used to predefine certain transformations. I've often used them to transform UTC timestamps to the local timezone. Note that you define the property 1-3 times. Once as a python property, once for how you would like the SQL to function, and once for a setter.
import pytz
from sqlalchemy.ext.hybrid import hybrid_property
class Appointment(Base):
scheduled_date_utc = Column(DateTime) # Naive UTC
scheduled_date_timezone = Column(TimezoneType()) # TimezoneType is from sqlalchemy-utils
@property
def scheduled_date(self) -> datetime:
# see https://stackoverflow.com/a/18646797/5015356
return self.scheduled_date_utc\
.replace(tzinfo=pytz.utc)\
.astimezone(pytz.timezone(self.scheduled_date_timezone))
@scheduled_date.expr
def scheduled_date(cls):
return func.timezone(cls.scheduled_date_timezone, cls.scheduled_date_utc)
To make the solution reusable, you could write a mixin with a wrapper around __setattr__
:
import pytz
class TimeZoneMixin:
def is_timezone_aware_attr(self, attr):
return hasattr(self, attr + '_utc') and hasattr(self, attr + '_timezone')
def __getattr__(self, attr):
"""
__getattr__ is only called as a last resort, if no other
matching columns exist
"""
if self.is_timezone_aware_attr(attr):
return func.timezone(getattr(self, attr + '_utc'),
getattr(self, attr + '_timezone'))
raise AttributeError()
def __setattr__(self, attr, value):
if self.is_timezone_aware_attr(attr):
setattr(self, attr + '_utc', value.astimezone(tzinfo=pytz.utc))
setattr(self, attr + '_utc', value.tzinfo)
raise AttributeError()
Or to make it use only one shared timezone
object:
import pytz
class TimeZoneMixin:
timezone = Column(TimezoneType())
def is_timezone_aware_attr(self, attr):
return hasattr(self, attr + '_utc')
def __getattr__(self, attr):
"""
__getattr__ is only called as a last resort, if no other
matching columns exist
"""
if self.is_timezone_aware_attr(attr):
return func.timezone(getattr(self, attr + '_utc'), self.timezone)
raise AttributeError()
def __setattr__(self, attr, value):
if self.is_timezone_aware_attr(attr):
setattr(self, attr + '_utc', value.astimezone(tzinfo=pytz.utc))
self.timezone = value.tzinfo
raise AttributeError()