Compare commits

...

35 Commits

Author SHA1 Message Date
kennethreitz 7814ec2864 fix broken tests 2017-05-27 13:04:22 -04:00
kennethreitz f39c932039 cleanups 2017-05-27 12:12:58 -04:00
kennethreitz 96ff770071 move things around 2017-05-27 12:11:51 -04:00
kennethreitz cd16ee94f0 move things around 2017-05-27 12:11:45 -04:00
kennethreitz 7c68489682 tests 2017-05-27 12:10:48 -04:00
kennethreitz 93e163722b Merge branch 'Tafkas-master' 2017-05-27 12:08:32 -04:00
kennethreitz b3ac13fcbf Merge pull request #59 from Tafkas/master
add compatibility for Python3
2017-05-27 09:08:18 -07:00
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
11 changed files with 1305 additions and 348 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
+1 -1
View File
@@ -1,2 +1,2 @@
tests:
pytest test_maya.py
pytest test_maya.py test_maya_interval.py
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
-287
View File
@@ -1,287 +0,0 @@
# ___ __ ___ _ _ ___
# || \/ | ||=|| \\// ||=||
# || | || || // || ||
# Ignore warnings for yaml usage.
import warnings
import ruamel.yaml
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)
import email.utils
import time
from datetime import datetime as Datetime
import pytz
import humanize
import dateparser
import pendulum
from tzlocal import get_localzone
_EPOCH_START = (1970, 1, 1)
def validate_class_type_arguments(operator):
"""
Decorator to validate all the arguments to function
are of the type of calling class
"""
def inner(function):
def wrapper(self, *args, **kwargs):
for arg in args + tuple(kwargs.values()):
if not isinstance(arg, self.__class__):
raise TypeError('unorderable types: {}() {} {}()'.format(
type(self).__name__, operator, type(arg).__name__))
return function(self, *args, **kwargs)
return wrapper
return inner
class MayaDT(object):
"""The Maya Datetime object."""
def __init__(self, epoch):
super(MayaDT, self).__init__()
self._epoch = epoch
def __repr__(self):
return '<MayaDT epoch={}>'.format(self._epoch)
def __str__(self):
return self.rfc2822()
def __format__(self, *args, **kwargs):
"""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
@validate_class_type_arguments('!=')
def __ne__(self, maya_dt):
return self._epoch != maya_dt._epoch
@validate_class_type_arguments('<')
def __lt__(self, maya_dt):
return self._epoch < maya_dt._epoch
@validate_class_type_arguments('<=')
def __le__(self, maya_dt):
return self._epoch <= maya_dt._epoch
@validate_class_type_arguments('>')
def __gt__(self, maya_dt):
return self._epoch > maya_dt._epoch
@validate_class_type_arguments('>=')
def __ge__(self, maya_dt):
return self._epoch >= maya_dt._epoch
def add(self, **kwargs):
""""Returns a new MayaDT object with the given offsets."""
return self.from_datetime(pendulum.instance(self.datetime()).add(**kwargs))
def subtract(self, **kwargs):
""""Returns a new MayaDT object with the given offsets."""
return self.from_datetime(pendulum.instance(self.datetime()).subtract(**kwargs))
# Timezone Crap
# -------------
@property
def timezone(self):
"""Returns the UTC tzinfo name. It's always UTC. Always."""
return 'UTC'
@property
def _tz(self):
"""Returns the UTC tzinfo object."""
return pytz.timezone(self.timezone)
@property
def local_timezone(self):
"""Returns the name of the local timezone, for informational purposes."""
return self._local_tz.zone
@property
def _local_tz(self):
"""Returns the local timezone."""
return get_localzone()
@staticmethod
def __dt_to_epoch(dt):
"""Converts a datetime into an epoch."""
# Assume UTC if no datetime is provided.
if dt.tzinfo is None:
dt = dt.replace(tzinfo=pytz.utc)
epoch_start = Datetime(*_EPOCH_START, tzinfo=pytz.timezone('UTC'))
return (dt - epoch_start).total_seconds()
# Importers
# ---------
@classmethod
def from_datetime(klass, dt):
"""Returns MayaDT instance from datetime."""
return klass(klass.__dt_to_epoch(dt))
@classmethod
def from_iso8601(klass, string):
"""Returns MayaDT instance from iso8601 string."""
return parse(string)
@staticmethod
def from_rfc2822(string):
"""Returns MayaDT instance from rfc2822 string."""
return parse(string)
@staticmethod
def from_rfc3339(string):
"""Returns MayaDT instance from rfc3339 string."""
return parse(string)
# Exporters
# ---------
def datetime(self, to_timezone=None, naive=False):
"""Returns a timezone-aware datetime...
Defaulting to UTC (as it should).
Keyword Arguments:
to_timezone {string} -- timezone to convert to (default: None/UTC)
naive {boolean} -- if True, the tzinfo is simply dropped (default: False)
"""
if to_timezone:
dt = self.datetime().astimezone(pytz.timezone(to_timezone))
else:
dt = Datetime.utcfromtimestamp(self._epoch)
dt.replace(tzinfo=self._tz)
# Strip the timezone info if requested to do so.
if naive:
return dt.replace(tzinfo=None)
else:
if dt.tzinfo is None:
dt = dt.replace(tzinfo=self._tz)
return dt
def iso8601(self):
"""Returns an ISO 8601 representation of the MayaDT."""
# Get a timezone-naive datetime.
dt = self.datetime(naive=True)
return '{}Z'.format(dt.isoformat())
def rfc2822(self):
"""Returns an RFC 2822 representation of the MayaDT."""
return email.utils.formatdate(self.epoch, usegmt=True)
def rfc3339(self):
"""Returns an RFC 3339 representation of the MayaDT."""
return self.datetime().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4]+"Z"
# Properties
# ----------
@property
def year(self):
return self.datetime().year
@property
def month(self):
return self.datetime().month
@property
def day(self):
return self.datetime().day
@property
def week(self):
return self.datetime().isocalendar()[1]
@property
def weekday(self):
"""Return the day of the week as an integer. Monday is 1 and Sunday is 7"""
return self.datetime().isoweekday()
@property
def hour(self):
return self.datetime().hour
@property
def minute(self):
return self.datetime().minute
@property
def second(self):
return self.datetime().second
@property
def microsecond(self):
return self.datetime().microsecond
@property
def epoch(self):
return self._epoch
# Human Slang Extras
# ------------------
def slang_date(self):
""""Returns human slang representation of date."""
dt = self.datetime(naive=True, to_timezone=self.local_timezone)
return humanize.naturaldate(dt)
def slang_time(self):
""""Returns human slang representation of time."""
dt = self.datetime(naive=True, to_timezone=self.local_timezone)
return humanize.naturaltime(dt)
def now():
"""Returns a MayaDT instance for this exact moment."""
epoch = time.time()
return MayaDT(epoch=epoch)
def when(string, timezone='UTC'):
""""Returns a MayaDT instance for the human moment specified.
Powered by dateparser. Useful for scraping websites.
Examples:
'next week', 'now', 'tomorrow', '300 years ago', 'August 14, 2015'
Keyword Arguments:
string -- string to be parsed
timezone -- timezone referenced from (default: '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.
Powered by pendulum. Accepts most known formats. Useful for working with data.
Keyword Arguments:
string -- string to be parsed
day_first -- if true, the first value (e.g. 01/05/2016) is parsed as day (default: False)
"""
dt = pendulum.parse(string, day_first=day_first)
return MayaDT.from_datetime(dt)
+1
View File
@@ -0,0 +1 @@
from .core import *
+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
+579
View File
@@ -0,0 +1,579 @@
# ___ __ ___ _ _ ___
# || \/ | ||=|| \\// ||=||
# || | || || // || ||
# Ignore warnings for yaml usage.
import warnings
import ruamel.yaml
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)
import email.utils
import time
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)
def validate_class_type_arguments(operator):
"""
Decorator to validate all the arguments to function
are of the type of calling class
"""
def inner(function):
def wrapper(self, *args, **kwargs):
for arg in args + tuple(kwargs.values()):
if not isinstance(arg, self.__class__):
raise TypeError('unorderable types: {}() {} {}()'.format(
type(self).__name__, operator, type(arg).__name__))
return function(self, *args, **kwargs)
return wrapper
return inner
class MayaDT(object):
"""The Maya Datetime object."""
def __init__(self, epoch):
super(MayaDT, self).__init__()
self._epoch = epoch
def __repr__(self):
return '<MayaDT epoch={}>'.format(self._epoch)
def __str__(self):
return self.rfc2822()
def __format__(self, *args, **kwargs):
"""Return's the datetime's format"""
return format(self.datetime(), *args, **kwargs)
@validate_class_type_arguments('==')
def __eq__(self, maya_dt):
return int(self._epoch) == int(maya_dt._epoch)
@validate_class_type_arguments('!=')
def __ne__(self, maya_dt):
return int(self._epoch) != int(maya_dt._epoch)
@validate_class_type_arguments('<')
def __lt__(self, maya_dt):
return int(self._epoch) < int(maya_dt._epoch)
@validate_class_type_arguments('<=')
def __le__(self, maya_dt):
return int(self._epoch) <= int(maya_dt._epoch)
@validate_class_type_arguments('>')
def __gt__(self, maya_dt):
return int(self._epoch) > int(maya_dt._epoch)
@validate_class_type_arguments('>=')
def __ge__(self, maya_dt):
return int(self._epoch) >= int(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))
def subtract(self, **kwargs):
""""Returns a new MayaDT object with the given offsets."""
return self.from_datetime(pendulum.instance(self.datetime()).subtract(**kwargs))
# Timezone Crap
# -------------
@property
def timezone(self):
"""Returns the UTC tzinfo name. It's always UTC. Always."""
return 'UTC'
@property
def _tz(self):
"""Returns the UTC tzinfo object."""
return pytz.timezone(self.timezone)
@property
def local_timezone(self):
"""Returns the name of the local timezone, for informational purposes."""
return self._local_tz.zone
@property
def _local_tz(self):
"""Returns the local timezone."""
return get_localzone()
@staticmethod
def __dt_to_epoch(dt):
"""Converts a datetime into an epoch."""
# Assume UTC if no datetime is provided.
if dt.tzinfo is None:
dt = dt.replace(tzinfo=pytz.utc)
epoch_start = Datetime(*_EPOCH_START, tzinfo=pytz.timezone('UTC'))
return (dt - epoch_start).total_seconds()
# Importers
# ---------
@classmethod
def from_datetime(klass, dt):
"""Returns MayaDT instance from datetime."""
return klass(klass.__dt_to_epoch(dt))
@classmethod
def from_iso8601(klass, string):
"""Returns MayaDT instance from iso8601 string."""
return parse(string)
@staticmethod
def from_rfc2822(string):
"""Returns MayaDT instance from rfc2822 string."""
return parse(string)
@staticmethod
def from_rfc3339(string):
"""Returns MayaDT instance from rfc3339 string."""
return parse(string)
# Exporters
# ---------
def datetime(self, to_timezone=None, naive=False):
"""Returns a timezone-aware datetime...
Defaulting to UTC (as it should).
Keyword Arguments:
to_timezone {string} -- timezone to convert to (default: None/UTC)
naive {boolean} -- if True, the tzinfo is simply dropped (default: False)
"""
if to_timezone:
dt = self.datetime().astimezone(pytz.timezone(to_timezone))
else:
dt = Datetime.utcfromtimestamp(self._epoch)
dt.replace(tzinfo=self._tz)
# Strip the timezone info if requested to do so.
if naive:
return dt.replace(tzinfo=None)
else:
if dt.tzinfo is None:
dt = dt.replace(tzinfo=self._tz)
return dt
def iso8601(self):
"""Returns an ISO 8601 representation of the MayaDT."""
# Get a timezone-naive datetime.
dt = self.datetime(naive=True)
return '{}Z'.format(dt.isoformat())
def rfc2822(self):
"""Returns an RFC 2822 representation of the MayaDT."""
return email.utils.formatdate(self.epoch, usegmt=True)
def rfc3339(self):
"""Returns an RFC 3339 representation of the MayaDT."""
return self.datetime().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4] + "Z"
# Properties
# ----------
@property
def year(self):
return self.datetime().year
@property
def month(self):
return self.datetime().month
@property
def day(self):
return self.datetime().day
@property
def week(self):
return self.datetime().isocalendar()[1]
@property
def weekday(self):
"""Return the day of the week as an integer. Monday is 1 and Sunday is 7"""
return self.datetime().isoweekday()
@property
def hour(self):
return self.datetime().hour
@property
def minute(self):
return self.datetime().minute
@property
def second(self):
return self.datetime().second
@property
def microsecond(self):
return self.datetime().microsecond
@property
def epoch(self):
return int(self._epoch)
# Human Slang Extras
# ------------------
def slang_date(self):
""""Returns human slang representation of date."""
dt = self.datetime(naive=True, to_timezone=self.local_timezone)
return humanize.naturaldate(dt)
def slang_time(self):
""""Returns human slang representation of time."""
dt = self.datetime(naive=True, to_timezone=self.local_timezone)
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():
"""Returns a MayaDT instance for this exact moment."""
epoch = time.time()
return MayaDT(epoch=epoch)
def when(string, timezone='UTC'):
""""Returns a MayaDT instance for the human moment specified.
Powered by dateparser. Useful for scraping websites.
Examples:
'next week', 'now', 'tomorrow', '300 years ago', 'August 14, 2015'
Keyword Arguments:
string -- string to be parsed
timezone -- timezone referenced from (default: '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.
Powered by pendulum. Accepts most known formats. Useful for working with data.
Keyword Arguments:
string -- string to be parsed
day_first -- if true, the first value (e.g. 01/05/2016) is parsed as day (default: 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)
+3 -3
View File
@@ -35,13 +35,13 @@ 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'],
packages=['maya'],
install_requires=required,
license='MIT',
classifiers=(
+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 maya.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