#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#
'''Extends the standard `datetime` module.
- Python's ``datetime.strftime`` doesn't handle dates previous to 1900.
This module define classes to override `date` and `datetime` to support the
formatting of a date through its full proleptic Gregorian date range.
Based on code submitted to comp.lang.python by Andrew Dalke, copied from
Django and generalized.
You may use this module as a drop-in replacement of the standard library
`datetime` module.
'''
from __future__ import (division as _py3_division,
print_function as _py3_print,
absolute_import as _py3_abs_imports)
import sys
from datetime import * # noqa
from datetime import timedelta
import datetime as _stdlib # noqa
from xoutil.deprecation import deprecate_linked
deprecate_linked()
del deprecate_linked
from re import compile as _regex_compile
from time import strftime as _time_strftime
#: Simple constants for .weekday() method
class WEEKDAY:
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
class ISOWEEKDAY:
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
SUNDAY = 7
try:
date(1800, 1, 1).strftime("%Y") # noqa
except ValueError:
# This happens in Pytnon 2.7, I was considering to replace `strftime`
# function from `time` module, that is used for all `strftime` methods;
# but (WTF), Python double checks the year (in each method and then again
# in `time.strftime` function).
class date(_stdlib.date):
__doc__ = _stdlib.date.__doc__
def strftime(self, fmt):
return strftime(self, fmt)
def __sub__(self, other):
return assure(super(date, self).__sub__(other))
class datetime(_stdlib.datetime):
__doc__ = _stdlib.datetime.__doc__
def strftime(self, fmt):
return strftime(self, fmt)
def __sub__(self, other):
return assure(super(datetime, self).__sub__(other))
def combine(self, date, time):
return assure(super(datetime, self).combine(date, time))
def date(self):
return assure(super(datetime, self).date())
@staticmethod
def now(tz=None):
return assure(super(datetime, datetime).now(tz=tz))
def assure(obj):
'''Make sure that a `date` or `datetime` instance is a safe version.
With safe it's meant that will use the adapted subclass on this module
or the standard if these weren't generated.
Classes that could be assured are: `date`, `datetime`, `time` and
`timedelta`.
'''
t = type(obj)
name = t.__name__
if name == date.__name__:
return obj if t is date else date(*obj.timetuple()[:3])
elif name == datetime.__name__:
if t is datetime:
return obj
else:
args = obj.timetuple()[:6] + (obj.microsecond, obj.tzinfo)
return datetime(*args)
elif isinstance(obj, (_stdlib.time, timedelta)):
return obj
else:
raise TypeError('Not valid type for datetime assuring: %s' % name)
else:
[docs] def assure(obj):
'''Make sure that a `date` or `datetime` instance is a safe version.
This is only a type checker alternative to standard library.
'''
if isinstance(obj, (date, datetime, _stdlib.time, timedelta)):
return obj
else:
raise TypeError('Not valid type for datetime assuring: %s' % obj)
from xoutil.deprecation import deprecated # noqa
@deprecated(assure)
def new_date(d):
'''Generate a safe date from a legacy datetime date object.'''
return date(d.year, d.month, d.day)
@deprecated(assure)
def new_datetime(d):
'''Generate a safe datetime given a legacy date or datetime object.'''
args = [d.year, d.month, d.day]
if isinstance(d, datetime.__base__): # legacy datetime
args.extend([d.hour, d.minute, d.second, d.microsecond, d.tzinfo])
return datetime(*args)
del deprecated
# This library does not support strftime's "%s" or "%y" format strings.
# Allowed if there's an even number of "%"s because they are escaped.
_illegal_formatting = _regex_compile(br"((^|[^%])(%%)*%[sy])")
def _year_find_all(fmt, year, no_year_tuple):
text = _time_strftime(fmt, (year,) + no_year_tuple)
regex = _regex_compile(str(year))
return {match.start() for match in regex.finditer(text)}
_TD_LABELS = 'dhms' # days, hours, minutes, seconds
def _strfnumber(number, format_spec='%0.2f'):
'''Convert a floating point number into string using a smart way.
Used internally in strfdelta.
'''
res = format_spec % number
if '.' in res:
res = res.rstrip('0')
if res.endswith('.'):
res = res[:-1]
return res
[docs]def strfdelta(delta):
'''
Format a timedelta using a smart pretty algorithm.
Only two levels of values will be printed.
::
>>> def t(h, m):
... return timedelta(hours=h, minutes=m)
>>> strfdelta(t(4, 56)) == '4h 56m'
True
'''
ss, sss = str('%s%s'), str(' %s%s')
if delta.days:
days = delta.days
delta -= timedelta(days=days)
hours = delta.total_seconds() / 60 / 60
res = ss % (days, _TD_LABELS[0])
if hours >= 0.01:
res += sss % (_strfnumber(hours), _TD_LABELS[1])
else:
seconds = delta.total_seconds()
if seconds > 60:
minutes = seconds / 60
if minutes > 60:
hours = int(minutes / 60)
minutes -= hours * 60
res = ss % (hours, _TD_LABELS[1])
if minutes >= 0.01:
res += sss % (_strfnumber(minutes), _TD_LABELS[2])
else:
minutes = int(minutes)
seconds -= 60 * minutes
res = ss % (minutes, _TD_LABELS[2])
if seconds >= 0.01:
res += sss % (_strfnumber(seconds), _TD_LABELS[3])
else:
res = ss % (_strfnumber(seconds, '%0.3f'), _TD_LABELS[3])
return res
[docs]def strftime(dt, fmt):
'''Used as `strftime` method of `date` and `datetime` redefined classes.
Also could be used with standard instances.
'''
if dt.year >= 1900:
bases = type(dt).mro()
i = 0
base = _strftime = type(dt).strftime
while _strftime == base:
aux = getattr(bases[i], 'strftime', base)
if aux != base:
_strftime = aux
else:
i += 1
return _strftime(dt, fmt)
else:
illegal_formatting = _illegal_formatting.search(fmt)
if illegal_formatting is None:
year = dt.year
# For every non-leap year century, advance by 6 years to get into
# the 28-year repeat cycle
delta = 2000 - year
year += 6 * (delta // 100 + delta // 400)
year += ((2000 - year) // 28) * 28 # Move to around the year 2000
no_year_tuple = dt.timetuple()[1:]
sites = _year_find_all(fmt, year, no_year_tuple)
sites &= _year_find_all(fmt, year + 28, no_year_tuple)
res = _time_strftime(fmt, (year,) + no_year_tuple)
syear = "%04d" % dt.year
for site in sites:
res = res[:site] + syear + res[site + 4:]
return res
else:
msg = 'strftime of dates before 1900 does not handle %s'
raise TypeError(msg % illegal_formatting.group(0))
def parse_date(value=None):
if value:
y, m, d = value.split('-')
return date(int(y), int(m), int(d))
else:
return date.today()
def parse_datetime(value=None):
'''Parse a datime in format 'YYYY-MM-DD HH:MM[:SS][.MS]'.
The hour-minute component is mandatory.
'''
if value:
d, t = value.split()
y, m, d = d.split('-')
if '.' in t:
moment, ms = t.split('.')
else:
moment, ms = t, '0'
timing = moment.split(':')
if len(timing) == 2:
h, mn = timing
s = 0
elif len(timing) == 3:
h, mn, s = timing
else:
raise ValueError('Invalid time string %r' % t)
return datetime(
int(y), int(m), int(d),
int(h), int(mn), int(s),
int(ms)
)
else:
return datetime.now()
[docs]def get_month_first(ref=None):
'''Given a reference date, returns the first date of the same month. If
`ref` is not given, then uses current date as the reference.
'''
aux = ref or date.today()
y, m = aux.year, aux.month
return date(y, m, 1)
[docs]def get_month_last(ref=None):
'''Given a reference date, returns the last date of the same month. If
`ref` is not given, then uses current date as the reference.
'''
aux = ref or date.today()
y, m = aux.year, aux.month
if m == 12:
m = 1
y += 1
else:
m += 1
return date(y, m, 1) - timedelta(1)
[docs]def get_next_month(ref=None, lastday=False):
'''Get the first or last day of the *next month*.
If `lastday` is False return the first date of the `next month`.
Otherwise, return the last date.
The *next month* is computed with regards to a reference date. If `ref`
is None, take the current date as the reference.
Examples:
>>> get_next_month(date(2017, 1, 23))
date(2017, 2, 1)
>>> get_next_month(date(2017, 1, 23), lastday=True)
date(2017, 2, 28)
.. versionadded:: 1.7.3
'''
result = get_month_last(ref) + timedelta(days=1)
if lastday:
return get_month_last(result)
else:
return result
[docs]def is_full_month(start, end):
'''Returns true if the arguments comprises a whole month.
'''
sd, sm, sy = start.day, start.month, start.year
em, ey = end.month, end.year
return ((sd == 1) and (sm == em) and (sy == ey) and
(em != (end + timedelta(1)).month))
[docs]class flextime(timedelta):
@classmethod
def parse_simple_timeformat(cls, which):
if 'h' in which:
hour, rest = which.split('h')
else:
hour, rest = 0, which
return int(hour), int(rest), 0
def __new__(cls, *args, **kwargs):
first = None
if args:
first, rest = args[0], args[1:]
_super = super(flextime, cls).__new__
if first and not rest and not kwargs:
hour, minutes, seconds = cls.parse_simple_timeformat(first)
return _super(cls, hours=hour, minutes=minutes, seconds=seconds)
else:
return _super(cls, *args, **kwargs)
# TODO: Merge this with the new time span.
[docs]def daterange(*args):
'''Similar to standard 'range' function, but for date objets.
Returns an iterator that yields each date in the range of ``[start,
stop)``, not including the stop.
If `start` is given, it must be a date (or `datetime`) value; and in this
case only `stop` may be an integer meaning the numbers of days to look
ahead (or back if `stop` is negative).
If only `stop` is given, `start` will be the first day of stop's month.
`step`, if given, should be a non-zero integer meaning the numbers of days
to jump from one date to the next. It defaults to ``1``. If it's positive
then `stop` should happen after `start`, otherwise no dates will be
yielded. If it's negative `stop` should be before `start`.
As with `range`, `stop` is never included in the yielded dates.
'''
import operator
# Use base classes to allow broader argument values
from datetime import date, datetime
if len(args) == 1:
start, stop, step = None, args[0], None
elif len(args) == 2:
start, stop = args
step = None
else:
start, stop, step = args
if not step and step is not None:
raise ValueError('Invalid step value %r' % step)
if not start:
if not isinstance(stop, (date, datetime)):
raise TypeError('stop must a date if start is None')
else:
start = get_month_first(stop)
else:
if stop is not None and not isinstance(stop, (date, datetime)):
stop = start + timedelta(days=stop)
if step is None or step > 0:
compare = operator.lt
else:
compare = operator.gt
step = timedelta(days=(step if step else 1))
# Encloses the generator so that signature validation exceptions happen
# without needing to call next().
def _generator():
current = start
while stop is None or compare(current, stop):
yield current
current += step
return _generator()
if sys.version_info < (3, 0):
class infinity_extended_date(date):
'A date that compares to Infinity'
def operator(name, T=True):
def result(self, other):
from xoutil.infinity import Infinity
if other is Infinity:
return T
elif other is -Infinity:
return not T
else:
return getattr(date, name)(self, other)
return result
# It seems that @total_ordering is worthless because date implements
# this operators
__le__ = operator('__le__')
__ge__ = operator('__ge__', T=False)
__lt__ = operator('__lt__')
__gt__ = operator('__gt__', T=False)
del operator
def __eq__(self, other):
from xoutil.infinity import Infinity
# I have to put this because when doing ``timespan != date``
# Python 2 may chose to call date's __ne__ instead of
# TimeSpan.__ne__. I assume the same applies to __eq__.
if isinstance(other, _EmptyTimeSpan):
return False
if isinstance(other, TimeSpan):
return TimeSpan.from_date(self) == other
elif other is Infinity or other is -Infinity:
return False
else:
return date.__eq__(self, other)
if sys.version_info < (3, 0):
def __ne__(self, other):
res = self == other
if res is not NotImplemented:
return not res
else:
return res
class infinity_extended_datetime(datetime):
'A datetime that compares to Infinity'
def operator(name, T=True):
def result(self, other):
from xoutil.infinity import Infinity
if other is Infinity:
return T
elif other is -Infinity:
return not T
else:
return getattr(datetime, name)(self, other)
return result
# It seems that @total_ordering is worthless because date implements
# this operators
__le__ = operator('__le__')
__ge__ = operator('__ge__', T=False)
__lt__ = operator('__lt__')
__gt__ = operator('__gt__', T=False)
del operator
def __eq__(self, other):
from xoutil.infinity import Infinity
# I have to put this because when doing ``timespan != date``
# Python 2 may chose to call date's __ne__ instead of
# TimeSpan.__ne__. I assume the same applies to __eq__.
if isinstance(other, _EmptyTimeSpan):
return False
if isinstance(other, DateTimeSpan):
return DateTimeSpan.from_datetime(self) == other
elif other is Infinity or other is -Infinity:
return False
else:
return datetime.__eq__(self, other)
if sys.version_info < (3, 0):
def __ne__(self, other):
res = self == other
if res is not NotImplemented:
return not res
else:
return res
else:
infinity_extended_date = date
infinity_extended_datetime = datetime
[docs]class DateField(object):
'''A simple descriptor for dates.
Ensures that assigned values must be parseable dates and parses them.
'''
def __init__(self, name, nullable=False):
self.name = name
self.nullable = nullable
def __get__(self, instance, owner):
from xoutil.context import context
if instance is not None:
res = instance.__dict__[self.name]
if res and NEEDS_FLEX_DATE in context:
return infinity_extended_date(res.year, res.month, res.day)
else:
return res
else:
return self
def __set__(self, instance, value):
from datetime import datetime as dt, date
if value in (None, False):
# We regard False as None, so that working with Odoo is easier:
# missing values in Odoo, often come as False instead of None.
if not self.nullable:
raise ValueError('Setting None to a required field')
else:
value = None
elif isinstance(value, dt):
value = value.date()
elif not isinstance(value, date):
value = parse_date(value)
instance.__dict__[self.name] = value
class DateTimeField(object):
'''A simple descriptor for datetimes.
Ensures that assigned values must be parseable date or datetime and parses
them.
If `prefer_last_minute` is False when converting from date, the time
component will be '00:00:00', if True, the time component will be
'23:59:59'.
.. versionadded:: 1.9.7
'''
def __init__(self, name, nullable=False, prefer_last_minute=False):
self.name = name
self.nullable = nullable
self.prefer_last_minute = prefer_last_minute
def __get__(self, instance, owner):
from xoutil.context import context
if instance is not None:
res = instance.__dict__[self.name]
if res and NEEDS_FLEX_DATE in context:
return infinity_extended_datetime(
res.year, res.month, res.day,
res.hour, res.minute, res.second,
res.microsecond,
res.tzinfo
)
else:
return res
else:
return self
def __set__(self, instance, value):
import datetime as stdlib
if value in (None, False):
# We regard False as None, so that working with Odoo is easier:
# missing values in Odoo, often come as False instead of None.
if not self.nullable:
raise ValueError('Setting None to a required field')
else:
value = None
elif isinstance(value, stdlib.datetime):
# needed because datetime is subclass of date, and the next
# condition would match.
pass
elif isinstance(value, stdlib.date):
if not self.prefer_last_minute:
value = stdlib.datetime(value.year, value.month, value.day)
else:
value = stdlib.datetime(value.year, value.month, value.day,
23, 59, 59)
else:
try:
value = parse_datetime(value)
except ValueError:
value = parse_date(value)
self.__set__(instance, value) # lazy me
return
instance.__dict__[self.name] = value
[docs]class TimeSpan(object):
'''A *continuous* span of time.
Time spans objects are iterable. They yield exactly two times: first the
start date, and then the end date::
>>> ts = TimeSpan('2017-08-01', '2017-09-01')
>>> tuple(ts)
(date(2017, 8, 1), date(2017, 9, 1))
Time spans objects have two items::
>>> ts[0]
date(2017, 8, 1)
>>> ts[1]
date(2017, 9, 1)
>>> ts[:]
(date(2017, 8, 1), date(2017, 9, 1))
Two time spans are equal if their start_date and end_date are equal. When
comparing a time span with a date, the date is coerced to a time span
(`from_date`:meth:).
.. note:: Comparing time spans with date time spans `coerces the time span
<DateTimeSpan.from_timespan>`:meth: before comparing.
A time span with its `start` set to None is unbound to the past. A time
span with its `end` set to None is unbound to the future. A time span
that is both unbound to the past and the future contains all possible
dates. A time span that is not unbound in any direction is
`bound <bound>`:attr:.
A bound time span is `valid`:attr: if its start date comes before its end
date.
Time spans can `intersect <__mul__>`:meth:, compared for containment of
dates and by the subset/superset order operations (``<=``, ``>=``). In
this regard, they represent the *set* of dates between `start` and `end`,
inclusively.
.. warning:: Time spans don't implement the union or difference operations
expected in sets because the difference/union of two span is not
necessarily *continuous*.
'''
start_date = DateField('start_date', nullable=True)
end_date = DateField('end_date', nullable=True)
def __init__(self, start_date=None, end_date=None):
self.start_date = start_date
self.end_date = end_date
[docs] @classmethod
def from_date(self, date):
# type: (date) -> TimeSpan
'''Return a new time span that covers a single `date`.'''
return self(start_date=date, end_date=date)
@property
def past_unbound(self):
# type: () -> bool
'True if the time span is not bound into the past.'
return self.start_date is None
@property
def future_unbound(self):
# type: () -> bool
'True if the time span is not bound into the future.'
return self.end_date is None
@property
def unbound(self):
# type: () -> bool
'''True if the time span is `unbound into the past <past_unbound>`:attr: or
`unbount into the future <future_unbound>`:attr: or both.
'''
return self.future_unbound or self.past_unbound
@property
def bound(self):
# type: () -> bool
'True if the time span is not `unbound <unbound>`:attr:.'
return not self.unbound
@property
def valid(self):
# type: () -> bool
'''A bound time span is valid if it starts before it ends.
Unbound time spans are always valid.
'''
from xoutil.context import context
with context(NEEDS_FLEX_DATE):
if self.bound:
return self.start_date <= self.end_date
else:
return True
[docs] def __contains__(self, other):
'''Test date `other` is in the time span.
'''
# type: (date) -> bool
from datetime import date
if isinstance(other, date):
if self.start_date and self.end_date:
return self.start_date <= other <= self.end_date
elif self.start_date:
return self.start_date <= other
elif self.end_date:
return other <= self.end_date
else:
return True
else:
return False
[docs] def overlaps(self, other):
# type: (TimeSpan) -> bool
'''Test if the time spans overlaps.'''
return bool(self & other)
[docs] def isdisjoint(self, other):
# type: (TimeSpan) -> bool
return not self.overlaps(other)
[docs] def __le__(self, other):
# type: (TimeSpan) -> bool
'True if `other` is a superset.'
return (self & other) == self
issubset = __le__
def __lt__(self, other):
# type: (TimeSpan) -> bool
'True if `other` is a proper superset.'
return self != other and self <= other
def __gt__(self, other):
# type: (TimeSpan) -> bool
'True if `other` is a proper subset.'
return self != other and self >= other
[docs] def __ge__(self, other):
# type: (TimeSpan) -> bool
'True if `other` is a subset.'
# Notice that ge is not the opposite of lt.
return (self & other) == other
issuperset = covers = __ge__
def __iter__(self):
# type: () -> Iterator[date, date]
yield self.start_date
yield self.end_date
def __getitem__(self, index):
# type: (int) -> date
this = tuple(self)
return this[index]
def __eq__(self, other):
# type: (TimeSpan) -> bool
import datetime
if isinstance(other, datetime.date):
other = type(self).from_date(other)
elif isinstance(other, DateTimeSpan):
return other == self
if not isinstance(other, TimeSpan):
return NotImplemented
return (self.start_date == other.start_date and
self.end_date == other.end_date)
def __hash__(self):
return hash((TimeSpan, self.start_date, self.end_date))
if sys.version_info < (3, 0):
def __ne__(self, other):
# type: (TimeSpan) -> bool
res = self == other
if res is not NotImplemented:
return not res
else:
return res
[docs] def __and__(self, other):
# type: (TimeSpan) -> TimeSpan
'''Get the time span that is the intersection with another time span.
If two time spans don't overlap, return `EmptyTimeSpan`:data:.
If `other` is not a TimeSpan we try to create one. If `other` is a
date, we create the TimeSpan that starts and end that very day. Other
types are passed unchanged to the constructor.
When `other` is a `DateTimeSpan`:class:, convert `self` to a `date
time span <DateTimeSpan.from_timespan>`:meth: before doing the
intersection.
'''
import datetime
from xoutil.infinity import Infinity
from xoutil.context import context
if isinstance(other, _EmptyTimeSpan):
return other
elif isinstance(other, datetime.date):
other = TimeSpan.from_date(other)
elif isinstance(other, DateTimeSpan):
return other & self
elif not isinstance(other, TimeSpan):
raise TypeError("Invalid type '%s'" % type(other).__name__)
with context(NEEDS_FLEX_DATE):
start = max(
self.start_date or -Infinity,
other.start_date or -Infinity
)
end = min(
self.end_date or Infinity,
other.end_date or Infinity
)
if start <= end:
if start is -Infinity:
start = None
if end is Infinity:
end = None
return type(self)(start, end)
else:
return EmptyTimeSpan
__mul__ = __rmul__ = __rand__ = __and__
def __bool__(self):
return True
__nonzero__ = __bool__
[docs] def __len__(self):
'''The amount of dates in the span.
.. warning:: If the time span is `unbound`:attr: this method returns
NotImplemented. This will make python complain with a
TypeError.
.. versionadded:: 1.8.2
'''
if self.bound:
return (self.end_date - self.start_date).days
else:
return NotImplemented
[docs] def __lshift__(self, delta):
'''Return the time span displaced to the past in `delta`.
:param delta: The number of days to displace. It can be either an
integer or a `datetime.timedelta`:class:. The integer
will be converted to ``timedelta(days=delta)``.
.. note:: Delta values that don't amount to at least a day will be the
same as 0.
.. versionadded:: 1.8.2
.. warning:: Python does have a boundaries for the dates it can
represent, so displacing a TimeSpan can cause OverflowError.
'''
import numbers
if isinstance(delta, numbers.Integral):
delta = timedelta(days=delta) # noqa
start = self.start_date - delta if self.start_date else None
end = self.end_date - delta if self.end_date else None
return type(self)(start, end)
[docs] def __rshift__(self, delta):
'''Return the time span displaced to the future in `delta`.
:param delta: The number of days to displace. It can be either an
integer or a `datetime.timedelta`:class:. The integer
will be converted to ``timedelta(days=delta)``.
.. note:: Delta values that don't amount to at least a day will be the
same as 0.
.. versionadded:: 1.8.2
.. warning:: Python does have a boundaries for the dates it can
represent, so displacing a TimeSpan can cause OverflowError.
'''
return self << -delta
[docs] def intersection(self, *others):
'Return ``self [& other1 & ...]``.'
import operator
from functools import reduce
return reduce(operator.mul, others, self)
[docs] def diff(self, other):
# type: (TimeSpan) -> Tuple[TimeSpan, TimeSpan]
'''Return the two time spans which (combined) contain all the dates in
`self` which are not in `other`.
Notice this method returns a tuple of exactly two items.
If `other` and `self` don't overlap, return ``(self, EmptyTimeSpan)``.
If ``self <= other`` is True, return the tuple with the empty time
span in both positions.
Otherwise `self` will have some dates which are not in `other`; there
are possible three cases:
a) other starts before or at self's start date; return the empty time
span and the time span containing the dates after `other.end_date`
up to `self.end_date`
b) other ends at or after self's end date; return the dates from
`self.start_date` up to the date before `other.start_date` and the
empty time span.
c) `other` is fully contained in `self`; return two non-empty time
spans as in the previous cases.
.. versionadded:: 1.9.7
'''
if not self & other:
return self, EmptyTimeSpan
other = self & other
if self == other:
return EmptyTimeSpan, EmptyTimeSpan
else:
assert self > other
day = timedelta(days=1)
if self.start_date == other.start_date:
return (EmptyTimeSpan,
TimeSpan(other.end_date + day, self.end_date))
elif self.end_date == other.end_date:
return (TimeSpan(self.start_date, other.start_date - day),
EmptyTimeSpan)
else:
return (TimeSpan(self.start_date, other.start_date - day),
TimeSpan(other.end_date + day, self.end_date))
def __repr__(self):
start, end = self
return 'TimeSpan(%r, %r)' % (start.isoformat() if start else None,
end.isoformat() if end else None)
class _EmptyTimeSpan(object):
__slots__ = [] # no inner structure
def __bool__(self):
return False
__nonzero__ = __bool__
def __contains__(self, which):
return False # I don't contain noone
# The empty is equal only to itself
def __eq__(self, which):
from datetime import date
if isinstance(which, (TimeSpan, date, _EmptyTimeSpan)):
# We expect `self` to be a singleton, but pickle protocol 1 does
# not warrant to call our __new__.
return self is which
else:
return NotImplemented
def __ne__(self, other):
res = self == other
if res is not NotImplemented:
return not res
else:
return res
# The empty set is a subset of any other set. dates are regarded as the
# set that contains that
def __le__(self, which):
from datetime import date
if isinstance(which, (TimeSpan, date, _EmptyTimeSpan)):
return True
else:
return NotImplemented
# The empty set is only a superset of itself.
__ge__ = covers = __eq__
# The empty set is a *proper* subset of any set but itself. The empty
# set is disjoint with any other set but itself.
__lt__ = isdisjoint = __ne__
# The empty set is a *proper* superset of no one
def __gt__(self, which):
from datetime import date
if isinstance(which, (TimeSpan, date, _EmptyTimeSpan)):
return True
else:
return NotImplemented
# `empty | x == empty + x == x`
def __add__(self, which):
from datetime import date
if isinstance(which, (TimeSpan, date, _EmptyTimeSpan)):
return which
else:
raise TypeError
__or__ = __add__
# `empty & x == empty * x == empty`
def __mul__(self, other):
from datetime import date
if isinstance(other, (TimeSpan, date, _EmptyTimeSpan)):
return self
else:
raise TypeError
__and__ = __mul__
def __repr__(self):
return 'EmptyTimeSpan'
def __new__(cls):
res = getattr(cls, '_instance', None)
if res is None:
res = cls._instance = super(_EmptyTimeSpan, cls).__new__(cls)
return res
def __reduce__(self):
# So that unpickling returns the singleton
return type(self), ()
def __len__(self):
return 0
def __lshift__(self, delta):
return self
def __rshift__(self, delta):
return self
EmptyTimeSpan = _EmptyTimeSpan()
# TODO: Move this to xoutil.objects or somewhere else
class SynchronizedField(object):
'''A synchronized descriptor.
Whenever the `source` gets updated, update the second.
'''
def __init__(self, descriptor, setting_descriptor, set_throu_get=True):
self.descriptor = descriptor
self.setting_descriptor = setting_descriptor
self.set_throu_get = set_throu_get
def __get__(self, instance, owner):
return self.descriptor.__get__(instance, owner)
def __set__(self, instance, value):
from xoutil.context import context
self.descriptor.__set__(instance, value)
if (SynchronizedField, self.setting_descriptor) not in context:
with context((SynchronizedField, self.setting_descriptor)):
if self.set_throu_get:
value = self.__get__(instance, type(instance))
self.setting_descriptor.__set__(instance, value)
[docs]class DateTimeSpan(TimeSpan):
'''A *continuous* span of time (with datetime at each boundary).
`DateTimeSpan`:class: is a minor extension of `TimeSpan`:class:, and is a
subclass.
DateTimeSpan objects are iterable. They yield exactly two datetimes:
first the start date, and then the end date::
>>> ts = DateTimeSpan('2017-08-01 11:00', '2017-09-01 23:00')
>>> tuple(ts)
(datetime(2017, 8, 1, 11, 0), date(2017, 9, 1, 23, 0))
The API of DateTimeSpan is just the natural transformation of the API of
`TimeSpan`:class:.
The `start_date` and `end_date` attributes are interlocked with the
`start_datetime` and `end_datetime`. By changing `start_date`, you also
change `start_datetime` with the same date at 00:00 without tzinfo. By
setting `start_datetime` you also update `start_date`. By setting
`end_date` you also update `end_datetime` with the same date at 23:59:59
without tzinfo.
.. versionadded:: 1.9.7
.. warning:: DateTimeSpan is provided on a provisional basis. Future
releases can change its API or remove it completely.
'''
start_datetime = SynchronizedField(
DateTimeField('start_datetime', nullable=True),
TimeSpan.start_date,
)
end_datetime = SynchronizedField(
DateTimeField('end_datetime', nullable=True, prefer_last_minute=True),
TimeSpan.end_date,
)
start_date = SynchronizedField(
TimeSpan.start_date,
start_datetime.descriptor,
)
end_date = SynchronizedField(
TimeSpan.end_date,
end_datetime.descriptor,
)
def __init__(self, start_datetime=None, end_datetime=None):
# Don't call super because our fields are synchronized.
self.start_datetime = start_datetime
self.end_datetime = end_datetime
[docs] @classmethod
def from_datetime(self, dt):
# type: (datetime) -> DateTimeSpan
'''Return a new date time span that covers a single `datetime`.
If `dt` is actually a date, the start_datetime will be at '00:00:00'
and the end_datetime will be at '23:59:59'.
'''
return self(start_datetime=dt, end_datetime=dt)
[docs] @classmethod
def from_timespan(self, ts):
# type: (TimeSpan) -> DateTimeSpan
'''Return a new date time span from a timespan.
Notice the start datetime will be set at '00:00:00' and the end
datetime at '23:59:59'.
If `ts` is already a DateTimeSpan, return it unchanged.
'''
if isinstance(ts, DateTimeSpan):
return ts
else:
return self(start_datetime=ts.start_date,
end_datetime=ts.end_date)
@property
def past_unbound(self):
# type: () -> bool
'True if the time span is not bound into the past.'
return self.start_datetime is None
@property
def future_unbound(self):
# type: () -> bool
'True if the time span is not bound into the future.'
return self.end_datetime is None
@property
def unbound(self):
# type: () -> bool
'''True if the time span is `unbound into the past <past_unbound>`:attr: or
`unbount into the future <future_unbound>`:attr: or both.
'''
return self.future_unbound or self.past_unbound
@property
def bound(self):
# type: () -> bool
'True if the time span is not `unbound <unbound>`:attr:.'
return not self.unbound
@property
def valid(self):
# type: () -> bool
'''A bound time span is valid if it starts before it ends.
Unbound time spans are always valid.
'''
from xoutil.context import context
with context(NEEDS_FLEX_DATE):
if self.bound:
return self.start_datetime <= self.end_datetime
else:
return True
[docs] def __contains__(self, other):
# type: (date) -> bool
'''Test if datetime `other` is in the datetime span.
If `other` is a `~datetime.date`:class:, we convert it to a naive
datetime at midnight (00:00:00).
'''
from datetime import date, datetime
if isinstance(other, date):
if not isinstance(other, datetime):
other = datetime(other.year, other.month, other.day)
if self.start_datetime and self.end_datetime:
return self.start_datetime <= other <= self.end_datetime
elif self.start_datetime:
return self.start_datetime <= other
elif self.end_datetime:
return other <= self.end_datetime
else:
return True
else:
return False
[docs] def overlaps(self, other):
# type: (TimeSpan) -> DateTimeSpan
'''Test if the time spans overlaps.'''
return bool(self & other)
[docs] def isdisjoint(self, other):
# type: (TimeSpan) -> bool
return not self.overlaps(other)
[docs] def __le__(self, other):
# type: (TimeSpan) -> bool
'True if `other` is a superset.'
return (self & other) == self
issubset = __le__
def __lt__(self, other):
# type: (TimeSpan) -> bool
'True if `other` is a proper superset.'
return self != other and self <= other
def __gt__(self, other):
# type: (TimeSpan) -> bool
'True if `other` is a proper subset.'
return self != other and self >= other
[docs] def __ge__(self, other):
# type: (TimeSpan) -> bool
'True if `other` is a subset.'
# Notice that ge is not the opposite of lt.
return (self & other) == other
issuperset = covers = __ge__
def __iter__(self):
# type: () -> Iterator[datetime]
yield self.start_datetime
yield self.end_datetime
def __getitem__(self, index):
# type: (int) -> datetime
this = tuple(self)
return this[index]
def __eq__(self, other):
# type: (TimeSpan) -> bool
import datetime
if isinstance(other, datetime.date):
other = type(self).from_datetime(other)
elif isinstance(other, TimeSpan) and \
not isinstance(other, DateTimeSpan): # noqa
other = self.from_timespan(other)
elif not isinstance(other, DateTimeSpan):
return NotImplemented
return (self.start_datetime == other.start_datetime and
self.end_datetime == other.end_datetime)
def __hash__(self):
return hash((DateTimeSpan, self.start_datetime, self.end_datetime))
if sys.version_info < (3, 0):
def __ne__(self, other):
# type: (TimeSpan) -> bool
res = self == other
if res is not NotImplemented:
return not res
else:
return res
[docs] def __and__(self, other):
# type: (TimeSpan) -> DateTimeSpan
'''Get the date time span that is the intersection with another time span.
If two time spans don't overlap, return the object
`EmptyTimeSpan`:any:.
If `other` is not a DateTimeSpan we try to create one. If `other` is
a date/datetime, we create use `from_datetime`:meth:. If `other` is
TimeSpan we use `from_timespan`:meth:. Other types are passed
unchanged to the constructor.
'''
import datetime
from xoutil.infinity import Infinity
from xoutil.context import context
if isinstance(other, _EmptyTimeSpan):
return other
elif isinstance(other, datetime.date):
other = DateTimeSpan.from_datetime(other)
elif isinstance(other, TimeSpan):
other = DateTimeSpan.from_timespan(other)
elif not isinstance(other, TimeSpan):
raise TypeError("Invalid type '%s'" % type(other).__name__)
with context(NEEDS_FLEX_DATE):
start = max(
self.start_datetime or -Infinity,
other.start_datetime or -Infinity
)
end = min(
self.end_datetime or Infinity,
other.end_datetime or Infinity
)
if start <= end:
if start is -Infinity:
start = None
if end is Infinity:
end = None
return type(self)(start, end)
else:
return EmptyTimeSpan
__mul__ = __rmul__ = __rand__ = __and__
def __bool__(self):
# type: () -> bool
return True
__nonzero__ = __bool__
[docs] def __lshift__(self, delta):
# type: (Union[int, timedelta]) -> DateTimeSpan
'''Return the date time span displaced to the past in `delta`.
:param delta: The number of days to displace. It can be either an
integer or a `datetime.timedelta`:class:. The integer
will be converted to ``timedelta(days=delta)``.
.. warning:: Python does have a boundaries for the dates it can
represent, so displacing can cause OverflowError.
'''
import numbers
if isinstance(delta, numbers.Integral):
delta = timedelta(days=delta)
start = self.start_datetime - delta if self.start_datetime else None
end = self.end_datetime - delta if self.end_datetime else None
return type(self)(start, end)
[docs] def __rshift__(self, delta):
# type: (Union[int, timedelta]) -> DateTimeSpan
'''Return the date time span displaced to the future in `delta`.
:param delta: The number of days to displace. It can be either an
integer or a `datetime.timedelta`:class:. The integer
will be converted to ``timedelta(days=delta)``.
.. warning:: Python does have a boundaries for the dates it can
represent, so displacing can cause OverflowError.
'''
return self << -delta
[docs] def intersection(self, *others):
# type: (TimeSpan) -> DateTimeSpan
'Return ``self [& other1 & ...]``.'
import operator
from functools import reduce
return reduce(operator.mul, others, self)
[docs] def diff(self, other):
# type: (TimeSpan) -> Tuple[DateTimeSpan, DateTimeSpan]
'''Return the two datetime spans which (combined) contain all the
seconds in `self` which are not in `other`.
Notice this method returns a tuple of exactly two items.
If `other` and `self` don't overlap, return ``(self, EmptyTimeSpan)``.
If ``self <= other`` is True, return the tuple with the empty time
span in both positions.
Otherwise `self` will have some datetimes which are not in `other`;
there are possible three cases:
a) other starts before or at self's start datetime; return the empty
time span and the datetime span from the second after
`other.end_datetime` up to `self.end_datetime`
b) other ends at or after self's end date; return the datetime span
from `self.start_datetime` up to the second before
`other.start_datetime` and the empty time span.
c) `other` is fully contained in `self`; return two non-empty datetime
spans as in the previous cases.
'''
if not self & other:
return self, EmptyTimeSpan
other = self & other
if self == other:
return EmptyTimeSpan, EmptyTimeSpan
else:
assert self > other
sec = timedelta(seconds=1)
if self.start_datetime == other.start_datetime:
return (EmptyTimeSpan,
DateTimeSpan(other.end_datetime + sec,
self.end_datetime))
elif self.end_datetime == other.end_datetime:
return (DateTimeSpan(self.start_datetime,
other.start_datetime - sec),
EmptyTimeSpan)
else:
return (DateTimeSpan(self.start_datetime,
other.start_datetime - sec),
DateTimeSpan(other.end_datetime + sec,
self.end_datetime))
def __repr__(self):
start, end = self
return 'DateTimeSpan(%r, %r)' % (
start.isoformat().replace('T', ' ') if start else None,
end.isoformat().replace('T', ' ') if end else None
)
# A context to switch on/off returning a subtype of date from DateFields.
# Used within TimeSpan to allow comparison with Infinity.
NEEDS_FLEX_DATE = object()
try:
timezone # noqa
except NameError:
# Copied from Python 3.5.2
# TODO: Document this in xoutil
class timezone(_stdlib.tzinfo):
'''Fixed offset from UTC implementation of tzinfo.'''
__slots__ = '_offset', '_name'
# Sentinel value to disallow None
_Omitted = object()
def __new__(cls, offset, name=_Omitted):
if not isinstance(offset, timedelta):
raise TypeError("offset must be a timedelta")
if name is cls._Omitted:
if not offset:
return cls.utc
name = None
elif not isinstance(name, str):
raise TypeError("name must be a string")
if not cls._minoffset <= offset <= cls._maxoffset:
raise ValueError("offset must be a timedelta "
"strictly between -timedelta(hours=24) and "
"timedelta(hours=24).")
if (offset.microseconds != 0 or offset.seconds % 60 != 0):
raise ValueError("offset must be a timedelta "
"representing a whole number of minutes")
return cls._create(offset, name)
@classmethod
def _create(cls, offset, name=None):
self = _stdlib.tzinfo.__new__(cls)
self._offset = offset
self._name = name
return self
def __getinitargs__(self):
"""pickle support"""
if self._name is None:
return (self._offset,)
return (self._offset, self._name)
def __eq__(self, other):
if type(other) != timezone:
return False
return self._offset == other._offset
def __hash__(self):
return hash(self._offset)
def __repr__(self):
"""Convert to formal string, for repr().
>>> tz = timezone.utc
>>> repr(tz)
'datetime.timezone.utc'
>>> tz = timezone(timedelta(hours=-5), 'EST')
>>> repr(tz)
"datetime.timezone(datetime.timedelta(-1, 68400), 'EST')"
"""
if self is self.utc:
return 'datetime.timezone.utc'
try:
qn = self.__class__.__qualname__ # not valid in Python 2
except AttributeError:
qn = self.__class__.__name__
if self._name is None:
return "%s.%s(%r)" % (self.__class__.__module__, qn,
self._offset)
else:
return "%s.%s(%r, %r)" % (self.__class__.__module__, qn,
self._offset, self._name)
def __str__(self):
return self.tzname(None)
def utcoffset(self, dt):
if isinstance(dt, datetime) or dt is None:
return self._offset
raise TypeError("utcoffset() argument must be a datetime instance"
" or None")
def tzname(self, dt):
if isinstance(dt, datetime) or dt is None:
if self._name is None:
return self._name_from_offset(self._offset)
return self._name
raise TypeError("tzname() argument must be a datetime instance"
" or None")
def dst(self, dt):
if isinstance(dt, datetime) or dt is None:
return None
raise TypeError("dst() argument must be a datetime instance"
" or None")
def fromutc(self, dt):
if isinstance(dt, datetime):
if dt.tzinfo is not self:
raise ValueError("fromutc: dt.tzinfo "
"is not self")
return dt + self._offset
raise TypeError("fromutc() argument must be a datetime instance"
" or None")
_maxoffset = timedelta(hours=23, minutes=59)
_minoffset = -_maxoffset
@staticmethod
def _name_from_offset(delta):
if delta < timedelta(0):
sign = '-'
delta = -delta
else:
sign = '+'
hours, rest = divmod(delta, timedelta(hours=1))
minutes = rest // timedelta(minutes=1)
return 'UTC{}{:02d}:{:02d}'.format(sign, hours, minutes)
timezone.utc = timezone._create(timedelta(0))
timezone.min = timezone._create(timezone._minoffset)
timezone.max = timezone._create(timezone._maxoffset)
# _EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
del sys
# TODO: this function was intended for a local 'strftime' that it's already
# implemented in 'xoutil.future.datetime'.
if 'eight' in __name__:
def _wrap_strftime(object, format, timetuple):
'''Correctly substitute for %z and %Z escapes in strftime formats.'''
# from datetime import timedelta
import time as _time
# Don't call utcoffset() or tzname() unless actually needed.
freplace = None # the string to use for %f
zreplace = None # the string to use for %z
Zreplace = None # the string to use for %Z
# Scan format for %z and %Z escapes, replacing as needed.
newformat = []
push = newformat.append
i, n = 0, len(format)
while i < n:
ch = format[i]
i += 1
if ch == '%':
if i < n:
ch = format[i]
i += 1
if ch == 'f':
if freplace is None:
freplace = '%06d' % getattr(object,
'microsecond', 0)
newformat.append(freplace)
elif ch == 'z':
if zreplace is None:
zreplace = ""
if hasattr(object, "utcoffset"):
offset = object.utcoffset()
if offset is not None:
sign = '+'
if offset.days < 0:
offset = -offset
sign = '-'
h, m = divmod(offset, timedelta(hours=1))
# not a whole minute
assert not m % timedelta(minutes=1)
m //= timedelta(minutes=1)
zreplace = '%c%02d%02d' % (sign, h, m)
assert '%' not in zreplace
newformat.append(zreplace)
elif ch == 'Z':
if Zreplace is None:
Zreplace = ""
if hasattr(object, "tzname"):
s = object.tzname()
if s is not None:
# strftime is going to have at this:
# escape %
Zreplace = s.replace('%', '%%')
newformat.append(Zreplace)
else:
push('%')
push(ch)
else:
push('%')
else:
push(ch)
newformat = "".join(newformat)
print(newformat, timetuple)
return _time.strftime(newformat, timetuple)
# def strftime(self, fmt): # Method for class date
# "Format using strftime()."
# return _wrap_strftime(self, fmt, self.timetuple())