Compare commits

..

1 Commits

Author SHA1 Message Date
kennethreitz 52ca9a1cc4 Update README.rst 2017-05-29 14:41:27 -04:00
15 changed files with 517 additions and 814 deletions
-1
View File
@@ -24,7 +24,6 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
Pipfile.lock
# PyInstaller
# Usually these files are written by a python script from a template
+1 -10
View File
@@ -2,17 +2,8 @@ language: python
python:
- "2.7"
- "3.6"
- "3.7-dev"
matrix:
allow_failures:
- python: "3.7-dev"
# command to install dependencies
install:
- pip install pipenv
- pipenv install '-e .' --skip-lock --ignore-pipfile
- pipenv install --dev --skip-lock
install: pip install pipenv; pipenv lock; pipenv install --dev
# command to run tests
script: pipenv run pytest tests/
-2
View File
@@ -22,5 +22,3 @@ In chronological order:
- Joshua Li <joshua.r.li.98@gmail.com> (`@JoshuaRLi <https://github.com/JoshuaRLi>`_)
- Sébastien Eustace <sebastien@eustace.io> (`@sdispater <https://github.com/sdispater>`_)
- Evan Mattiza <emattiza@gmail.com> (`@emattiza <https://github.com/emattiza>`_)
- Dima Spivak <dima@spivak.ch> (`@dimaspivak <https://github.com/dimaspivak>`_)
- Tom Barron <tusculum@gmail.com> (`@dtbarron <https://github.com/tbarron>`_)
-2
View File
@@ -1,5 +1,3 @@
.PHONY: tests docs
tests:
pytest tests/
docs:
+9 -2
View File
@@ -1,4 +1,11 @@
[dev-packages]
freezegun = "*"
pytest = "*"
sphinx = "*"
Sphinx = "*"
[packages]
humanize = "*"
pytz = "*"
dateparser = "*"
"ruamel.yaml" = "*"
tzlocal = "*"
pendulum = ">=1.0"
Generated
+72
View File
@@ -0,0 +1,72 @@
{
"_meta": {
"hash": {
"sha256": "5617ff73ba51e60721267b24dc01e83f33d2a3692870b60e394b0f75ed2dc313"
},
"requires": {},
"sources": [
{
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"dateparser": {
"version": "==0.6.0"
},
"humanize": {
"version": "==0.5.1"
},
"pendulum": {
"version": "==1.2.0"
},
"python-dateutil": {
"version": "==2.6.0"
},
"pytz": {
"version": "==2017.2"
},
"pytzdata": {
"version": "==2017.2"
},
"regex": {
"version": "==2017.04.29"
},
"ruamel.ordereddict": {
"version": "==0.4.9"
},
"ruamel.yaml": {
"version": "==0.14.12"
},
"six": {
"version": "==1.10.0"
},
"tzlocal": {
"version": "==1.4"
}
},
"develop": {
"appdirs": {
"version": "==1.4.3"
},
"packaging": {
"version": "==16.8"
},
"py": {
"version": "==1.4.33"
},
"pyparsing": {
"version": "==2.2.0"
},
"pytest": {
"version": "==3.0.7"
},
"setuptools": {
"version": "==35.0.2"
},
"six": {
"version": "==1.10.0"
}
}
}
+11 -30
View File
@@ -10,6 +10,7 @@ Maya: Datetimes for Humans™
.. image:: https://img.shields.io/badge/SayThanks-!-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz
test
Datetimes are very frustrating to work with in Python, especially when dealing
with different locales on different systems. This library exists to make the
@@ -20,12 +21,6 @@ Datetimes should be interacted with via an API written for humans.
Maya is mostly built around the headaches and use-cases around parsing datetime data from websites.
.. image:: https://farm4.staticflickr.com/3702/33288285996_5b69d2b8f7_k_d.jpg
Art by `Sam Flores
<https://www.instagram.com/samagram12/>`_ (Photo by `Kenneth Reitz
<https://www.instagram.com/kennethreitz/>`_).
☤ Basic Usage of Maya
---------------------
@@ -69,15 +64,6 @@ Behold, datetimes for humans!
>>> rand_day = maya.when('2011-02-07', timezone='US/Eastern')
<MayaDT epoch=1297036800.0>
# Maya speaks Python.
>>> m = maya.MayaDT.from_datetime(datetime.utcnow())
>>> print(m)
Wed, 20 Sep 2017 17:24:32 GMT
>>> m = maya.MayaDT.from_struct(time.gmtime())
>>> print(m)
Wed, 20 Sep 2017 17:24:32 GMT
>>> rand_day.day
7
@@ -89,18 +75,13 @@ Behold, datetimes for humans!
UTC
# Range of hours in a day:
>>> maya.intervals(start=maya.now(), end=maya.now().add(days=1), interval=60*60)
>>> maya.interval(start=maya.now(), end=maya.now().add(days=1), interval=60*60)
<generator object intervals at 0x105ba5820>
# snap modifiers
>>> dt = maya.when('Mon, 21 Feb 1994 21:21:42 GMT')
>>> dt.snap('@d+3h').rfc2822()
'Mon, 21 Feb 1994 03:00:00 GMT'
☤ Advanced Usage of Maya
------------------------
In addition to timestamps, Maya also includes a wonderfully 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 finesse and ease.
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:
@@ -108,15 +89,15 @@ For example:
>>> from maya import MayaInterval
# Create an event that is one hour long, starting now.
# 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 are a number of methods available to you, which you can use to compare this event to another event.
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?
---------------------
@@ -133,7 +114,7 @@ From here, there are a number of methods available to you, which you can use to
☤ What about Delorean, Arrow, & Pendulum?
-----------------------------------------
All these project complement each other, and are friends. Pendulum, for example, helps power Maya's parsing.
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.
@@ -145,9 +126,9 @@ I think these projects complement each-other, personally. Maya is great for pars
☤ Installing Maya
-----------------
Installation is easy, with `pipenv <http://pipenv.org/>`_::
Installation is easy, with pip::
$ pipenv install maya
$ pip install maya
✨🍰✨
+1 -1
View File
@@ -1 +1 @@
__version__ = '0.4.2'
__version__ = '0.3.2'
+9 -11
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
"""
maya.compat
~~~~~~~~~~~~~~~
@@ -11,19 +12,24 @@ import sys
# -------
# Pythons
# -------
# Syntax sugar.
_ver = sys.version_info
# : Python 2.x?
#: Python 2.x?
is_py2 = (_ver[0] == 2)
# : Python 3.x?
#: Python 3.x?
is_py3 = (_ver[0] == 3)
# ---------
# Specifics
# ---------
if is_py2:
cmp = cmp
elif is_py3:
elif is_py3:
def cmp(a, b):
"""
Compare two objects.
@@ -32,10 +38,8 @@ elif is_py3:
"""
if a < b:
return -1
elif a == b:
return 0
else:
return 1
@@ -55,42 +59,36 @@ def comparable(klass):
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__
+116 -258
View File
@@ -1,22 +1,28 @@
# ___ __ ___ _ _ ___
# || \/ | ||=|| \\// ||=||
# || | || || // || ||
# Ignore warnings for yaml usage.
import warnings
import ruamel.yaml
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)
import email.utils
import time
import functools
from datetime import timedelta, datetime as Datetime
import re
import functools
import pytz
import humanize
import dateparser
import pendulum
import snaptime
from tzlocal import get_localzone
from dateutil.relativedelta import relativedelta
from .compat import cmp, comparable
_EPOCH_START = (1970, 1, 1)
def validate_class_type_arguments(operator):
"""
@@ -25,20 +31,14 @@ def validate_class_type_arguments(operator):
"""
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__
)
)
raise TypeError('unorderable types: {}() {} {}()'.format(
type(self).__name__, operator, type(arg).__name__))
return function(self, *args, **kwargs)
return wrapper
return inner
@@ -51,34 +51,26 @@ def validate_arguments_type_of_function(param_type=None):
Note: Use this decorator on the functions of the class.
"""
def inner(function):
def wrapper(self, *args, **kwargs):
type_ = param_type or type(self)
for arg in args + tuple(kwargs.values()):
if not isinstance(arg, type_):
raise TypeError(
(
'Invalid Type: {}.{}() accepts only the '
'arguments of type "<{}>"'
).format(
type(self).__name__,
function.__name__,
type_.__name__,
)
)
raise TypeError(('Invalid Type: {}.{}() accepts only the '
'arguments of type "<{}>"').format(
type(self).__name__,
function.__name__,
type_.__name__,
)
)
return function(self, *args, **kwargs)
return wrapper
return inner
class MayaDT(object):
"""The Maya Datetime object."""
__EPOCH_START = (1970, 1, 1)
def __init__(self, epoch):
super(MayaDT, self).__init__()
@@ -122,49 +114,26 @@ class MayaDT(object):
return hash(int(self.epoch))
def __add__(self, duration):
return self.add(
seconds=_seconds_or_timedelta(duration).total_seconds()
)
return self.add(seconds=seconds_or_timedelta(duration).total_seconds())
def __radd__(self, duration):
return self + duration
def __sub__(self, duration_or_date):
if isinstance(duration_or_date, MayaDT):
return self.subtract_date(dt=duration_or_date)
else:
return self.subtract(
seconds=_seconds_or_timedelta(duration_or_date).total_seconds()
)
def __sub__(self, duration):
return self.subtract(
seconds=seconds_or_timedelta(duration).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)
)
""""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)
)
def subtract_date(self, **kwargs):
"""Returns a timedelta object with the duration between the dates"""
return timedelta(seconds=self.epoch - kwargs['dt'].epoch)
def snap(self, instruction):
"""
Returns a new MayaDT object modified by the given instruction.
Powered by snaptime. See https://github.com/zartstrom/snaptime
for a complete documentation about the snaptime instructions.
"""
return self.from_datetime(snaptime.snap(self.datetime(), instruction))
""""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."""
@@ -177,11 +146,8 @@ class MayaDT(object):
@property
def local_timezone(self):
"""Returns the name of the local timezone."""
if self._local_tz.zone in pytz.all_timezones:
return self._local_tz.zone
return self.timezone
"""Returns the name of the local timezone, for informational purposes."""
return self._local_tz.zone
@property
def _local_tz(self):
@@ -192,31 +158,23 @@ class MayaDT(object):
@validate_arguments_type_of_function(Datetime)
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(*MayaDT.__EPOCH_START, tzinfo=pytz.timezone('UTC'))
epoch_start = Datetime(*_EPOCH_START, tzinfo=pytz.timezone('UTC'))
return (dt - epoch_start).total_seconds()
# Importers
# ---------
@classmethod
@validate_arguments_type_of_function(Datetime)
def from_datetime(klass, dt):
"""Returns MayaDT instance from datetime."""
return klass(klass.__dt_to_epoch(dt))
@classmethod
@validate_arguments_type_of_function(time.struct_time)
def from_struct(klass, struct, timezone=pytz.UTC):
"""Returns MayaDT instance from a 9-tuple struct
It's assumed to be from gmtime().
"""
struct_time = time.mktime(struct) - utc_offset(struct)
dt = Datetime.fromtimestamp(struct_time, timezone)
return klass(klass.__dt_to_epoch(dt))
@classmethod
def from_iso8601(klass, iso8601_string):
"""Returns MayaDT instance from iso8601 string."""
@@ -234,37 +192,30 @@ class MayaDT(object):
# Exporters
# ---------
def datetime(self, to_timezone=None, naive=False):
"""Returns a timezone-aware datetime...
Defaulting to UTC (as it should).
Keyword Arguments:
to_timezone {str} -- timezone to convert to (default: None/UTC)
naive {bool} -- if True,
the tzinfo is simply dropped (default: False)
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 local_datetime(self):
"""Returns a local timezone-aware datetime object
It's the same as:
mayaDt.datetime(to_timezone=mayaDt.local_timezone)
"""
return self.datetime(to_timezone=self.local_timezone, naive=False)
def iso8601(self):
"""Returns an ISO 8601 representation of the MayaDT."""
# Get a timezone-naive datetime.
@@ -281,6 +232,7 @@ class MayaDT(object):
# Properties
# ----------
@property
def year(self):
return self.datetime().year
@@ -293,20 +245,13 @@ class MayaDT(object):
def day(self):
return self.datetime().day
@property
def date(self):
return self.datetime().date()
@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 the day of the week as an integer. Monday is 1 and Sunday is 7"""
return self.datetime().isoweekday()
@property
@@ -331,6 +276,7 @@ class MayaDT(object):
# Human Slang Extras
# ------------------
def slang_date(self):
""""Returns human slang representation of date."""
dt = self.datetime(naive=True, to_timezone=self.local_timezone)
@@ -342,38 +288,15 @@ class MayaDT(object):
return humanize.naturaltime(dt)
def utc_offset(time_struct=None):
"""
Returns the time offset from UTC accounting for DST
Keyword Arguments:
time_struct {time.struct_time} -- the struct time for which to
return the UTC offset.
If None, use current local time.
"""
if time_struct:
ts = time_struct
else:
ts = time.localtime()
if ts[-1]:
offset = time.altzone
else:
offset = time.timezone
return offset
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)
@@ -382,46 +305,40 @@ def to_iso8601(dt):
def end_of_day_midnight(dt):
if dt.time() == time.min:
return dt
else:
return (
dt.replace(hour=0, minute=0, second=0, microsecond=0) +
timedelta(days=1)
)
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.
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 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'
)
'Exactly 2 of start, end, and duration must be specified')
# Convert duration to timedelta if seconds were provided.
if duration:
duration = _seconds_or_timedelta(duration)
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')
@@ -429,56 +346,28 @@ class MayaInterval(object):
self.end = end
def __repr__(self):
return '<MayaInterval start={0!r} end={1!r}>'.format(
self.start, self.end
)
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.iso8601(), self.end.iso8601())
@classmethod
def parse_iso8601_duration(cls, duration, start=None, end=None):
match = re.match(
r'(?:P(?P<weeks>\d+)W)|(?:P(?:(?:(?P<years>\d+)Y)?(?:(?P<months>\d+)M)?(?:(?P<days>\d+)D))?(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?)',
duration
)
time_components = {}
if match:
time_components = match.groupdict(0)
for key, value in time_components.items():
time_components[key] = int(value)
duration = relativedelta(**time_components)
if start:
return parse(start.datetime() + duration)
if end:
return parse(end.datetime() - duration)
return None
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, end=end)
raise NotImplementedError()
try:
end = parse(end)
except pendulum.parsing.exceptions.ParserError as e:
end = cls.parse_iso8601_duration(end, start=start)
return cls(start=start, end=end)
# 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()
@validate_arguments_type_of_function()
def __and__(self, maya_interval):
@@ -491,7 +380,8 @@ class MayaInterval(object):
@validate_arguments_type_of_function()
def __eq__(self, maya_interval):
return (
self.start == maya_interval.start and self.end == maya_interval.end
self.start == maya_interval.start and
self.end == maya_interval.end
)
def __hash__(self):
@@ -504,8 +394,8 @@ class MayaInterval(object):
@validate_arguments_type_of_function()
def __cmp__(self, maya_interval):
return (
cmp(self.start, maya_interval.start)
or cmp(self.end, maya_interval.end)
cmp(self.start, maya_interval.start) or
cmp(self.end, maya_interval.end)
)
@property
@@ -536,9 +426,8 @@ class MayaInterval(object):
MayaInterval(
interval_list[0].start,
max(interval_list[0].end, interval_list[1].end),
)
),
]
return interval_list
@validate_arguments_type_of_function()
@@ -546,7 +435,6 @@ class MayaInterval(object):
""""Removes the given interval."""
if not self & maya_interval:
return [self]
elif maya_interval.contains(self):
return []
@@ -558,8 +446,10 @@ class MayaInterval(object):
return interval_list
def split(self, duration, include_remainder=True):
# Convert seconds to timedelta, if appropriate.
duration = _seconds_or_timedelta(duration)
duration = seconds_or_timedelta(duration)
if duration <= timedelta(seconds=0):
raise ValueError('cannot call split with a non-positive timedelta')
@@ -567,36 +457,38 @@ class MayaInterval(object):
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)
duration = seconds_or_timedelta(duration)
timezone = pytz.timezone(timezone)
if duration <= timedelta(seconds=0):
raise ValueError('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()
)
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),
@@ -605,17 +497,23 @@ class MayaInterval(object):
@validate_arguments_type_of_function()
def intersection(self, maya_interval):
"""Returns the intersection between two intervals."""
start = max(self.start, maya_interval.start)
end = min(self.end, maya_interval.end)
either_instant = self.is_instant or maya_interval.is_instant
instant_overlap = (self.start == maya_interval.start or start <= end)
instant_overlap = (
self.start == maya_interval.start or
start <= end
)
if (either_instant and instant_overlap) or (start < end):
return MayaInterval(start, end)
@validate_arguments_type_of_function()
def contains(self, maya_interval):
return (
self.start <= maya_interval.start and self.end >= maya_interval.end
self.start <= maya_interval.start and
self.end >= maya_interval.end
)
def __contains__(self, maya_dt):
@@ -630,7 +528,8 @@ class MayaInterval(object):
@validate_arguments_type_of_function()
def is_adjacent(self, maya_interval):
return (
self.start == maya_interval.end or self.end == maya_interval.start
self.start == maya_interval.end or
self.end == maya_interval.start
)
@property
@@ -647,28 +546,14 @@ class MayaInterval(object):
""".format(
self.start.datetime().strftime(ical_dt_format),
self.end.datetime().strftime(ical_dt_format),
).replace(
' ', ''
).strip(
'\r\n'
).replace(
'\n', '\r\n'
)
).replace(' ', '').strip('\r\n').replace('\n', '\r\n')
@staticmethod
def flatten(interval_list):
return functools.reduce(
lambda reduced,
maya_interval: (
(
reduced[:-1] + maya_interval.combine(reduced[-1])
) if reduced else [
maya_interval
]
),
sorted(interval_list),
[],
)
return functools.reduce(lambda reduced, maya_interval: (
(reduced[:-1] + maya_interval.combine(reduced[-1]))
if reduced else [maya_interval]
), sorted(interval_list), [])
@classmethod
def from_datetime(cls, start_dt=None, end_dt=None, duration=None):
@@ -683,7 +568,7 @@ def now():
return MayaDT(epoch=epoch)
def when(string, timezone='UTC', prefer_dates_from='current_period'):
def when(string, timezone='UTC'):
""""Returns a MayaDT instance for the human moment specified.
Powered by dateparser. Useful for scraping websites.
@@ -694,53 +579,31 @@ def when(string, timezone='UTC', prefer_dates_from='current_period'):
Keyword Arguments:
string -- string to be parsed
timezone -- timezone referenced from (default: 'UTC')
prefer_dates_from -- what dates are prefered when `string` is ambigous.
options are 'past', 'future', and 'current_period'
(default: 'current_period'). see: [1]
Reference:
[1] dateparser.readthedocs.io/en/latest/usage.html#handling-incomplete-dates
"""
settings = {
'TIMEZONE': timezone,
'RETURN_AS_TIMEZONE_AWARE': True,
'TO_TIMEZONE': 'UTC',
'PREFER_DATES_FROM': prefer_dates_from,
}
dt = dateparser.parse(string,
settings={'TIMEZONE': timezone, 'RETURN_AS_TIMEZONE_AWARE': True, 'TO_TIMEZONE': 'UTC'})
dt = dateparser.parse(string, settings=settings)
if dt is None:
raise ValueError('invalid datetime input specified.')
return MayaDT.from_datetime(dt)
def parse(string, timezone='UTC', day_first=False, year_first=True):
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.
Powered by pendulum. Accepts most known formats. Useful for working with data.
Keyword Arguments:
string -- string to be parsed
timezone -- timezone referenced from (default: 'UTC')
day_first -- if true, the first value (e.g. 01/05/2016)
is parsed as day.
if year_first is set to True, this distinguishes
between YDM and YMD. (default: False)
year_first -- if true, the first value (e.g. 2016/05/01)
is parsed as year (default: True)
day_first -- if true, the first value (e.g. 01/05/2016) is parsed as day (default: False)
"""
options = {}
options['tz'] = timezone
options['day_first'] = day_first
options['year_first'] = year_first
dt = pendulum.parse(str(string), **options)
dt = pendulum.parse(string, day_first=day_first)
return MayaDT.from_datetime(dt)
def _seconds_or_timedelta(duration):
def seconds_or_timedelta(duration):
"""Returns `datetime.timedelta` object for the passed duration.
Keyword Arguments:
@@ -751,22 +614,17 @@ def _seconds_or_timedelta(duration):
elif isinstance(duration, timedelta):
dt_timedelta = duration
else:
raise TypeError(
'Expects argument as `datetime.timedelta` object '
'or seconds in `int` format'
)
raise TypeError('Expects argument as `datetime.timedelta` object '
'or seconds in `int` format')
return dt_timedelta
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)
"""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)
+12 -48
View File
@@ -1,16 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import io
import os
import sys
import codecs
from shutil import rmtree
from setuptools import find_packages, setup, Command
from setuptools import setup
try:
# Python 3
from os import dirname
except ImportError:
# Python 2
from os.path import dirname
here = os.path.abspath(os.path.dirname(__file__))
here = os.path.abspath(dirname(__file__))
with codecs.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = '\n' + f.read()
@@ -23,56 +27,19 @@ if sys.argv[-1] == "publish":
required = [
'humanize',
'pytz',
'dateparser>=0.7.0',
'dateparser',
'ruamel.yaml',
'tzlocal',
'pendulum>=1.0, <=1.5.1',
'snaptime'
'pendulum'
]
packages = [
'maya',
]
class UploadCommand(Command):
"""Support setup.py upload."""
description = 'Build and publish the package.'
user_options = []
@staticmethod
def status(s):
"""Prints things in bold."""
print('\033[1m{0}\033[0m'.format(s))
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
self.status('Removing previous builds…')
rmtree(os.path.join(here, 'dist'))
except OSError:
pass
self.status('Building Source and Wheel (universal) distribution…')
os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable))
self.status('Uploading the package to PyPi via Twine…')
os.system('twine upload dist/*')
self.status('Pushing git tags…')
os.system('git tag v{0}'.format(about['__version__']))
os.system('git push --tags')
sys.exit()
# About dict to store version and package info
about = dict()
with codecs.open(os.path.join(here, 'maya', '__version__.py'), 'r', encoding='utf-8') as f:
with open(os.path.join(here, 'maya', '__version__.py'), 'r', 'utf-8') as f:
exec(f.read(), about)
setup(
@@ -100,7 +67,4 @@ setup(
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: Software Development :: Libraries :: Python Modules'
),
cmdclass={
'upload': UploadCommand,
},
)
-24
View File
@@ -1,24 +0,0 @@
from freezegun import freeze_time
import pytest
@pytest.fixture(params=[
("2018-03-25T00:00:00", 2),
("2018-03-25T01:00:00", 2),
("2018-03-25T02:00:00", 2),
("2018-03-25T02:30:00", 2),
("2018-03-25T03:00:00", 2),
("2018-03-25T04:00:00", 2),
("2018-10-28T00:00:00", 2),
("2018-10-28T01:00:00", 2),
("2018-10-28T02:00:00", 2),
("2018-10-28T02:30:00", 2),
("2018-10-28T03:00:00", 2),
("2018-10-28T04:00:00", 2),
], ids=lambda x: x[0] + "_off_" + str(x[1]))
def frozen_now(request):
now_string, tz_offset = request.param
with freeze_time(now_string, tz_offset=tz_offset):
yield
+72 -209
View File
@@ -1,121 +1,100 @@
import copy
import time
import calendar
from datetime import timedelta, datetime as Datetime
import pytz
import pytest
import copy
from datetime import timedelta
import maya
from maya.core import _seconds_or_timedelta # import private function
@pytest.mark.parametrize("string,expected", [
('February 21, 1994',
'Mon, 21 Feb 1994 00:00:00 GMT'),
])
def test_rfc2822(string, expected):
r = maya.parse(string).rfc2822()
def test_rfc2822():
r = maya.parse('February 21, 1994').rfc2822()
d = maya.MayaDT.from_rfc2822(r)
assert r == expected
assert r == 'Mon, 21 Feb 1994 00:00:00 GMT'
assert r == d.rfc2822()
@pytest.mark.parametrize("string,expected", [
('February 21, 1994',
'1994-02-21T00:00:00Z'),
])
def test_iso8601(string, expected):
r = maya.parse(string).iso8601()
def test_iso8601():
r = maya.parse('February 21, 1994').iso8601()
d = maya.MayaDT.from_iso8601(r)
assert r == expected
assert r == '1994-02-21T00:00:00Z'
assert r == d.iso8601()
@pytest.mark.parametrize("string,expected", [
('20161001T1430.4+05:30',
'2016-10-01T09:00:00.400000Z'),
('2016T14',
'2016-01-01T14:00:00Z'),
('2016-10T14',
'2016-10-01T14:00:00Z'),
('2012W05',
'2012-01-30T00:00:00Z'),
('2012W055',
'2012-02-03T00:00:00Z'),
('2012007',
'2012-01-07T00:00:00Z'),
('2016-W07T09',
'2016-02-15T09:00:00Z'),
])
def test_parse_iso8601(string, expected):
def test_parse_iso8601():
string = '20161001T1430.4+05:30'
expected = '2016-10-01T09:00:00.400000Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2016T14'
expected = '2016-01-01T14:00:00Z'
d = maya.MayaDT.from_iso8601(string)
@pytest.mark.usefixtures("frozen_now")
def test_struct():
now = round(time.time())
ts = time.gmtime(now)
m = maya.MayaDT.from_struct(ts)
dt = Datetime.fromtimestamp(now, pytz.UTC)
assert m._epoch is not None
assert m.datetime() == dt
ts = time.localtime(now)
m = maya.MayaDT.from_struct(ts)
dt = Datetime.fromtimestamp(
time.mktime(ts) - maya.core.utc_offset(ts), pytz.UTC
)
assert m._epoch is not None
assert m.datetime() == dt
assert expected == d.iso8601()
string = '2016-10T14'
expected = '2016-10-01T14:00:00Z'
d = maya.MayaDT.from_iso8601(string)
def test_issue_104():
e = 1507756331
t = Datetime.utcfromtimestamp(e)
t = maya.MayaDT.from_datetime(t)
assert str(t) == 'Wed, 11 Oct 2017 21:12:11 GMT'
t = time.gmtime(e)
t = maya.MayaDT.from_struct(t)
assert str(t) == 'Wed, 11 Oct 2017 21:12:11 GMT'
assert expected == d.iso8601()
string = '2012W05'
expected = '2012-01-30T00:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2012W055'
expected = '2012-02-03T00:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2012007'
expected = '2012-01-07T00:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2016-W07T09'
expected = '2016-02-15T09:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
def test_human_when():
r1 = maya.when('yesterday')
r2 = maya.when('today')
assert (r2.day - r1.day) in (1, -30, -29, -28, -27)
def test_machine_parse():
r1 = maya.parse('August 14, 2015')
assert r1.day == 14
r2 = maya.parse('August 15, 2015')
assert r2.day == 15
@pytest.mark.usefixtures("frozen_now")
def test_dt_tz_translation():
d1 = maya.now().datetime()
d2 = maya.now().datetime(to_timezone='EST')
assert (d1.hour - d2.hour) % 24 == 5
@pytest.mark.usefixtures("frozen_now")
def test_dt_tz_naive():
d1 = maya.now().datetime(naive=True)
assert d1.tzinfo is None
d2 = maya.now().datetime(to_timezone='EST', naive=True)
assert d2.tzinfo is None
assert (d1.hour - d2.hour) % 24 == 5
def test_random_date():
# Test properties for maya.when()
d1 = maya.when('11-17-11 08:09:10')
assert d1.year == 2011
@@ -127,6 +106,7 @@ def test_random_date():
assert d1.minute == 9
assert d1.second == 10
assert d1.microsecond == 0
# Test properties for maya.parse()
d2 = maya.parse('February 29, 1992 13:12:34')
assert d2.year == 1992
@@ -142,8 +122,10 @@ def test_random_date():
def test_print_date(capsys):
d = maya.when('11-17-11')
print(d)
out, err = capsys.readouterr()
assert out == 'Thu, 17 Nov 2011 00:00:00 GMT\n'
assert repr(d) == '<MayaDT epoch=1321488000.0>'
@@ -159,110 +141,55 @@ def test_slang_date():
def test_slang_time():
d = maya.when('1 hour ago')
d = maya.when('one hour ago')
assert d.slang_time() == 'an hour ago'
@pytest.mark.parametrize("string,kwds,expected", [
('February 21, 1994', {},
'1994-02-21 00:00:00+00:00'),
def test_parse():
d = maya.parse('February 21, 1994')
assert format(d) == '1994-02-21 00:00:00+00:00'
('01/05/2016', {},
'2016-01-05 00:00:00+00:00'),
d = maya.parse('01/05/2016')
assert format(d) == '2016-01-05 00:00:00+00:00'
('01/05/2016', dict(day_first=True),
'2016-05-01 00:00:00+00:00'),
('2016/05/01', dict(year_first=True, day_first=False),
'2016-05-01 00:00:00+00:00'),
('2016/01/05', dict(year_first=True, day_first=True),
'2016-05-01 00:00:00+00:00'),
('01/05/2016', dict(timezone='UTC'),
'2016-01-05 00:00:00+00:00'),
('01/05/2016', dict(timezone='US/Central'),
'2016-01-05 06:00:00+00:00'),
])
def test_parse(string, kwds, expected):
d = maya.parse(string, **kwds)
assert format(d) == expected
@pytest.mark.usefixtures("frozen_now")
def test_when_past():
two_days_away = maya.now().add(days=2)
past_date = maya.when(
two_days_away.slang_date(),
prefer_dates_from='past')
assert past_date < maya.now()
@pytest.mark.usefixtures("frozen_now")
def test_when_future():
two_days_away = maya.now().add(days=2)
future_date = maya.when(
two_days_away.slang_date(),
prefer_dates_from='future')
assert future_date > maya.now()
@pytest.mark.usefixtures("frozen_now")
def test_when_past_day_name():
two_days_away = maya.now().add(days=2)
past_date = maya.when(
calendar.day_name[two_days_away.weekday],
prefer_dates_from='past')
assert past_date < maya.now()
@pytest.mark.usefixtures("frozen_now")
def test_when_future_day_name():
two_days_away = maya.now().add(days=2)
future_date = maya.when(
calendar.day_name[two_days_away.weekday],
prefer_dates_from='future')
assert future_date > maya.now()
d = maya.parse('01/05/2016', day_first=True)
assert format(d) == '2016-05-01 00:00:00+00:00'
def test_datetime_to_timezone():
dt = maya.when('2016-01-01').datetime(to_timezone='US/Eastern')
assert dt.tzinfo.zone == 'US/Eastern'
def test_rfc3339():
mdt = maya.when('2016-01-01')
mdt = maya.when('2016-01-01')
out = mdt.rfc3339()
mdt2 = maya.MayaDT.from_rfc3339(out)
assert mdt.epoch == mdt2.epoch
@pytest.mark.usefixtures("frozen_now")
def test_comparison_operations():
now = maya.now()
now_copy = copy.deepcopy(now)
tomorrow = maya.when('tomorrow')
assert (now == now_copy) is True
assert (now == tomorrow) is False
assert (now != now_copy) is False
assert (now != tomorrow) is True
assert (now < now_copy) is False
assert (now < tomorrow) is True
assert (now <= now_copy) is True
assert (now <= tomorrow) is True
assert (now > now_copy) is False
assert (now > tomorrow) is False
assert (now >= now_copy) is True
assert (now >= tomorrow) is False
# Check Exceptions
with pytest.raises(TypeError):
now == 1
@@ -280,98 +207,34 @@ def test_comparison_operations():
def test_seconds_or_timedelta():
# test for value in seconds
assert _seconds_or_timedelta(1234) == timedelta(0, 1234)
assert maya.seconds_or_timedelta(1234) == timedelta(0, 1234)
# test for value as `datetime.timedelta`
assert _seconds_or_timedelta(timedelta(0, 1234)) == timedelta(0, 1234)
assert maya.seconds_or_timedelta(timedelta(0, 1234)) == timedelta(0, 1234)
# test for invalid value
with pytest.raises(TypeError):
_seconds_or_timedelta('invalid interval')
maya.seconds_or_timedelta('invalid interval')
@pytest.mark.usefixtures("frozen_now")
def test_intervals():
now = maya.now()
tomorrow = now.add(days=1)
assert len(list(maya.intervals(now, tomorrow, 60 * 60))) == 24
assert len(list(maya.intervals(now, tomorrow, 60*60))) == 24
@pytest.mark.usefixtures("frozen_now")
def test_dunder_add():
now = maya.now()
assert now + 1 == now.add(seconds=1)
assert now + timedelta(seconds=1) == now.add(seconds=1)
@pytest.mark.usefixtures("frozen_now")
def test_dunder_radd():
now = maya.now()
assert now.add(seconds=1) == now + 1
assert now.add(seconds=1) == now + timedelta(seconds=1)
@pytest.mark.usefixtures("frozen_now")
def test_dunder_sub():
now = maya.now()
assert now - 1 == now.subtract(seconds=1)
assert now - timedelta(seconds=1) == now.subtract(seconds=1)
@pytest.mark.usefixtures("frozen_now")
def test_mayaDT_sub():
now = maya.now()
then = now.add(days=1)
assert then - now == timedelta(seconds=24 * 60 * 60)
assert now - then == timedelta(seconds=-24 * 60 * 60)
def test_core_local_timezone(monkeypatch):
@property
def mock_local_tz(self):
class StaticTzInfo(object):
zone = 'local'
def __repr__(self):
return "<StaticTzInfo 'local'>"
return StaticTzInfo()
monkeypatch.setattr(maya.MayaDT, '_local_tz', mock_local_tz)
mdt = maya.MayaDT(0)
assert mdt.local_timezone == 'UTC'
def test_getting_datetime_for_local_timezone(monkeypatch):
@property
def mock_local_tz(self):
class StaticTzInfo(object):
zone = 'Europe/Zurich'
def __repr__(self):
return "<StaticTzInfo 'Europe/Zurich'>"
return StaticTzInfo()
monkeypatch.setattr(maya.MayaDT, '_local_tz', mock_local_tz)
d = maya.parse('1994-02-21T12:00:00+05:30')
dt = pytz.timezone('Europe/Zurich').localize(
Datetime(1994, 2, 21, 7, 30))
assert d.local_datetime() == dt
@pytest.mark.parametrize("when_str,snap_str,expected_when", [
('Mon, 21 Feb 1994 21:21:42 GMT', '@d',
'Mon, 21 Feb 1994 00:00:00 GMT'),
])
def test_snaptime(when_str, snap_str, expected_when):
# given
dt = maya.when(when_str)
# when
dt = dt.snap(snap_str)
# then
assert dt == maya.when(expected_when)
+214 -202
View File
@@ -15,20 +15,26 @@ 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)
@@ -58,9 +64,7 @@ def test_interval_init_end_duration():
assert interval.start == end.subtract(seconds=duration)
@pytest.mark.parametrize(
'start_doy1,end_doy1,start_doy2,end_doy2,intersection_doys',
(
@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),
@@ -72,8 +76,7 @@ def test_interval_init_end_duration():
(1, 3, 1, 1, (1, 1)),
(2, 3, 1, 1, None),
(1, 3, 2, 2, (2, 2)),
),
ids=(
), ids=(
'overlapping',
'non-overlapping',
'adjacent',
@@ -84,19 +87,21 @@ def test_interval_init_end_duration():
'instant overlapping',
'instant overlapping start only (left)',
'instant disjoint (left)',
'instant overlapping (left)',
),
)
'instant overlapping (left)'
))
def test_interval_intersection(
start_doy1, end_doy1, start_doy2, end_doy2, intersection_doys
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)
base.add(days=start_doy1),
base.add(days=end_doy1),
)
interval2 = maya.MayaInterval(
base.add(days=start_doy2), base.add(days=end_doy2)
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(
@@ -105,6 +110,7 @@ def test_interval_intersection(
)
else:
assert (interval1 & interval2) is None
# check invalid argument
with pytest.raises(TypeError):
interval1 & 'invalid type'
@@ -113,10 +119,13 @@ def test_interval_intersection(
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))
)
assert not interval.intersects(maya.MayaInterval(
base.add(days=2),
base.add(days=3),
))
# check invalid argument
with pytest.raises(TypeError):
interval.intersects('invalid type')
@@ -126,11 +135,13 @@ 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)
)
# check invalid argument
with pytest.raises(TypeError):
interval1.intersection('invalid type')
@@ -140,11 +151,14 @@ 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))
# check invalid argument
with pytest.raises(TypeError):
interval == 'invalid type'
with pytest.raises(TypeError):
interval != 'invalid type'
@@ -153,6 +167,7 @@ def test_interval_timedelta():
start = maya.now()
delta = timedelta(hours=1)
interval = maya.MayaInterval(start=start, duration=delta)
assert interval.timedelta == delta
@@ -160,55 +175,69 @@ 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',
(
@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'),
)
), ids=(
'overlapping',
'non-overlapping',
'adjacent',
'equal',
'subset',
))
def test_interval_contains(
start_doy1, end_doy1, start_doy2, end_doy2, expected
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)
base.add(days=start_doy1),
base.add(days=end_doy1),
)
interval2 = maya.MayaInterval(
base.add(days=start_doy2), base.add(days=end_doy2)
base.add(days=start_doy2),
base.add(days=end_doy2),
)
assert interval1.contains(interval2) is expected
assert (interval2 in interval1) is expected
# check invalid argument
with pytest.raises(TypeError):
interval1.contains('invalid type')
@pytest.mark.parametrize(
'start_doy,end_doy,dt_doy,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):
), 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)
start=base.add(days=start_doy),
end=base.add(days=end_doy),
)
dt = base.add(days=dt_doy)
assert (dt in interval) is expected
# check invalid argument
with pytest.raises(TypeError):
'invalid type' in interval
@@ -218,84 +247,91 @@ 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))
)
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=(
@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)
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2), end=base.add(days=end2)
start=base.add(days=start2),
end=base.add(days=end2),
)
assert cmp(interval1, interval2) == expected
# check invalid argument
with pytest.raises(TypeError):
cmp(interval1, 'invalid type')
@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'),
)
@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)
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2), end=base.add(days=end2)
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
]
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
# check invalid argument
with pytest.raises(TypeError):
interval2.combine('invalid type')
@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=(
@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',
@@ -304,109 +340,115 @@ def test_interval_combine(start1, end1, start2, end2, expected):
'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)
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2), end=base.add(days=end2)
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
]
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
# check invalid argument
with pytest.raises(TypeError):
interval1.subtract('invalid type')
@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'),
)
@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)
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2), end=base.add(days=end2)
start=base.add(days=start2),
end=base.add(days=end2),
)
assert interval1.is_adjacent(interval2) == expected
# check invalid argument
with pytest.raises(TypeError):
interval1.is_adjacent('invalid type')
@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=(
@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)
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
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)
)
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(ValueError):
list(interval.split(timedelta(seconds=0)))
with pytest.raises(ValueError):
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=(
@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',
@@ -417,20 +459,21 @@ def test_interval_split_non_positive_delta():
'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
):
))
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
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]),
@@ -441,6 +484,7 @@ def test_quantize_invalid_delta():
start = maya.now()
end = start.add(days=1)
interval = maya.MayaInterval(start=start, end=end)
with pytest.raises(ValueError):
interval.quantize(timedelta(minutes=0))
with pytest.raises(ValueError):
@@ -451,13 +495,12 @@ 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)
]
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)
@@ -465,35 +508,35 @@ 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)
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))
]
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)
]
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)
)
]
assert maya.MayaInterval.flatten(intervals) == [maya.MayaInterval(
start=base,
duration=timedelta(hours=max_hour, minutes=30),
)]
def test_interval_flatten_containing():
@@ -501,16 +544,16 @@ def test_interval_flatten_containing():
max_hour = 20
base = maya.now()
containing_interval = maya.MayaInterval(
start=base, end=base.add(hours=max_hour + step)
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 = [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]
@@ -518,55 +561,24 @@ 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)
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
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
end_dt=end.datetime(naive=False),
duration=duration,
)
assert interval3.start == start
assert interval3.end == end
def test_interval_iso8601():
start = maya.when('11-17-11 08:09:10')
interval = maya.MayaInterval(start=start, duration=1)
assert interval.iso8601() == '2011-11-17T08:09:10Z/2011-11-17T08:09:11Z'
def test_interval_from_iso8601():
interval = maya.MayaInterval.from_iso8601(
"2018-03-18T14:27:18Z/2018-04-01T04:15:27Z"
)
s = maya.when("2018-03-18T14:27:18Z")
e = maya.when("2018-04-01T04:15:27Z")
assert interval.start == s
assert interval.end == e
def test_interval_from_iso8601_duration():
interval = maya.MayaInterval.from_iso8601(
"2018-03-18T14:27:18Z/P13DT13H48M9S"
)
s = maya.when("2018-03-18T14:27:18Z")
e = maya.when("2018-04-01T04:15:27Z")
assert interval.start == s
assert interval.end == e
interval = maya.MayaInterval.from_iso8601(
"2018-03-05T14:27:18Z/P2W"
)
s = maya.when("2018-03-05T14:27:18Z")
e = maya.when("2018-03-19T14:27:18Z")
assert interval.start == s
assert interval.end == e
-14
View File
@@ -1,14 +0,0 @@
[tox]
envlist = py27, py34, py35, py36
[testenv]
deps = pipenv
commands=
pipenv install --dev --skip-lock
pytest tests/ {posargs}
[testenv:py27]
basepython = python2.7
deps =
{[testenv]deps}
funcsigs