diff --git a/edbob/pyramid/forms/formalchemy/__init__.py b/edbob/pyramid/forms/formalchemy/__init__.py index 4715a96..77cd0b4 100644 --- a/edbob/pyramid/forms/formalchemy/__init__.py +++ b/edbob/pyramid/forms/formalchemy/__init__.py @@ -104,7 +104,7 @@ def required(value, field=None): raise formalchemy.ValidationError(msg) -def pretty_datetime(value): +def pretty_datetime(value, from_='local', to='local'): """ Formats a ``datetime.datetime`` instance and returns a "pretty" human-readable string from it, e.g. "42 minutes ago". ``value`` is @@ -113,10 +113,10 @@ def pretty_datetime(value): if not isinstance(value, datetime.datetime): return str(value) if value else '' - value = edbob.local_time(value) - fmt = formalchemy.fields.DateTimeFieldRenderer.format + if not value.tzinfo: + value = edbob.local_time(value, from_=from_, to=to) return literal('%s' % ( - value.strftime(fmt), + value.strftime('%Y-%m-%d %H:%M:%S %Z%z'), pretty.date(value))) diff --git a/edbob/time.py b/edbob/time.py index 1f0d63a..46bfeca 100644 --- a/edbob/time.py +++ b/edbob/time.py @@ -29,91 +29,150 @@ import datetime import pytz import logging +import warnings + +import edbob -__all__ = ['local_time', 'set_timezone', 'utc_time'] +__all__ = ['utc_time', 'local_time'] + +timezones = {} log = logging.getLogger(__name__) -timezone = None - def init(config): """ - Initializes the time framework. Currently this only sets the local - timezone according to config. - """ - - tz = config.get('edbob.time', 'timezone') - if tz: - set_timezone(tz) - log.info("Timezone set to '%s'" % tz) - else: - log.warning("No timezone configured; falling back to US/Central") - set_timezone('US/Central') - - -def local_time(timestamp=None): - """ - Tries to return a "localized" version of ``timestamp``, which should be a - UTC-based ``datetime.datetime`` instance. - - If a local timezone has been configured, then - ``datetime.datetime.utcnow()`` will be called to obtain a value for - ``timestamp`` if one is not specified. Then ``timestamp`` will be modified - in such a way that its ``tzinfo`` member contains the local timezone, but - the effective UTC value for the timestamp remains accurate. - - If a local timezone has *not* been configured, then - ``datetime.datetime.now()`` will be called instead to obtain the value - should none be specified. ``timestamp`` will be returned unchanged. - """ - - if timezone: - if timestamp is None: - timestamp = datetime.datetime.utcnow() - timestamp = pytz.utc.localize(timestamp) - return timestamp.astimezone(timezone) - - if timestamp is None: - timestamp = datetime.datetime.now() - return timestamp - - -def set_timezone(tz): - """ - Sets edbob's notion of the "local" timezone. ``tz`` should be an Olson - name. + Reads configuration to become aware of all timezones which may concern the + application. .. highlight:: ini - You usually don't need to call this yourself, since it's called by - :func:`edbob.init()` whenever the config file includes a timezone (but - only as long as ``edbob.time`` is configured to be initialized):: - - [edbob] - init = edbob.time + The bare minimum configuration required is the ``zone.local`` setting:: [edbob.time] - timezone = US/Central + zone.local = US/Pacific + + Multiple timezones may be configured like so:: + + [edbob.time] + zone.local = America/Los_Angeles + zone.head_office = America/New_York + zone.that_other_place = Asia/Manila + zone.some_app = US/Central + + See `Wikipedia + `_ for a + (presumably) full list of valid timezone names. + + .. note:: + A ``zone.utc`` option is automatically created for you; there is no need + to define it. """ - global timezone + for key in config.options('edbob.time'): + if key.startswith('zone.'): + tz = config.get('edbob.time', key) + if tz: + key = key[5:] + log.info("'%s' timezone set to '%s'" % (key, tz)) + set_timezone(tz, key) - if tz is None: - timezone = None - else: - timezone = pytz.timezone(tz) + if 'local' not in timezones: + tz = config.get('edbob.time', 'timezone') + if tz: + warnings.warn("Config option 'timezone' in 'edbob.time' section is deprecated. " + "Please set 'zone.local' instead.", + DeprecationWarning) + set_timezone(tz) + else: + log.warning("'local' timezone not configured; falling back to 'America/Chicago'") + set_timezone('America/Chicago') + + set_timezone('UTC', 'utc') -def utc_time(timestamp=None): +def get_timezone(key='local'): """ - Returns a timestamp whose ``tzinfo`` member is set to the UTC timezone. - - If ``timestamp`` is not provided, then ``datetime.datetime.utcnow()`` will - be called to obtain the value. + Returns the timezone referenced by ``key``. """ - if timestamp is None: - timestamp = datetime.datetime.utcnow() - return pytz.utc.localize(timestamp) + if key not in timezones: + edbob.config.require('edbob.time', 'zone.%s' % key) + return timezones[key] + + +def set_timezone(tz, key='local'): + """ + Stores a timezone in the global dictionary, using ``key``. + + ``tz`` must be a valid "Olson" time zone name, e.g. ``'US/Central'``. See + `Wikipedia `_ + for a (presumably) full list of valid names. + """ + + timezones[key] = pytz.timezone(tz) + + +def local_time(stamp=None, from_='local', naive=False): + """ + Returns a :class:`datetime.datetime` instance, with its ``tzinfo`` member + set to the timezone referenced by the key ``'local'``. If ``naive`` is + ``True``, the result is stripped of its ``tzinfo`` member. + + If ``stamp`` is provided, and it is not already "aware," then its value is + interpreted as being local to the timezone referenced by ``from_``. + + If ``stamp`` is not provided, the current time is assumed. + """ + + if not stamp: + stamp = utc_time() + elif not stamp.tzinfo: + stamp = localize(stamp, from_=from_) + return localize(stamp, naive=naive) + + +def localize(stamp, from_='local', to='local', naive=False): + """ + Creates a "localized" version of ``stamp`` and returns it. + + ``stamp`` must be a :class:`datetime.datetime` instance. If it is naive, + its value is interpreted as being local to the timezone referenced by + ``from_``. If it is already aware (i.e. not naive), then ``from_`` is + ignored. + + The timezone referenced by ``to`` is used to determine the final, "local" + value for the timestamp. If ``naive`` is ``True``, the timestamp is + stripped of its ``tzinfo`` member before being returned. Otherwise, it + will remain aware of its timezone. + """ + + if not stamp.tzinfo: + tz = get_timezone(from_) + stamp = tz.localize(stamp) + tz = get_timezone(to) + stamp = stamp.astimezone(tz) + if naive: + stamp = stamp.replace(tzinfo=None) + return stamp + + +def utc_time(stamp=None, from_='local', naive=False): + """ + Returns a :class:`datetime.datetime` instance, with its ``tzinfo`` member + set to the UTC timezone. If ``naive`` is ``True``, the result is stripped + of its ``tzinfo`` member. + + If ``stamp`` is provided, and it is not already "aware," then its value is + interpreted as being local to the timezone referenced by ``from_``. + + If ``stamp`` is not provided, the current time is assumed. + """ + + if not stamp: + stamp = datetime.datetime.utcnow() + stamp = pytz.utc.localize(stamp) + elif not stamp.tzinfo: + stamp = localize(stamp, from_=from_) + return localize(stamp, to='utc', naive=naive)