Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit fdd3b5c4e4ebf083061c4759e38daf410a21d1c4 0 parents
@sfllaw sfllaw authored
7 .gitignore
@@ -0,0 +1,7 @@
+# Python
+*.py[co]
+
+# setup.py
+dist
+build
+MANIFEST
9 .hgignore
@@ -0,0 +1,9 @@
+syntax:glob
+
+# Python
+*.py[co]
+
+# setup.py
+dist
+build
+MANIFEST
28 LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2012, Ecometrica Canada Limited. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+- Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+- Neither the name of Ecometrica nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3  MANIFEST.in
@@ -0,0 +1,3 @@
+include LICENSE
+include run-tests.py
+include test/*.py
220 grandfatherson/__init__.py
@@ -0,0 +1,220 @@
+"""
+GrandFatherSon is a backup rotation calculator that implements the
+`grandfather-father-son rotation scheme
+<http://en.wikipedia.org/wiki/Backup_rotation_scheme#Grandfather-father-son_backup>`_.
+
+This is usually done by keeping a certain number of daily, weekly, and
+monthly backups. Older backups should be removed to reduce the amount
+of space used.
+
+
+Usage
+-----
+
+This module expects either ``datetime.date`` or ``datetime.datetime``
+objects as inputs. As an example, let's assume you have daily backups
+for the all of year 1999 that need rotating::
+
+ >>> import datetime
+ >>> start_date = datetime.date(1999, 1, 1)
+ >>> end_date = datetime.date(1999, 12, 31)
+ >>> backups = [start_date + datetime.timedelta(days=i)
+ ... for i in xrange((end_date - start_date).days + 1)]
+ >>> backups
+ [datetime.date(1999, 1, 1),
+ datetime.date(1999, 1, 2),
+ datetime.date(1999, 1, 3),
+ ...
+ datetime.date(1999, 12, 30),
+ datetime.date(1999, 12, 31)]
+
+Let's say that full backups are taken every Saturday, with incremental
+backups done daily. A week, or 7 days, of incremental backups should
+be kept. A months, or 4 weeks, of full backups are kept. In addition,
+for three months, the first full backup is kept for each month, with
+the others discarded.
+
+It's the last day of the year and you want to figure out which backups
+need to be pruned::
+
+ >>> now = datetime.date(1999, 12, 31)
+
+To see which files will be preserved, use the ``dates_to_keep``
+function::
+
+ >>> from grandfatherson import dates_to_keep, SATURDAY
+ >>> sorted(dates_to_keep(backups, days=7, weeks=4, months=3,
+ ... firstweekday=SATURDAY, now=now))
+ [datetime.date(1999, 10, 1),
+ datetime.date(1999, 11, 1),
+ datetime.date(1999, 12, 1),
+ datetime.date(1999, 12, 4),
+ datetime.date(1999, 12, 11),
+ datetime.date(1999, 12, 18),
+ datetime.date(1999, 12, 25),
+ datetime.date(1999, 12, 26),
+ datetime.date(1999, 12, 27),
+ datetime.date(1999, 12, 28),
+ datetime.date(1999, 12, 29),
+ datetime.date(1999, 12, 30),
+ datetime.date(1999, 12, 31)]
+
+If you leave off the ``now`` argument, it will default to using
+``datetime.datetime.now()``.
+
+To see which files should be deleted, use the ``dates_to_delete``
+function::
+
+ >>> from grandfatherson import dates_to_keep, SATURDAY
+ >>> sorted(dates_to_delete(backups, days=7, weeks=4, months=3,
+ ... firstweekday=SATURDAY, now=now))
+ [datetime.date(1999, 1, 1),
+ ...
+ datetime.date(1999, 9, 30),
+ datetime.date(1999, 10, 2),
+ ...
+ datetime.date(1999, 10, 31),
+ datetime.date(1999, 11, 2),
+ ...
+ datetime.date(1999, 11, 30),
+ datetime.date(1999, 12, 2),
+ datetime.date(1999, 12, 3),
+ datetime.date(1999, 12, 5),
+ ...
+ datetime.date(1999, 12, 10),
+ datetime.date(1999, 12, 12),
+ ...
+ datetime.date(1999, 12, 17),
+ datetime.date(1999, 12, 19),
+ ...
+ datetime.date(1999, 12, 24)]
+
+Finally, if you need to rotate backups that have timestamps in
+``datetime`` format, you can use the corresponding ``to_keep`` and
+``to_delete`` functions::
+
+ >>> now = datetime.datetime(1999, 12, 31, 23, 59, 59)
+ >>> start_datetime = datetime.datetime(1999, 12, 31, 0, 0, 0)
+ >>> end_datetime = datetime.datetime(1999, 12, 31, 23, 59, 59)
+ >>> backups = [start_datetime + datetime.timedelta(seconds=i)
+ ... for i
+ ... in xrange((end_datetime - start_datetime).seconds + 1)]
+ >>> backups
+ [datetime.datetime(1999, 12, 31, 0, 0),
+ datetime.datetime(1999, 12, 31, 0, 0, 1),
+ datetime.datetime(1999, 12, 31, 0, 0, 2),
+ ...
+ datetime.datetime(1999, 12, 31, 23, 59, 58),
+ datetime.datetime(1999, 12, 31, 23, 59, 59)]
+
+ >>> from grandfatherson import to_keep
+ >>> sorted(to_keep(backups, hours=2, minutes=10, seconds=10, now=now))
+ [datetime.datetime(1999, 12, 31, 22, 0),
+ datetime.datetime(1999, 12, 31, 23, 0),
+ datetime.datetime(1999, 12, 31, 23, 50),
+ ...
+ datetime.datetime(1999, 12, 31, 23, 59),
+ datetime.datetime(1999, 12, 31, 23, 59, 50),
+ ...
+ datetime.datetime(1999, 12, 31, 23, 59, 59)]
+
+ >>> from grandfatherson import to_delete
+ >>> sorted(to_delete(backups, hours=2, minutes=10, seconds=10, now=now))
+ [datetime.datetime(1999, 12, 31, 0, 0),
+ ...
+ datetime.datetime(1999, 12, 31, 21, 59, 59),
+ datetime.datetime(1999, 12, 31, 22, 0, 1),
+ ...
+ datetime.datetime(1999, 12, 31, 22, 59, 59),
+ datetime.datetime(1999, 12, 31, 23, 0, 1),
+ ...
+ datetime.datetime(1999, 12, 31, 23, 49, 59),
+ datetime.datetime(1999, 12, 31, 23, 50, 1),
+ ...
+ datetime.datetime(1999, 12, 31, 23, 58, 59),
+ datetime.datetime(1999, 12, 31, 23, 59, 1),
+ ...
+ datetime.datetime(1999, 12, 31, 23, 59, 49)]
+"""
+
+from calendar import (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY,
+ SUNDAY)
+from datetime import datetime, time
+import os
+
+from grandfatherson import filters
+
+
+def to_keep(datetimes,
+ years=0, months=0, weeks=0, days=0,
+ hours=0, minutes=0, seconds=0,
+ firstweekday=SATURDAY, now=None):
+ """
+ Return a set of datetimes that should be kept, out of ``datetimes``.
+
+ Keeps up to ``years``, ``months``, ``weeks``, ``days``,
+ ``hours``, ``minutes``, and ``seconds`` in the past.
+
+ When keeping weeks, it prefers to keep ``firstweekday``, which
+ defaults to Saturday.
+
+ If ``now`` is None, it will base its calculations on
+ ``datetime.datetime.now()``. Datetimes after this point will always be
+ kept.
+ """
+ datetimes = set(datetimes)
+ return (filters.Years.filter(datetimes, number=years, now=now) |
+ filters.Months.filter(datetimes, number=months, now=now) |
+ filters.Weeks.filter(datetimes, number=weeks,
+ firstweekday=firstweekday, now=now) |
+ filters.Days.filter(datetimes, number=days, now=now) |
+ filters.Hours.filter(datetimes, number=hours, now=now) |
+ filters.Minutes.filter(datetimes, number=minutes, now=now) |
+ filters.Seconds.filter(datetimes, number=seconds, now=now))
+
+
+def to_delete(datetimes,
+ years=0, months=0, weeks=0, days=0,
+ hours=0, minutes=0, seconds=0,
+ firstweekday=SATURDAY, now=None):
+ """
+ Return a set of datetimes that should be deleted, out of ``datetimes``.
+
+ See ``to_keep`` for a description of arguments.
+ """
+ datetimes = set(datetimes)
+ return datetimes - to_keep(datetimes,
+ years=years, months=months,
+ weeks=weeks, days=days,
+ hours=hours, minutes=minutes, seconds=seconds,
+ firstweekday=firstweekday, now=now)
+
+
+def dates_to_keep(dates,
+ years=0, months=0, weeks=0, days=0, firstweekday=SATURDAY,
+ now=None):
+ """
+ Return a set of dates that should be kept, out of ``dates``.
+
+ See ``to_keep`` for a description of arguments.
+ """
+ datetimes = to_keep((datetime.combine(d, time()) for d in dates),
+ years=years, months=months, weeks=weeks, days=days,
+ hours=0, minutes=0, seconds=0,
+ firstweekday=firstweekday, now=now)
+ return set(dt.date() for dt in datetimes)
+
+
+def dates_to_delete(dates,
+ years=0, months=0, weeks=0, days=0, firstweekday=SATURDAY,
+ now=None):
+ """
+ Return a set of date that should be deleted, out of ``dates``.
+
+ See ``to_keep`` for a description of arguments.
+ """
+ dates = set(dates)
+ return dates - dates_to_keep(dates,
+ years=years, months=months,
+ weeks=weeks, days=days,
+ firstweekday=firstweekday, now=now)
182 grandfatherson/filters.py
@@ -0,0 +1,182 @@
+"""
+Filters used by GrandFatherSon to decide which datetimes to keep.
+"""
+
+import calendar
+from datetime import date, datetime, time, timedelta
+
+
+class Filter(object):
+ """Base class."""
+
+ @classmethod
+ def mask(cls, dt, **options):
+ """
+ Return a datetime with the same value as ``dt``, keeping only
+ significant values.
+ """
+ raise NotImplemented
+
+ @classmethod
+ def start(cls, now, number, **options):
+ """
+ Return the starting datetime: ``number`` of units before ``now``.
+ """
+ return (cls.mask(now, **options) -
+ timedelta(**{cls.__name__.lower(): number - 1}))
+
+ @classmethod
+ def filter(cls, datetimes, number, now=None, **options):
+ """Return a set of datetimes, after filtering ``datetimes``.
+
+ The result will be the ``datetimes`` which are ``number`` of
+ units before ``now``, until ``now``, with approximately one
+ unit between each of them. The first datetime for any unit is
+ kept, later duplicates are removed.
+
+ If there are ``datetimes`` after ``now``, they will be
+ returned unfiltered.
+ """
+ if number < 0 or not isinstance(number, (int, long)):
+ raise ValueError('Invalid number: %s' % number)
+
+ if now is None:
+ now = datetime.now()
+ if not hasattr(now, 'second'):
+ # now looks like a date, so convert it into a datetime
+ now = datetime.combine(now, time(23, 59, 59, 999999))
+
+ # Always keep datetimes from the future
+ future = set(dt for dt in datetimes if dt > now)
+
+ if number == 0:
+ return future
+
+ # Don't consider datetimes from before the start
+ start = cls.start(now, number, **options)
+ valid = (dt for dt in datetimes if start <= dt <= now)
+
+ # Deduplicate datetimes with the same mask() value by keeping
+ # the oldest.
+ kept = {}
+ for dt in sorted(valid):
+ kept.setdefault(cls.mask(dt), dt)
+
+ return set(kept.values()) | future
+
+
+class Seconds(Filter):
+ @classmethod
+ def mask(cls, dt, **options):
+ """
+ Return a datetime with the same value as ``dt``, to a
+ resolution of seconds.
+ """
+ return dt.replace(microsecond=0)
+
+
+class Minutes(Filter):
+ @classmethod
+ def mask(cls, dt, **options):
+ """
+ Return a datetime with the same value as ``dt``, to a
+ resolution of minutes.
+ """
+ return dt.replace(second=0, microsecond=0)
+
+
+class Hours(Filter):
+ @classmethod
+ def mask(cls, dt, **options):
+ """
+ Return a datetime with the same value as ``dt``, to a
+ resolution of hours.
+ """
+ return dt.replace(minute=0, second=0, microsecond=0)
+
+
+class Days(Filter):
+ @classmethod
+ def mask(cls, dt, **options):
+ """
+ Return a datetime with the same value as ``dt``, to a
+ resolution of days.
+ """
+ return dt.replace(hour=0, minute=0, second=0, microsecond=0)
+
+
+class Weeks(Filter):
+ DAYS_IN_WEEK = 7
+
+ @classmethod
+ def start(cls, now, number, firstweekday=calendar.SATURDAY, **options):
+ """
+ Return the starting datetime: ``number`` of weeks before ``now``.
+
+ ``firstweekday`` determines when the week starts. It defaults
+ to Saturday.
+ """
+ week = cls.mask(now, firstweekday=firstweekday, **options)
+ days = (number - 1) * cls.DAYS_IN_WEEK
+ return week - timedelta(days=days)
+
+ @classmethod
+ def mask(cls, dt, firstweekday=calendar.SATURDAY, **options):
+ """
+ Return a datetime with the same value as ``dt``, to a
+ resolution of weeks.
+
+ ``firstweekday`` determines when the week starts. It defaults
+ to Saturday.
+ """
+ correction = (dt.weekday() - firstweekday) % cls.DAYS_IN_WEEK
+ week = dt - timedelta(days=correction)
+ return week.replace(hour=0, minute=0, second=0, microsecond=0)
+
+
+class Months(Filter):
+ MONTHS_IN_YEAR = 12
+
+ @classmethod
+ def start(cls, now, number, **options):
+ """
+ Return the starting datetime: ``number`` of months before ``now``.
+ """
+ year = now.year
+ month = now.month - number + 1
+ # Handle negative months
+ if month < 0:
+ year = year + (month / cls.MONTHS_IN_YEAR)
+ month = month % cls.MONTHS_IN_YEAR
+ # Handle December
+ if month == 0:
+ year = year - 1
+ month = 12
+ return cls.mask(now, **options).replace(year=year, month=month)
+
+ @classmethod
+ def mask(cls, dt, **options):
+ """
+ Return a datetime with the same value as ``dt``, to a
+ resolution of months.
+ """
+ return dt.replace(day=1,
+ hour=0, minute=0, second=0, microsecond=0)
+
+
+class Years(Filter):
+ @classmethod
+ def start(cls, now, number, **options):
+ """
+ Return the starting datetime: ``number`` of years before ``now``.
+ """
+ return cls.mask(now).replace(year=(now.year - number + 1))
+
+ @classmethod
+ def mask(cls, dt, **options):
+ """
+ Return a datetime with the same value as ``dt``, to a
+ resolution of years.
+ """
+ return dt.replace(month=1, day=1,
+ hour=0, minute=0, second=0, microsecond=0)
5 run-tests.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+from test import *
+
+Main()
71 setup.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+from distutils.cmd import Command
+
+import os
+import re
+import subprocess
+import sys
+
+
+_dir_ = os.path.dirname(__file__)
+
+
+class test(Command):
+ description = "run tests"
+ user_options = [
+ ('tests=', 't', 'test names, separated by commas'),
+ ]
+
+ def initialize_options(self):
+ self.tests = None
+
+ def finalize_options(self):
+ if self.tests is not None:
+ self.tests = re.split('[, ]+', self.tests)
+
+ def run(self):
+ """Run test/tests.py and quit with exit status"""
+ progname = os.path.join(_dir_, 'run-tests.py')
+ cmd = [sys.executable, progname]
+ if self.verbose == 0:
+ cmd.append('--quiet')
+ elif self.verbose >= 2:
+ cmd.append('--verbose')
+ if self.tests:
+ cmd.extend(self.tests)
+ retcode = subprocess.call(cmd)
+ if retcode != 0:
+ sys.exit(retcode)
+
+
+def long_description():
+ sys.path.insert(0, _dir_)
+ import grandfatherson
+ return grandfatherson.__doc__
+
+
+setup(name='GrandFatherSon',
+ version='1.0',
+ description='Grandfather-father-son backup rotation calculator',
+ long_description=long_description(),
+ author='Ecometrica',
+ author_email='info@ecometrica.com',
+ url='http://github.com/ecometrica/grandfatherson/',
+ packages=['grandfatherson'],
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: System Administrators',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Topic :: Software Development :: Libraries',
+ 'Topic :: System :: Archiving',
+ ],
+ license='BSD License',
+ cmdclass={'test': test},
+)
40 test/__init__.py
@@ -0,0 +1,40 @@
+import doctest
+import os
+
+import grandfatherson
+
+from test_filters import *
+
+
+class Main(unittest.main):
+ """Loads doctests with the rest of the TestSuite"""
+ doctests = [grandfatherson]
+
+ def parseArgs(self, *args, **kwargs):
+ unittest.main.parseArgs(self, *args, **kwargs)
+ if not getattr(self, 'testNames', None):
+ self.createDocTests(None)
+
+ def createTests(self):
+ doctests = set(d.__name__ for d in self.doctests)
+
+ # Filter out doctests to avoid confusing super'
+ if self.testNames:
+ testnames = set(self.testNames)
+ self.testNames = list(testnames - doctests)
+ else:
+ testnames = set()
+
+ unittest.main.createTests(self)
+
+ self.createDocTests(testnames & doctests)
+
+ def createDocTests(self, testnames):
+ optionflags = (doctest.ELLIPSIS |
+ doctest.NORMALIZE_WHITESPACE)
+ # Add doctests back in
+ for mod in self.doctests:
+ if testnames is None or mod.__name__ in testnames:
+ self.test.addTest(
+ doctest.DocTestSuite(mod, optionflags=optionflags)
+ )
672 test/test_filters.py
@@ -0,0 +1,672 @@
+from datetime import datetime
+import unittest
+
+from grandfatherson import (MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
+ FRIDAY, SATURDAY, SUNDAY)
+from grandfatherson.filters import (Seconds, Minutes, Hours, Days, Weeks,
+ Months, Years)
+
+
+class TestSeconds(unittest.TestCase):
+ def setUp(self):
+ self.now = datetime(2000, 1, 1, 0, 0, 1, 1)
+ self.datetimes = [
+ datetime(2000, 1, 1, 0, 0, 1, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 1),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(1999, 12, 31, 23, 59, 57, 0),
+ ]
+
+ def test_mask(self):
+ self.assertEqual(
+ Seconds.mask(datetime(1999, 12, 31, 23, 59, 59, 999999)),
+ datetime(1999, 12, 31, 23, 59, 59, 0)
+ )
+
+ def test_future(self):
+ datetimes = [datetime(2010, 1, 15, 0, 0, 0, 0)] # Wikipedia
+ self.assertEqual(Seconds.filter(datetimes, number=0, now=self.now),
+ set(datetimes))
+ self.assertEqual(Seconds.filter(datetimes, number=1, now=self.now),
+ set(datetimes))
+
+ def test_invalid_number(self):
+ self.assertRaises(ValueError,
+ Seconds.filter, [], number=-1, now=self.now)
+ self.assertRaises(ValueError,
+ Seconds.filter, [], number=0.1, now=self.now)
+ self.assertRaises(ValueError,
+ Seconds.filter, [], number='1', now=self.now)
+
+ def test_no_input(self):
+ self.assertEqual(Seconds.filter([], number=1, now=self.now),
+ set())
+
+ def test_no_results(self):
+ self.assertEqual(Seconds.filter([self.now], number=0, now=self.now),
+ set())
+ self.assertEqual(Seconds.filter(self.datetimes, number=0,
+ now=self.now),
+ set())
+
+ def test_current(self):
+ self.assertEqual(Seconds.filter(self.datetimes, number=1,
+ now=self.now),
+ set([datetime(2000, 1, 1, 0, 0, 1, 0)]))
+
+ def test_duplicates(self):
+ # Ensure we get the oldest per-second datetime when there are
+ # duplicates: i.e. not datetime(2000, 1, 1, 0, 0, 0, 1)
+ self.assertEqual(Seconds.filter(self.datetimes, number=2,
+ now=self.now),
+ set([datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 1, 0)]))
+
+ def test_microseconds(self):
+ self.assertEqual(Seconds.filter(self.datetimes, number=3,
+ now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 1, 0)]))
+
+ def test_before_start(self):
+ # datetime(1999, 12, 31, 23, 59, 57, 0) is too old to show up
+ # in the results
+ self.assertEqual(Seconds.filter(self.datetimes, number=4,
+ now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 1, 0)]))
+
+ def test_all_input(self):
+ self.assertEqual(Seconds.filter(self.datetimes, number=5,
+ now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 57, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 1, 0)]))
+
+ self.assertEqual(Seconds.filter(self.datetimes, number=6,
+ now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 57, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 1, 0)]))
+
+
+class TestMinutes(unittest.TestCase):
+ def setUp(self):
+ self.now = datetime(2000, 1, 1, 0, 1, 1, 1)
+ self.datetimes = [
+ datetime(2000, 1, 1, 0, 1, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 1, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(1999, 12, 31, 23, 57, 0, 0),
+ ]
+
+ def test_mask(self):
+ self.assertEqual(
+ Minutes.mask(datetime(1999, 12, 31, 23, 59, 59, 999999)),
+ datetime(1999, 12, 31, 23, 59, 0, 0)
+ )
+
+ def test_future(self):
+ datetimes = [datetime(2010, 1, 15, 0, 0, 0, 0)] # Wikipedia
+ self.assertEqual(Minutes.filter(datetimes, number=0, now=self.now),
+ set(datetimes))
+ self.assertEqual(Minutes.filter(datetimes, number=1, now=self.now),
+ set(datetimes))
+
+ def test_invalid_number(self):
+ self.assertRaises(ValueError,
+ Minutes.filter, [], number=-1, now=self.now)
+ self.assertRaises(ValueError,
+ Minutes.filter, [], number=0.1, now=self.now)
+ self.assertRaises(ValueError,
+ Minutes.filter, [], number='1', now=self.now)
+
+ def test_no_input(self):
+ self.assertEqual(Minutes.filter([], number=1, now=self.now),
+ set())
+
+ def test_no_results(self):
+ self.assertEqual(Minutes.filter([self.now], number=0, now=self.now),
+ set())
+ self.assertEqual(Minutes.filter(self.datetimes, number=0,
+ now=self.now),
+ set())
+
+ def test_current(self):
+ self.assertEqual(Minutes.filter(self.datetimes, number=1,
+ now=self.now),
+ set([datetime(2000, 1, 1, 0, 1, 0, 0)]))
+
+ def test_duplicates(self):
+ # Ensure we get the oldest per-minute datetime when there are
+ # duplicates: i.e. not datetime(2000, 1, 1, 0, 0, 1, 0)
+ self.assertEqual(Minutes.filter(self.datetimes, number=2,
+ now=self.now),
+ set([datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 1, 0, 0)]))
+
+ def test_microseconds(self):
+ self.assertEqual(Minutes.filter(self.datetimes, number=3,
+ now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 1, 0, 0)]))
+
+ def test_before_start(self):
+ # datetime(1999, 12, 31, 23, 57, 0, 0) is too old to show up
+ # in the results
+ self.assertEqual(Minutes.filter(self.datetimes, number=4,
+ now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 1, 0, 0)]))
+
+ def test_all_input(self):
+ self.assertEqual(Minutes.filter(self.datetimes, number=5,
+ now=self.now),
+ set([datetime(1999, 12, 31, 23, 57, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 1, 0, 0)]))
+
+ self.assertEqual(Minutes.filter(self.datetimes, number=6,
+ now=self.now),
+ set([datetime(1999, 12, 31, 23, 57, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 1, 0, 0)]))
+
+
+class TestHours(unittest.TestCase):
+ def setUp(self):
+ self.now = datetime(2000, 1, 1, 1, 1, 1, 1)
+ self.datetimes = [
+ datetime(2000, 1, 1, 1, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 1, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(1999, 12, 31, 21, 0, 0, 0),
+ ]
+
+ def test_mask(self):
+ self.assertEqual(
+ Hours.mask(datetime(1999, 12, 31, 23, 59, 59, 999999)),
+ datetime(1999, 12, 31, 23, 0, 0, 0)
+ )
+
+ def test_future(self):
+ datetimes = [datetime(2010, 1, 15, 0, 0, 0, 0)] # Wikipedia
+ self.assertEqual(Hours.filter(datetimes, number=0, now=self.now),
+ set(datetimes))
+ self.assertEqual(Hours.filter(datetimes, number=1, now=self.now),
+ set(datetimes))
+
+ def test_invalid_number(self):
+ self.assertRaises(ValueError,
+ Hours.filter, [], number=-1, now=self.now)
+ self.assertRaises(ValueError,
+ Hours.filter, [], number=0.1, now=self.now)
+ self.assertRaises(ValueError,
+ Hours.filter, [], number='1', now=self.now)
+
+ def test_no_input(self):
+ self.assertEqual(Hours.filter([], number=1, now=self.now),
+ set())
+
+ def test_no_results(self):
+ self.assertEqual(Hours.filter([self.now], number=0, now=self.now),
+ set())
+ self.assertEqual(Hours.filter(self.datetimes, number=0, now=self.now),
+ set())
+
+ def test_current(self):
+ self.assertEqual(Hours.filter(self.datetimes, number=1, now=self.now),
+ set([datetime(2000, 1, 1, 1, 0, 0, 0)]))
+
+ def test_duplicates(self):
+ # Ensure we get the oldest per-hour datetime when there are
+ # duplicates: i.e. not datetime(2000, 1, 1, 0, 1, 0, 0)
+ self.assertEqual(Hours.filter(self.datetimes, number=2,
+ now=self.now),
+ set([datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 1, 0, 0, 0)]))
+
+ def test_microseconds(self):
+ self.assertEqual(Hours.filter(self.datetimes, number=3, now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 1, 0, 0, 0)]))
+
+ def test_before_start(self):
+ # datetime(1999, 12, 31, 21, 0, 0, 0) is too old to show up
+ # in the results
+ self.assertEqual(Hours.filter(self.datetimes, number=4, now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 1, 0, 0, 0)]))
+
+ def test_all_input(self):
+ self.assertEqual(Hours.filter(self.datetimes, number=5, now=self.now),
+ set([datetime(1999, 12, 31, 21, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 1, 0, 0, 0)]))
+
+ self.assertEqual(Hours.filter(self.datetimes, number=6, now=self.now),
+ set([datetime(1999, 12, 31, 21, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 1, 0, 0, 0)]))
+
+
+class TestDays(unittest.TestCase):
+ def setUp(self):
+ self.now = datetime(2000, 1, 1, 1, 1, 1, 1)
+ self.datetimes = [
+ datetime(2000, 1, 1, 1, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(1999, 12, 30, 0, 0, 0, 0),
+ datetime(1999, 12, 28, 0, 0, 0, 0),
+ ]
+
+ def test_mask(self):
+ self.assertEqual(
+ Days.mask(datetime(1999, 12, 31, 23, 59, 59, 999999)),
+ datetime(1999, 12, 31, 0, 0, 0, 0)
+ )
+
+ def test_future(self):
+ datetimes = [datetime(2010, 1, 15, 0, 0, 0, 0)] # Wikipedia
+ self.assertEqual(Days.filter(datetimes, number=0, now=self.now),
+ set(datetimes))
+ self.assertEqual(Days.filter(datetimes, number=1, now=self.now),
+ set(datetimes))
+
+ def test_invalid_number(self):
+ self.assertRaises(ValueError,
+ Days.filter, [], number=-1, now=self.now)
+ self.assertRaises(ValueError,
+ Days.filter, [], number=0.1, now=self.now)
+ self.assertRaises(ValueError,
+ Days.filter, [], number='1', now=self.now)
+
+ def test_no_input(self):
+ self.assertEqual(Days.filter([], number=1, now=self.now),
+ set())
+
+ def test_no_results(self):
+ self.assertEqual(Days.filter([self.now], number=0, now=self.now),
+ set())
+ self.assertEqual(Days.filter(self.datetimes, number=0, now=self.now),
+ set())
+
+ def test_current(self):
+ self.assertEqual(Days.filter(self.datetimes, number=1, now=self.now),
+ set([datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_duplicates(self):
+ # Ensure we get the oldest per-day datetime when there are
+ # duplicates: i.e. not datetime(2000, 1, 1, 1, 0, 0, 0)
+ self.assertEqual(Days.filter(self.datetimes, number=2, now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_before_start(self):
+ # datetime(1999, 12, 28, 0, 0, 0, 0) is too old to show up
+ # in the results
+ self.assertEqual(Days.filter(self.datetimes, number=4, now=self.now),
+ set([datetime(1999, 12, 30, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_all_input(self):
+ self.assertEqual(Days.filter(self.datetimes, number=5, now=self.now),
+ set([datetime(1999, 12, 28, 0, 0, 0, 0),
+ datetime(1999, 12, 30, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ self.assertEqual(Days.filter(self.datetimes, number=6, now=self.now),
+ set([datetime(1999, 12, 28, 0, 0, 0, 0),
+ datetime(1999, 12, 30, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_leap_year(self):
+ # 2004 is a leap year, because it is divisible by 4
+ now = datetime(2004, 3, 1, 0, 0, 0, 0)
+ datetimes_2004 = [
+ datetime(2004, 3, 1, 0, 0, 0, 0),
+ datetime(2004, 2, 29, 0, 0, 0, 0),
+ datetime(2004, 2, 28, 0, 0, 0, 0),
+ datetime(2004, 2, 27, 0, 0, 0, 0),
+ ]
+
+ self.assertEqual(Days.filter(datetimes_2004, number=1, now=now),
+ set([datetime(2004, 3, 1, 0, 0, 0, 0)]))
+
+ self.assertEqual(Days.filter(datetimes_2004, number=2, now=now),
+ set([datetime(2004, 2, 29, 0, 0, 0, 0),
+ datetime(2004, 3, 1, 0, 0, 0, 0)]))
+
+ self.assertEqual(Days.filter(datetimes_2004, number=3, now=now),
+ set([datetime(2004, 2, 28, 0, 0, 0, 0),
+ datetime(2004, 2, 29, 0, 0, 0, 0),
+ datetime(2004, 3, 1, 0, 0, 0, 0)]))
+
+ def test_not_leap_year(self):
+ # 1900 was not a leap year, because it is divisible by 400
+ now = datetime(1900, 3, 1, 0, 0, 0, 0)
+ datetimes_1900 = [
+ datetime(1900, 3, 1, 0, 0, 0, 0),
+ datetime(1900, 2, 28, 0, 0, 0, 0),
+ datetime(1900, 2, 27, 0, 0, 0, 0),
+ ]
+
+ self.assertEqual(Days.filter(datetimes_1900, number=1, now=now),
+ set([datetime(1900, 3, 1, 0, 0, 0, 0)]))
+
+ self.assertEqual(Days.filter(datetimes_1900, number=2, now=now),
+ set([datetime(1900, 2, 28, 0, 0, 0, 0),
+ datetime(1900, 3, 1, 0, 0, 0, 0)]))
+
+ self.assertEqual(Days.filter(datetimes_1900, number=3, now=now),
+ set([datetime(1900, 2, 27, 0, 0, 0, 0),
+ datetime(1900, 2, 28, 0, 0, 0, 0),
+ datetime(1900, 3, 1, 0, 0, 0, 0)]))
+
+
+class TestWeeks(unittest.TestCase):
+ def setUp(self):
+ # 1 January 2000 is a Saturday
+ self.now = datetime(2000, 1, 1, 1, 1, 1, 1)
+ self.datetimes = [
+ datetime(2000, 1, 1, 1, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(1999, 12, 18, 0, 0, 0, 0),
+ datetime(1999, 12, 4, 0, 0, 0, 0),
+ ]
+
+ def test_mask(self):
+ # 31 December 1999 is a Friday.
+ dt = datetime(1999, 12, 31, 23, 59, 59, 999999)
+ self.assertEqual(dt.weekday(), FRIDAY)
+
+ # Default firstweekday is Saturday
+ self.assertEqual(Weeks.mask(dt),
+ Weeks.mask(dt, firstweekday=SATURDAY))
+ self.assertEqual(Weeks.mask(dt),
+ datetime(1999, 12, 25, 0, 0, 0, 0))
+
+ # Sunday
+ self.assertEqual(Weeks.mask(dt, firstweekday=SUNDAY),
+ datetime(1999, 12, 26, 0, 0, 0, 0))
+
+ # If firstweekday is the same as dt.weekday, then it should return
+ # the same day.
+ self.assertEqual(Weeks.mask(dt, firstweekday=dt.weekday()),
+ Days.mask(dt))
+
+ def test_future(self):
+ datetimes = [datetime(2010, 1, 15, 0, 0, 0, 0)] # Wikipedia
+ self.assertEqual(Weeks.filter(datetimes, number=0, now=self.now),
+ set(datetimes))
+ self.assertEqual(Weeks.filter(datetimes, number=1, now=self.now),
+ set(datetimes))
+
+ def test_invalid_number(self):
+ self.assertRaises(ValueError,
+ Weeks.filter, [], number=-1, now=self.now)
+ self.assertRaises(ValueError,
+ Weeks.filter, [], number=0.1, now=self.now)
+ self.assertRaises(ValueError,
+ Weeks.filter, [], number='1', now=self.now)
+
+ def test_no_input(self):
+ self.assertEqual(Weeks.filter([], number=1, now=self.now),
+ set())
+
+ def test_no_results(self):
+ self.assertEqual(Weeks.filter([self.now], number=0, now=self.now),
+ set())
+ self.assertEqual(Weeks.filter(self.datetimes, number=0, now=self.now),
+ set())
+
+ def test_current(self):
+ self.assertEqual(Weeks.filter(self.datetimes, number=1, now=self.now),
+ set([datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_duplicates(self):
+ # Ensure we get the oldest per-day datetime when there are
+ # duplicates: i.e. not datetime(2000, 1, 1, 1, 0, 0, 0)
+ self.assertEqual(Weeks.filter(self.datetimes, number=2, now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_before_start(self):
+ # datetime(1999, 12, 4, 0, 0, 0, 0) is too old to show up
+ # in the results
+ self.assertEqual(Weeks.filter(self.datetimes, number=4, now=self.now),
+ set([datetime(1999, 12, 18, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_all_input(self):
+ self.assertEqual(Weeks.filter(self.datetimes, number=5, now=self.now),
+ set([datetime(1999, 12, 4, 0, 0, 0, 0),
+ datetime(1999, 12, 18, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ self.assertEqual(Weeks.filter(self.datetimes, number=6, now=self.now),
+ set([datetime(1999, 12, 4, 0, 0, 0, 0),
+ datetime(1999, 12, 18, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+
+class TestMonths(unittest.TestCase):
+ def setUp(self):
+ self.now = datetime(2000, 2, 1, 1, 1, 1, 1)
+ self.datetimes = [
+ datetime(2000, 2, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 1, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(1999, 10, 1, 0, 0, 0, 0),
+ ]
+
+ def test_mask(self):
+ self.assertEqual(
+ Months.mask(datetime(1999, 12, 31, 23, 59, 59, 999999)),
+ datetime(1999, 12, 1, 0, 0, 0, 0)
+ )
+
+ def test_future(self):
+ datetimes = [datetime(2010, 1, 15, 0, 0, 0, 0)] # Wikipedia
+ self.assertEqual(Months.filter(datetimes, number=0, now=self.now),
+ set(datetimes))
+ self.assertEqual(Months.filter(datetimes, number=1, now=self.now),
+ set(datetimes))
+
+ def test_invalid_number(self):
+ self.assertRaises(ValueError,
+ Months.filter, [], number=-1, now=self.now)
+ self.assertRaises(ValueError,
+ Months.filter, [], number=0.1, now=self.now)
+ self.assertRaises(ValueError,
+ Months.filter, [], number='1', now=self.now)
+
+ def test_no_input(self):
+ self.assertEqual(Months.filter([], number=1, now=self.now),
+ set())
+
+ def test_no_results(self):
+ self.assertEqual(Months.filter([self.now], number=0, now=self.now),
+ set())
+ self.assertEqual(Months.filter(self.datetimes, number=0, now=self.now),
+ set())
+
+ def test_current(self):
+ self.assertEqual(Months.filter(self.datetimes, number=1, now=self.now),
+ set([datetime(2000, 2, 1, 0, 0, 0, 0)]))
+
+ def test_duplicates(self):
+ # Ensure we get the oldest per-month datetime when there are
+ # duplicates: i.e. not datetime(2000, 1, 1, 1, 0, 0, 0)
+ self.assertEqual(Months.filter(self.datetimes, number=2, now=self.now),
+ set([datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 2, 1, 0, 0, 0, 0)]))
+
+ def test_new_year(self):
+ self.assertEqual(Months.filter(self.datetimes, number=3, now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 2, 1, 0, 0, 0, 0)]))
+
+ def test_before_start(self):
+ # datetime(1999, 10, 1, 0, 0, 0, 0) is too old to show up
+ # in the results
+ self.assertEqual(Months.filter(self.datetimes, number=4, now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 2, 1, 0, 0, 0, 0)]))
+
+ def test_all_input(self):
+ self.assertEqual(Months.filter(self.datetimes, number=5, now=self.now),
+ set([datetime(1999, 10, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 2, 1, 0, 0, 0, 0)]))
+
+ self.assertEqual(Months.filter(self.datetimes, number=6, now=self.now),
+ set([datetime(1999, 10, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(2000, 2, 1, 0, 0, 0, 0)]))
+
+ def test_multiple_years(self):
+ now = datetime(2000, 1, 1, 0, 0, 0, 0)
+ datetimes = [
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 1, 0, 0, 0, 0),
+ datetime(1999, 1, 1, 0, 0, 0, 0),
+ datetime(1998, 12, 1, 0, 0, 0, 0),
+ datetime(1997, 12, 1, 0, 0, 0, 0),
+ ]
+
+ # 12 months back ignores datetime(1999, 1, 1, 0, 0, 0, 0)
+ self.assertEqual(Months.filter(datetimes, number=12, now=now),
+ set([datetime(1999, 12, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ # But 13 months back gets it
+ self.assertEqual(Months.filter(datetimes, number=13, now=now),
+ set([datetime(1999, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ # But 14 months back gets datetime(1998, 12, 1, 0, 0, 0, 0)
+ self.assertEqual(Months.filter(datetimes, number=14, now=now),
+ set([datetime(1998, 12, 1, 0, 0, 0, 0),
+ datetime(1999, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ # As does 24 months back
+ self.assertEqual(Months.filter(datetimes, number=24, now=now),
+ set([datetime(1998, 12, 1, 0, 0, 0, 0),
+ datetime(1999, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ # 36 months back should get datetime(1997, 12, 1, 0, 0, 0, 0)
+ self.assertEqual(Months.filter(datetimes, number=36, now=now),
+ set([datetime(1997, 12, 1, 0, 0, 0, 0),
+ datetime(1998, 12, 1, 0, 0, 0, 0),
+ datetime(1999, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 1, 0, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+
+class TestYears(unittest.TestCase):
+ def setUp(self):
+ self.now = datetime(2000, 1, 1, 1, 1, 1, 1)
+ self.datetimes = [
+ datetime(2000, 1, 1, 1, 0, 0, 0),
+ datetime(2000, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(1998, 1, 1, 0, 0, 0, 0),
+ datetime(1996, 1, 1, 0, 0, 0, 0),
+ ]
+
+ def test_mask(self):
+ self.assertEqual(
+ Years.mask(datetime(1999, 12, 31, 23, 59, 59, 999999)),
+ datetime(1999, 1, 1, 0, 0, 0, 0)
+ )
+
+ def test_future(self):
+ datetimes = [datetime(2010, 1, 15, 0, 0, 0, 0)] # Wikipedia
+ self.assertEqual(Years.filter(datetimes, number=0, now=self.now),
+ set(datetimes))
+ self.assertEqual(Years.filter(datetimes, number=1, now=self.now),
+ set(datetimes))
+
+ def test_invalid_number(self):
+ self.assertRaises(ValueError,
+ Years.filter, [], number=-1, now=self.now)
+ self.assertRaises(ValueError,
+ Years.filter, [], number=0.1, now=self.now)
+ self.assertRaises(ValueError,
+ Years.filter, [], number='1', now=self.now)
+
+ def test_no_input(self):
+ self.assertEqual(Years.filter([], number=1, now=self.now),
+ set())
+
+ def test_no_results(self):
+ self.assertEqual(Years.filter([self.now], number=0, now=self.now),
+ set())
+ self.assertEqual(Years.filter(self.datetimes, number=0, now=self.now),
+ set())
+
+ def test_current(self):
+ self.assertEqual(Years.filter(self.datetimes, number=1, now=self.now),
+ set([datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_duplicates(self):
+ # Ensure we get the oldest per-month datetime when there are
+ # duplicates: i.e. not datetime(2000, 1, 1, 1, 0, 0, 0)
+ self.assertEqual(Years.filter(self.datetimes, number=2, now=self.now),
+ set([datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_before_start(self):
+ # datetime(1996, 1, 1, 0, 0, 0, 0) is too old to show up
+ # in the results
+ self.assertEqual(Years.filter(self.datetimes, number=4, now=self.now),
+ set([datetime(1998, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ def test_all_input(self):
+ self.assertEqual(Years.filter(self.datetimes, number=5, now=self.now),
+ set([datetime(1996, 1, 1, 0, 0, 0, 0),
+ datetime(1998, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
+
+ self.assertEqual(Years.filter(self.datetimes, number=6, now=self.now),
+ set([datetime(1996, 1, 1, 0, 0, 0, 0),
+ datetime(1998, 1, 1, 0, 0, 0, 0),
+ datetime(1999, 12, 31, 23, 59, 59, 999999),
+ datetime(2000, 1, 1, 0, 0, 0, 0)]))
Please sign in to comment.
Something went wrong with that request. Please try again.