Compare commits

...

28 Commits

Author SHA1 Message Date
kennethreitz d50dc8701c simply test suite 2017-05-27 12:07:57 -04:00
Christian Stade-Schuldt 6c1cc24ad5 minor 2017-05-26 01:14:59 -07:00
Christian Stade-Schuldt 46b7e369b3 adapt compat.py style to the one from requests 2017-05-25 12:14:58 -07:00
Christian Stade-Schuldt 2b60e817ea add compatibility for Python3 2017-05-25 12:01:01 -07:00
kennethreitz 0fea886c00 Merge pull request #57 from c17r/patch-1
Regenerate lock file for 3.6
2017-05-25 08:57:55 -07:00
Christian Sauer ef52cb4b7d Regenerate lock file for 3.6
Maya uses `ruamel.yaml` which needs `ruamel.ordereddict` under 2.7 but does not under 3.x.  `ruamel.ordereddict` doesn't build under 3.x.  Maya's Pipfile.lock file was created under 2.7 so `ruamel.ordereddict` is listed as a dependency.
2017-05-25 11:56:25 -04:00
kennethreitz f1be1585d3 Update README.rst 2017-05-25 11:21:11 -04:00
kennethreitz 89ee548ec1 Update README.rst 2017-05-25 11:21:01 -04:00
kennethreitz 09351afc68 Update README.rst 2017-05-25 10:41:23 -04:00
kennethreitz 5d4a217fd6 Update README.rst 2017-05-25 10:41:03 -04:00
kennethreitz 27950e6d35 fixes 2017-05-23 11:45:33 -07:00
David Gouldin f2c52af853 Merge branch 'fix-interval-contains' 2017-05-23 11:44:57 -07:00
David Gouldin 01cc151b5c Correcting order of 'in' operator in code and tests 2017-05-23 11:43:48 -07:00
kennethreitz eda4a92da9 Merge branch 'master' of github.com:kennethreitz/maya 2017-05-23 11:38:24 -07:00
kennethreitz 6fbf8f2d74 fix the bug 2017-05-23 11:38:09 -07:00
kennethreitz 343abed679 fix the bug 2017-05-23 11:36:39 -07:00
kennethreitz 704af5a3a0 Update README.rst 2017-05-23 11:27:47 -07:00
kennethreitz f9f5b97f62 Update README.rst 2017-05-23 11:27:03 -07:00
kennethreitz 36fc82211f remove bunk 2017-05-23 10:51:03 -07:00
kennethreitz 6eba043582 amazing MayaInterval class, compliments of @dgouldin :) 2017-05-23 10:49:36 -07:00
kennethreitz 23652fe16a new lock file 2017-05-15 20:14:13 -04:00
kennethreitz 36379fc2f2 .org 2017-05-15 20:12:43 -04:00
kennethreitz c7b804985e Update README.rst 2017-05-15 20:10:27 -04:00
kennethreitz e4a928e8f4 new version 2017-05-15 20:09:11 -04:00
kennethreitz 080831519d range 2017-05-15 20:06:35 -04:00
kennethreitz 43705d84d6 test intervals 2017-05-15 20:05:00 -04:00
kennethreitz 6521a7dbc5 maya.intervals 2017-05-15 20:00:56 -04:00
kennethreitz 1796fc1d40 fix failing tests 2017-05-15 15:22:26 -04:00
8 changed files with 1023 additions and 67 deletions
+1 -1
View File
@@ -4,6 +4,6 @@ python:
- "3.6"
# command to install dependencies
install: pip install pipenv; pipenv install --dev
install: pip install pipenv; pipenv lock; pipenv install --dev
# command to run tests
script: pipenv run pytest
Generated
+21 -51
View File
@@ -1,102 +1,72 @@
{
"_meta": {
"hash": {
"sha256": "5617ff73ba51e60721267b24dc01e83f33d2a3692870b60e394b0f75ed2dc313"
},
"requires": {},
"sources": [
{
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
],
"requires": {},
"hash": {
"sha256": "5617ff73ba51e60721267b24dc01e83f33d2a3692870b60e394b0f75ed2dc313"
}
]
},
"default": {
"convertdate": {
"hash": "sha256:9c5d8dc984e51789ffaa92e988939ba8734c56ed718a3943998e7832fffa66ed",
"version": "==2.1.0"
},
"dateparser": {
"hash": "sha256:bbdb38e35dcf42653bbc88dc8870d0ef7eb5679af64b4f2ffc43d43acd3b24ba",
"version": "==0.5.1"
},
"ephem": {
"hash": "sha256:7a4c82b1def2893e02aec0394f108d24adb17bd7b0ca6f4bc78eb7120c0212ac",
"version": "==3.7.6.0"
"version": "==0.6.0"
},
"humanize": {
"hash": "sha256:a43f57115831ac7c70de098e6ac46ac13be00d69abbf60bdcac251344785bb19",
"version": "==0.5.1"
},
"jdatetime": {
"hash": "sha256:b6bb0bbd822eb0d3f9a55c4e8bfda0ccace3dcfa6d16bdcb29c124ad2ce66335",
"version": "==1.8.1"
},
"pendulum": {
"hash": "sha256:2417888ceddbb9c3903317ad321e27b2217aacf4ddcc1993ac3ad4db96f3a701",
"version": "==1.0.2"
"version": "==1.2.0"
},
"python-dateutil": {
"hash": "sha256:537bf2a8f8ce6f6862ad705cd68f9e405c0b5db014aa40fa29eab4335d4b1716",
"version": "==2.6.0"
},
"pytz": {
"hash": "sha256:a1ea35e87a63c7825846d5b5c81d23d668e8a102d3b1b465ce95afe1b3a2e065",
"version": "==2016.10"
"version": "==2017.2"
},
"pytzdata": {
"hash": "sha256:cf28a9bbb4e7f94ae21f4a3c64439aa44f81d5814fe9989e9ab7374658b6e596",
"version": "==2016.10"
"version": "==2017.2"
},
"regex": {
"hash": "sha256:45b62acff46cb886246e40227a872089d8d4972dbf2f114ec1ae64e5893e87bf",
"version": "==2017.02.08"
"version": "==2017.04.29"
},
"ruamel.ordereddict": {
"version": "==0.4.9"
},
"ruamel.yaml": {
"hash": "sha256:7cfd653648a1d4a635ce7ae254b4dc6ec7df931a5a653c79e627547be5dda71b",
"version": "==0.13.13"
"version": "==0.14.12"
},
"six": {
"hash": "sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1",
"version": "==1.10.0"
},
"tzlocal": {
"hash": "sha256:d160c2ce4f8b1831dabfe766bd844cf9012f766539cf84139c2faac5201882ce",
"version": "==1.3"
},
"umalqurra": {
"hash": "sha256:719f6a36f908ada1c29dae0d934dd0f1e1f6e3305784edbec23ad719397de678",
"version": "==0.2"
"version": "==1.4"
}
},
"develop": {
"appdirs": {
"hash": "sha256:85e58578db8f29538f3109c11250c2a5514a2fcdc9890d9b2fe777eb55517736",
"version": "==1.4.0"
"version": "==1.4.3"
},
"packaging": {
"hash": "sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388",
"version": "==16.8"
},
"py": {
"hash": "sha256:2d4bba2e25fff58140e6bdce1e485e89bb59776adbe01d490baa6b1f37a3dd6b",
"version": "==1.4.32"
"version": "==1.4.33"
},
"pyparsing": {
"hash": "sha256:67101d7acee692962f33dd30b5dce079ff532dd9aa99ff48d52a3dad51d2fe84",
"version": "==2.1.10"
"version": "==2.2.0"
},
"pytest": {
"hash": "sha256:da0ab50c7eec0683bc24f1c1137db1f4111752054ecdad63125e7ec71316b813",
"version": "==3.0.6"
"version": "==3.0.7"
},
"setuptools": {
"hash": "sha256:5f74aabe68c441b99dca68c22796d5cbf532cb38b0aeada17d1d3988809de6e6",
"version": "==34.1.1"
"version": "==35.0.2"
},
"six": {
"hash": "sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1",
"version": "==1.10.0"
}
}
}
}
+31 -3
View File
@@ -1,5 +1,5 @@
Maya: Timestamps for Humans™
============================
Maya: Datetimes for Humans™
===========================
.. image:: https://img.shields.io/pypi/v/maya.svg
:target: https://pypi.python.org/pypi/maya
@@ -73,6 +73,32 @@ Behold, datetimes for humans!
>>> rand_day.timezone
UTC
# Range of hours in a day:
>>> maya.interval(start=maya.now(), end=maya.now().add(days=1), interval=60*60)
<generator object intervals at 0x105ba5820>
☤ Advanced Usage of Maya
------------------------
In addition to timestamps, Maya also includes a wonderfuly powerful ``MayaInterval`` class, which represents a range of time (e.g. an event). With this class, you can perform a multitude of advanced calendar calculations with finese and ease.
For example:
.. code-block:: pycon
>>> from maya import MayaInterval
# Create an event that is one hour long, starting now.
>>> event_start = maya.now()
>>> event_end = event_start.add(hours=1)
>>> event = MayaInterval(start=event_start, end=event_end)
From here, there a a number of methods available to you, which you can use to compare this event to another event.
☤ Why is this useful?
---------------------
@@ -88,11 +114,13 @@ Behold, datetimes for humans!
☤ What about Delorean, Arrow, & Pendulum?
-----------------------------------------
All these project complement eachother, and are friends. Pendulum, for example, helps power Maya's parsing.
Arrow, for example, is a fantastic library, but isn't what I wanted in a datetime library. In many ways, it's better than Maya for certain things. In some ways, in my opinion, it's not.
I simply desire a sane API for datetimes that made sense to me for all the things I'd ever want to do—especially when dealing with timezone algebra. Arrow doesn't do all of the things I need (but it does a lot more!). Maya does do exactly what I need.
I think these projects complement each-other, personally. Maya is great for parsing websites. For example- Arrow supports floors and ceilings and spans of dates, which Maya does not at all.
I think these projects complement each-other, personally. Maya is great for parsing websites, and dealing with calendar events!
☤ Installing Maya
+100
View File
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""
maya.compat
~~~~~~~~~~~~~~~
This module handles import compatibility issues between Python 2 and
Python 3.
"""
import sys
# -------
# Pythons
# -------
# Syntax sugar.
_ver = sys.version_info
#: Python 2.x?
is_py2 = (_ver[0] == 2)
#: Python 3.x?
is_py3 = (_ver[0] == 3)
# ---------
# Specifics
# ---------
if is_py2:
cmp = cmp
elif is_py3:
def cmp(a, b):
"""
Compare two objects.
Returns a negative number if C{a < b}, zero if they are equal, and a
positive number if C{a > b}.
"""
if a < b:
return -1
elif a == b:
return 0
else:
return 1
def comparable(klass):
"""
Class decorator that ensures support for the special C{__cmp__} method.
On Python 2 this does nothing.
On Python 3, C{__eq__}, C{__lt__}, etc. methods are added to the class,
relying on C{__cmp__} to implement their comparisons.
"""
# On Python 2, __cmp__ will just work, so no need to add extra methods:
if not is_py3:
return klass
def __eq__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c == 0
def __ne__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c != 0
def __lt__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c < 0
def __le__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c <= 0
def __gt__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c > 0
def __ge__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c >= 0
klass.__lt__ = __lt__
klass.__gt__ = __gt__
klass.__le__ = __le__
klass.__ge__ = __ge__
klass.__eq__ = __eq__
klass.__ne__ = __ne__
return klass
+300 -8
View File
@@ -1,4 +1,3 @@
# ___ __ ___ _ _ ___
# || \/ | ||=|| \\// ||=||
# || | || || // || ||
@@ -6,19 +5,22 @@
# Ignore warnings for yaml usage.
import warnings
import ruamel.yaml
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)
import email.utils
import time
from datetime import datetime as Datetime
from datetime import timedelta, datetime as Datetime
import functools
import pytz
import humanize
import dateparser
import pendulum
from tzlocal import get_localzone
from compat import cmp, comparable
_EPOCH_START = (1970, 1, 1)
@@ -41,7 +43,6 @@ def validate_class_type_arguments(operator):
return inner
class MayaDT(object):
"""The Maya Datetime object."""
@@ -59,10 +60,9 @@ class MayaDT(object):
"""Return's the datetime's format"""
return format(self.datetime(), *args, **kwargs)
@validate_class_type_arguments('==')
def __eq__(self, maya_dt):
return self._epoch == maya_dt._epoch
return int(self._epoch) == int(maya_dt._epoch)
@validate_class_type_arguments('!=')
def __ne__(self, maya_dt):
@@ -84,6 +84,19 @@ class MayaDT(object):
def __ge__(self, maya_dt):
return self._epoch >= maya_dt._epoch
def __hash__(self):
return hash(int(self.epoch))
def __add__(self, item):
return self.add(seconds=seconds_or_timedelta(item).total_seconds())
def __radd__(self, item):
return self + item
def __sub__(self, item):
return self.subtract(
seconds=seconds_or_timedelta(item).total_seconds())
def add(self, **kwargs):
""""Returns a new MayaDT object with the given offsets."""
return self.from_datetime(pendulum.instance(self.datetime()).add(**kwargs))
@@ -187,7 +200,7 @@ class MayaDT(object):
def rfc3339(self):
"""Returns an RFC 3339 representation of the MayaDT."""
return self.datetime().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4]+"Z"
return self.datetime().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4] + "Z"
# Properties
# ----------
@@ -247,6 +260,263 @@ class MayaDT(object):
return humanize.naturaltime(dt)
def to_utc_offset_naive(dt):
if dt.tzinfo is None:
return dt
return dt.astimezone(pytz.utc).replace(tzinfo=None)
def to_utc_offset_aware(dt):
if dt.tzinfo is not None:
return dt
return pytz.utc.localize(dt)
def to_iso8601(dt):
return to_utc_offset_naive(dt).isoformat() + 'Z'
def end_of_day_midnight(dt):
return dt if dt.time() == time.min else \
(dt.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1))
@comparable
class MayaInterval(object):
"""
A MayaInterval represents a range between two datetimes, inclusive of the start
and exclusive of the end.
"""
def __init__(self, start=None, end=None, duration=None):
try:
# Ensure that proper arguments were passed.
assert any((
(start and end),
(start and duration is not None),
(end and duration is not None),
))
assert not all((start, end, duration is not None))
except AssertionError:
raise ValueError(
'Exactly 2 of start, end, and duration must be specified')
# Convert duration to timedelta if seconds were provided.
duration = seconds_or_timedelta(duration)
if not start:
start = end - duration
if not end:
end = start + duration
if start > end:
raise ValueError('MayaInterval cannot end before it starts')
self.start = start
self.end = end
def __repr__(self):
return '<MayaInterval start={0!r} end={1!r}>'.format(self.start, self.end)
def iso8601(self):
"""Returns an ISO 8601 representation of the MayaInterval."""
return '{0}/{1}'.format(self.start.iso6801, self.end.iso8601)
@classmethod
def from_iso8601(cls, s):
# # Start and end, such as "2007-03-01T13:00:00Z/2008-05-11T15:30:00Z"
# start, end = s.split('/')
# try:
# start = parse(start)
# except pendulum.parsing.exceptions.ParserError:
# start = self._parse_iso8601_duration(start)
# try:
# end = parse(start)
# except pendulum.parsing.exceptions.ParserError as e:
# end = self._parse_iso8601_duration(start)
# # Start and duration, such as "2007-03-01T13:00:00Z/P1Y2M10DT2H30M"
# # Duration and end, such as "P1Y2M10DT2H30M/2008-05-11T15:30:00Z"
raise NotImplementedError()
def __and__(self, i):
return self.intersection(i)
def __or__(self, i):
return self.combine(i)
def __eq__(self, i):
return (
self.start == i.start and
self.end == i.end
)
def __hash__(self):
return hash((self.start, self.end))
def __iter__(self):
yield self.start
yield self.end
def __cmp__(self, i):
return (
cmp(self.start, i.start) or
cmp(self.end, i.end)
)
@property
def duration(self):
return self.timedelta.total_seconds()
@property
def timedelta(self):
return timedelta(seconds=(self.end.epoch - self.start.epoch))
@property
def is_instant(self):
return self.timedelta == timedelta(seconds=0)
def intersects(self, i):
return self & i is not None
@property
def midpoint(self):
return self.start.add(seconds=(self.duration / 2))
def combine(self, i):
"""Returns a combined list of timespans, merged together."""
ii = sorted([self, i])
if self & i or self.is_adjacent(i):
return [
MayaInterval(
ii[0].start,
max(ii[0].end, ii[1].end),
),
]
return ii
def subtract(self, i):
""""Removes the given inerval."""
if not self & i:
return [self]
elif i.contains(self):
return []
ii = []
if self.start < i.start:
ii.append(MayaInterval(self.start, i.start))
if self.end > i.end:
ii.append(MayaInterval(i.end, self.end))
return ii
def split(self, duration, include_remainder=True):
# Convert seconds to timedelta, if appropriate.
duration = seconds_or_timedelta(duration)
assert duration > timedelta(seconds=0), 'cannot call split with a non-positive timedelta'
start = self.start
while start < self.end:
if start + duration <= self.end:
yield MayaInterval(start, start + duration)
elif include_remainder:
yield MayaInterval(start, self.end)
start += duration
def quantize(self, duration, snap_out=False, timezone='UTC'):
"""Returns a quanitzed interval."""
# Convert seconds to timedelta, if appropriate.
duration = seconds_or_timedelta(duration)
timezone = pytz.timezone(timezone)
assert duration > timedelta(seconds=0), 'cannot quantize by non-positive timedelta'
epoch = timezone.localize(Datetime(1970, 1, 1))
seconds = int(duration.total_seconds())
start_seconds = int((self.start.datetime(naive=False) - epoch).total_seconds())
end_seconds = int((self.end.datetime(naive=False) - epoch).total_seconds())
if start_seconds % seconds and not snap_out:
start_seconds += seconds
if end_seconds % seconds and snap_out:
end_seconds += seconds
start_seconds -= start_seconds % seconds
end_seconds -= end_seconds % seconds
if start_seconds > end_seconds:
start_seconds = end_seconds
return MayaInterval(
start=MayaDT.from_datetime(epoch).add(seconds=start_seconds),
end=MayaDT.from_datetime(epoch).add(seconds=end_seconds),
)
def intersection(self, i):
"""Returns the intersection between two intervals."""
start = max(self.start, i.start)
end = min(self.end, i.end)
either_instant = self.is_instant or i.is_instant
instant_overlap = (
self.start == i.start or
start <= end
)
if (either_instant and instant_overlap) or (start < end):
return MayaInterval(start, end)
def contains(self, i):
return (
self.start <= i.start and
self.end >= i.end
)
def __contains__(self, item):
if isinstance(item, MayaDT):
return self.contains_dt(item)
return self.contains(item)
def contains_dt(self, dt):
return self.start <= dt < self.end
def is_adjacent(self, i):
return (
self.start == i.end or
self.end == i.start
)
@property
def icalendar(self):
ical_dt_format = '%Y%m%dT%H%M%SZ'
return """
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART:{0}
DTEND:{1}
END:VEVENT
END:VCALENDAR
""".format(
self.start.datetime().strftime(ical_dt_format),
self.end.datetime().strftime(ical_dt_format),
).replace(' ', '').strip('\r\n').replace('\n', '\r\n')
@staticmethod
def flatten(ii):
return functools.reduce(lambda reduced, i: (
(reduced[:-1] + i.combine(reduced[-1]))
if reduced else [i]
), sorted(ii), [])
@classmethod
def from_datetime(cls, start_dt=None, end_dt=None, duration=None):
start = MayaDT.from_datetime(start_dt) if start_dt else None
end = MayaDT.from_datetime(end_dt) if end_dt else None
return cls(start=start, end=end, duration=duration)
def now():
@@ -254,6 +524,7 @@ def now():
epoch = time.time()
return MayaDT(epoch=epoch)
def when(string, timezone='UTC'):
""""Returns a MayaDT instance for the human moment specified.
@@ -267,13 +538,15 @@ def when(string, timezone='UTC'):
timezone -- timezone referenced from (default: 'UTC')
"""
dt = dateparser.parse(string, settings={'TIMEZONE': timezone, 'RETURN_AS_TIMEZONE_AWARE': True, 'TO_TIMEZONE': 'UTC'})
dt = dateparser.parse(string,
settings={'TIMEZONE': timezone, 'RETURN_AS_TIMEZONE_AWARE': True, 'TO_TIMEZONE': 'UTC'})
if dt is None:
raise ValueError('invalid datetime input specified.')
return MayaDT.from_datetime(dt)
def parse(string, day_first=False):
""""Returns a MayaDT instance for the machine-produced moment specified.
@@ -285,3 +558,22 @@ def parse(string, day_first=False):
"""
dt = pendulum.parse(string, day_first=day_first)
return MayaDT.from_datetime(dt)
def seconds_or_timedelta(s):
# Convert seconds into timedelta.
if isinstance(s, int):
s = timedelta(seconds=s)
return s
def intervals(start, end, interval):
"""Yields MayaDT objects between the start and end MayaDTs given, at a given interval (seconds or timedelta)."""
interval = seconds_or_timedelta(interval)
current_timestamp = start
while current_timestamp.epoch < end.epoch:
yield current_timestamp
current_timestamp = current_timestamp.add(seconds=interval.seconds)
+2 -2
View File
@@ -35,11 +35,11 @@ required = [
setup(
name='maya',
version='0.1.8',
version='0.3.1',
description='Datetimes for Humans.',
long_description=long_description,
author='Kenneth Reitz',
author_email='me@kennethreitz.com',
author_email='me@kennethreitz.org',
url='https://github.com/kennethreitz/maya',
py_modules=['maya'],
install_requires=required,
+27 -2
View File
@@ -1,5 +1,6 @@
import pytest
import copy
from datetime import timedelta
import maya
@@ -79,7 +80,7 @@ def test_machine_parse():
def test_dt_tz_translation():
d1 = maya.now().datetime()
d2 = maya.now().datetime(to_timezone='US/Eastern')
d2 = maya.now().datetime(to_timezone='EST')
assert (d1.hour - d2.hour) % 24 == 5
@@ -87,7 +88,7 @@ def test_dt_tz_naive():
d1 = maya.now().datetime(naive=True)
assert d1.tzinfo is None
d2 = maya.now().datetime(to_timezone='US/Eastern', naive=True)
d2 = maya.now().datetime(to_timezone='EST', naive=True)
assert d2.tzinfo is None
assert (d1.hour - d2.hour) % 24 == 5
@@ -202,3 +203,27 @@ def test_comparison_operations():
now > 1
with pytest.raises(TypeError):
now >= 1
def test_intervals():
now = maya.now()
tomorrow = now.add(days=1)
assert len(list(maya.intervals(now, tomorrow, 60*60))) == 24
def test_dunder_add():
now = maya.now()
assert now + 1 == now.add(seconds=1)
assert now + timedelta(seconds=1) == now.add(seconds=1)
def test_dunder_radd():
now = maya.now()
assert now.add(seconds=1) == now + 1
assert now.add(seconds=1) == now + timedelta(seconds=1)
def test_dunder_sub():
now = maya.now()
assert now - 1 == now.subtract(seconds=1)
assert now - timedelta(seconds=1) == now.subtract(seconds=1)
+541
View File
@@ -0,0 +1,541 @@
import random
from datetime import datetime, timedelta
import pytest
import pytz
import maya
from compat import cmp
Los_Angeles = pytz.timezone('America/Los_Angeles')
New_York = pytz.timezone('America/New_York')
Melbourne = pytz.timezone('Australia/Melbourne')
def test_interval_requires_2_of_start_end_duration():
start = maya.now()
end = start.add(hours=1)
with pytest.raises(ValueError):
maya.MayaInterval(start=start)
with pytest.raises(ValueError):
maya.MayaInterval(end=end)
with pytest.raises(ValueError):
maya.MayaInterval(duration=60)
with pytest.raises(ValueError):
maya.MayaInterval(start=start, end=end, duration=60)
maya.MayaInterval(start=start, end=end)
maya.MayaInterval(start=start, duration=60)
maya.MayaInterval(end=end, duration=60)
def test_interval_requires_end_time_after_or_on_start_time():
with pytest.raises(ValueError):
maya.MayaInterval(start=maya.now(), duration=0)
maya.MayaInterval(start=maya.now(), duration=-1)
def test_interval_init_start_end():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert interval.start == start
assert interval.end == end
def test_interval_init_start_duration():
start = maya.now()
duration = 1
interval = maya.MayaInterval(start=start, duration=duration)
assert interval.start == start
assert interval.end == start.add(seconds=duration)
def test_interval_init_end_duration():
end = maya.now()
duration = 1
interval = maya.MayaInterval(end=end, duration=duration)
assert interval.end == end
assert interval.start == end.subtract(seconds=duration)
@pytest.mark.parametrize('start_doy1,end_doy1,start_doy2,end_doy2,intersection_doys', (
(0, 2, 1, 3, (1, 2)),
(0, 2, 3, 4, None),
(0, 2, 2, 3, None),
(0, 1, 0, 1, (0, 1)),
(1, 1, 1, 3, (1, 1)),
(1, 1, 1, 1, (1, 1)),
(1, 1, 2, 3, None),
(2, 2, 1, 3, (2, 2)),
(1, 3, 1, 1, (1, 1)),
(2, 3, 1, 1, None),
(1, 3, 2, 2, (2, 2)),
), ids=(
'overlapping',
'non-overlapping',
'adjacent',
'equal',
'instant overlapping start only',
'instant equal',
'instant disjoint',
'instant overlapping',
'instant overlapping start only (left)',
'instant disjoint (left)',
'instant overlapping (left)'
))
def test_interval_intersection(
start_doy1, end_doy1, start_doy2, end_doy2, intersection_doys
):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(
base.add(days=start_doy1),
base.add(days=end_doy1),
)
interval2 = maya.MayaInterval(
base.add(days=start_doy2),
base.add(days=end_doy2),
)
if intersection_doys:
start_doy_intersection, end_doy_intersection = intersection_doys
assert interval1 & interval2 == maya.MayaInterval(
base.add(days=start_doy_intersection),
base.add(days=end_doy_intersection),
)
else:
assert (interval1 & interval2) is None
def test_interval_intersects():
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval = maya.MayaInterval(base, base.add(days=1))
assert interval.intersects(interval)
assert not interval.intersects(maya.MayaInterval(
base.add(days=2),
base.add(days=3),
))
def test_and_operator():
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(base, base.add(days=2))
interval2 = maya.MayaInterval(base.add(days=1), base.add(days=3))
assert (
interval1 & interval2 ==
interval2 & interval1 ==
interval1.intersection(interval2)
)
def test_interval_eq_operator():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert interval == maya.MayaInterval(start=start, end=end)
assert interval != maya.MayaInterval(start=start, end=end.add(days=1))
def test_interval_timedelta():
start = maya.now()
delta = timedelta(hours=1)
interval = maya.MayaInterval(start=start, duration=delta)
assert interval.timedelta == delta
def test_interval_duration():
start = maya.now()
delta = timedelta(hours=1)
interval = maya.MayaInterval(start=start, duration=delta)
assert interval.duration == delta.total_seconds()
@pytest.mark.parametrize('start_doy1,end_doy1,start_doy2,end_doy2,expected', (
(0, 2, 1, 3, False),
(0, 2, 3, 4, False),
(0, 2, 2, 3, False),
(0, 1, 0, 1, True),
(0, 3, 1, 2, True),
), ids=(
'overlapping',
'non-overlapping',
'adjacent',
'equal',
'subset',
))
def test_interval_contains(
start_doy1, end_doy1, start_doy2, end_doy2, expected
):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(
base.add(days=start_doy1),
base.add(days=end_doy1),
)
interval2 = maya.MayaInterval(
base.add(days=start_doy2),
base.add(days=end_doy2),
)
assert interval1.contains(interval2) is expected
assert (interval2 in interval1) is expected
@pytest.mark.parametrize('start_doy,end_doy,dt_doy,expected', (
(2, 4, 1, False),
(2, 4, 2, True),
(2, 4, 3, True),
(2, 4, 4, False),
(2, 4, 5, False),
), ids=(
'before-start',
'on-start',
'during',
'on-end',
'after-end',
))
def test_interval_in_operator_maya_dt(
start_doy, end_doy, dt_doy, expected
):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval = maya.MayaInterval(
start=base.add(days=start_doy),
end=base.add(days=end_doy),
)
dt = base.add(days=dt_doy)
assert (dt in interval) is expected
def test_interval_hash():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert hash(interval) == hash(maya.MayaInterval(start=start, end=end))
assert hash(interval) != hash(maya.MayaInterval(
start=start, end=end.add(days=1)))
def test_interval_iter():
start = maya.now()
end = start.add(days=1)
assert tuple(maya.MayaInterval(start=start, end=end)) == (start, end)
@pytest.mark.parametrize('start1,end1,start2,end2,expected', [
(1, 2, 1, 2, 0),
(1, 3, 2, 4, -1),
(2, 4, 1, 3, 1),
(1, 2, 1, 3, -1),
], ids=(
'equal',
'less-than',
'greater-than',
'use-end-time-if-start-time-identical',
))
def test_interval_cmp(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2),
end=base.add(days=end2),
)
assert cmp(interval1, interval2) == expected
@pytest.mark.parametrize('start1,end1,start2,end2,expected', [
(1, 2, 2, 3, [(1, 3)]),
(1, 3, 2, 4, [(1, 4)]),
(1, 2, 3, 4, [(1, 2), (3, 4)]),
(1, 5, 2, 3, [(1, 5)]),
], ids=(
'adjacent',
'overlapping',
'non-overlapping',
'contains',
))
def test_interval_combine(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2),
end=base.add(days=end2),
)
expected_intervals = [maya.MayaInterval(
start=base.add(days=start),
end=base.add(days=end),
) for start, end in expected]
assert interval1.combine(interval2) == expected_intervals
assert interval2.combine(interval1) == expected_intervals
@pytest.mark.parametrize('start1,end1,start2,end2,expected', [
(1, 2, 3, 4, [(1, 2)]),
(1, 2, 2, 4, [(1, 2)]),
(2, 3, 1, 4, []),
(1, 4, 2, 3, [(1, 2), (3, 4)]),
(1, 4, 0, 2, [(2, 4)]),
(1, 4, 3, 5, [(1, 3)]),
(1, 4, 1, 2, [(2, 4)]),
(1, 4, 3, 4, [(1, 3)]),
], ids=(
'non-overlapping',
'adjacent',
'contains',
'splits',
'overlaps-left',
'overlaps-right',
'overlaps-left-identical-start',
'overlaps-right-identical-end',
))
def test_interval_subtract(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2),
end=base.add(days=end2),
)
expected_intervals = [maya.MayaInterval(
start=base.add(days=start),
end=base.add(days=end),
) for start, end in expected]
assert interval1.subtract(interval2) == expected_intervals
@pytest.mark.parametrize('start1,end1,start2,end2,expected', [
(1, 2, 2, 3, True),
(2, 3, 1, 2, True),
(1, 3, 2, 3, False),
(2, 3, 4, 5, False),
], ids=(
'adjacent-right',
'adjacent-left',
'overlapping',
'non-overlapping',
))
def test_interval_is_adjacent(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2),
end=base.add(days=end2),
)
assert interval1.is_adjacent(interval2) == expected
@pytest.mark.parametrize('start,end,delta,include_remainder,expected', [
(0, 10, 5, False, [(0, 5), (5, 10)]),
(0, 10, 5, True, [(0, 5), (5, 10)]),
(0, 10, 3, False, [(0, 3), (3, 6), (6, 9)]),
(0, 10, 3, True, [(0, 3), (3, 6), (6, 9), (9, 10)]),
(0, 2, 5, False, []),
(0, 2, 5, True, [(0, 2)]),
], ids=(
'even-split',
'even-split-include-partial',
'uneven-split-do-not-include-partial',
'uneven-split-include-partial',
'delta-larger-than-timepsan-do-not-include-partial',
'delta-larger-than-timepsan-include-partial',
))
def test_interval_split(start, end, delta, include_remainder, expected):
base = maya.now()
interval = maya.MayaInterval(
start=base.add(days=start),
end=base.add(days=end),
)
delta = timedelta(days=delta)
expected_intervals = [
maya.MayaInterval(
start=base.add(days=s),
end=base.add(days=e),
) for s, e in expected
]
assert expected_intervals == list(interval.split(
delta, include_remainder=include_remainder))
def test_interval_split_non_positive_delta():
start = maya.now()
end = start.add(days=1)
interval = maya.MayaInterval(start=start, end=end)
with pytest.raises(AssertionError):
list(interval.split(timedelta(seconds=0)))
with pytest.raises(AssertionError):
list(interval.split(timedelta(seconds=-10)))
@pytest.mark.parametrize('start,end,minutes,timezone,snap_out,expected_start,expected_end', [
((5, 12), (8, 48), 30, None, False, (5, 30), (8, 30)),
((5, 12), (8, 48), 30, None, True, (5, 0), (9, 0)),
((5, 15), (9, 0), 15, None, False, (5, 15), (9, 0)),
((5, 15), (9, 0), 15, None, True, (5, 15), (9, 0)),
((6, 50), (9, 15), 60, 'America/New_York', False, (7, 0), (9, 0)),
((6, 50), (9, 15), 60, 'America/New_York', True, (6, 0), (10, 0)),
((6, 20), (6, 50), 60, None, False, (6, 0), (6, 0)),
((6, 20), (6, 50), 60, None, True, (6, 0), (7, 0)),
((6, 20), (6, 50), 60, 'America/Chicago', False, (6, 0), (6, 0)),
((6, 20), (6, 50), 60, 'America/Chicago', True, (6, 0), (7, 0)),
], ids=(
'normal',
'normal-snap_out',
'already-quantized',
'already-quantized-snap_out',
'with-timezone',
'with-timezone-snap_out',
'too-small',
'too-small-snap_out',
'too-small-with-timezone',
'too-small-with-timezone-snap_out',
))
def test_quantize(start, end, minutes, timezone, snap_out, expected_start, expected_end):
base = maya.MayaDT.from_datetime(datetime(2017, 1, 1))
interval = maya.MayaInterval(
start=base.add(hours=start[0], minutes=start[1]),
end=base.add(hours=end[0], minutes=end[1]),
)
kwargs = {'timezone': timezone} if timezone is not None else {}
quantized_interval = interval.quantize(
timedelta(minutes=minutes),
snap_out=snap_out,
**kwargs
)
assert quantized_interval == maya.MayaInterval(
start=base.add(hours=expected_start[0], minutes=expected_start[1]),
end=base.add(hours=expected_end[0], minutes=expected_end[1]),
)
def test_quantize_invalid_delta():
start = maya.now()
end = start.add(days=1)
interval = maya.MayaInterval(start=start, end=end)
with pytest.raises(AssertionError):
interval.quantize(timedelta(minutes=0))
with pytest.raises(AssertionError):
interval.quantize(timedelta(minutes=-1))
def test_interval_flatten_non_overlapping():
step = 2
max_hour = 20
base = maya.now()
intervals = [maya.MayaInterval(
start=base.add(hours=hour),
duration=timedelta(hours=step - 1),
) for hour in range(0, max_hour, step)]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == sorted(intervals)
def test_interval_flatten_adjacent():
step = 2
max_hour = 20
base = maya.when('jan/1/2011')
intervals = [
maya.MayaInterval(
start=base.add(hours=hour),
duration=timedelta(hours=step),
) for hour in range(0, max_hour, step)
]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [maya.MayaInterval(
start=base,
duration=timedelta(hours=max_hour),
)]
def test_interval_flatten_intersecting():
step = 2
max_hour = 20
base = maya.now()
intervals = [maya.MayaInterval(
start=base.add(hours=hour),
duration=timedelta(hours=step, minutes=30),
) for hour in range(0, max_hour, step)]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [maya.MayaInterval(
start=base,
duration=timedelta(hours=max_hour, minutes=30),
)]
def test_interval_flatten_containing():
step = 2
max_hour = 20
base = maya.now()
containing_interval = maya.MayaInterval(
start=base,
end=base.add(hours=max_hour + step),
)
intervals = [maya.MayaInterval(
start=base.add(hours=hour),
duration=timedelta(hours=step - 1),
) for hour in range(2, max_hour, step)]
intervals.append(containing_interval)
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [containing_interval]
def test_interval_from_datetime():
start = maya.now()
duration = timedelta(hours=1)
end = start + duration
interval = maya.MayaInterval.from_datetime(
start_dt=start.datetime(naive=False),
end_dt=end.datetime(naive=False),
)
assert interval.start == start
assert interval.end == end
interval2 = maya.MayaInterval.from_datetime(
start_dt=start.datetime(naive=False),
duration=duration,
)
assert interval2.start == start
assert interval2.end == end
interval3 = maya.MayaInterval.from_datetime(
end_dt=end.datetime(naive=False),
duration=duration,
)
assert interval3.start == start
assert interval3.end == end