#!/usr/bin/env python3
# Copyright (C) 2017 Elena Grandi <valhalla@trueelena.org>
# This program is free software. It comes without any warranty, to the
# extent permitted by applicable law. You can redistribute it and/or
# modify it under the terms of the Do What The Fuck You Want To Public
# License, Version 2, as published by Sam Hocevar. See
# http://www.wtfpl.net/ for more details.
# Debian release dates can be found in the package distro-info-data,
# but an hardcoded fallback is included to run this script where such
# package is not available.
import argparse
import csv
import datetime
from dateutil import relativedelta, parser
import math
import unittest
RELEASES = [
(datetime.date(2019, 7, 6), 'buster'),
(datetime.date(2017, 6, 17), 'stretch'),
(datetime.date(2015, 4, 25), 'jessie'),
(datetime.date(2013, 5, 4), 'wheezy'),
(datetime.date(2011, 2, 6), 'squeeze'),
(datetime.date(2009, 2, 14), 'lenny'),
(datetime.date(2007, 4, 8), 'etch'),
(datetime.date(2005, 6, 6), 'sarge'),
(datetime.date(2002, 7, 19), 'woody'),
(datetime.date(2000, 8, 15), 'potato'),
(datetime.date(1999, 3, 9), 'slink'),
(datetime.date(1998, 6, 24), 'hamm'),
(datetime.date(1997, 7, 2), 'bo'),
(datetime.date(1996, 12, 12), 'rex'),
(datetime.date(1996, 6, 17), 'buzz'),
]
MSG = "{today} is day {dody} of year {yodr} of the {release}"
FUTURE_MSG = "{today} could be day {dody} of year {yodr} of the {release}"
ERR_MSG = "{isodate} is in the Debianless Age"
class DebDate:
def __init__(
self,
distro_info_file='/usr/share/distro-info/debian.csv',
nameless_age=datetime.date(1993, 8, 1)):
self.releases = []
try:
with open(distro_info_file) as fp:
releases = csv.reader(fp, delimiter=',')
for row in releases:
if len(row) > 4:
try:
date = datetime.datetime.strptime(
row[4],
'%Y-%m-%d'
)
except ValueError:
pass
else:
self.releases.append((date.date(), row[1]))
except FileNotFoundError:
releases = []
self.releases.sort(key=lambda x: x[0], reverse=True)
if not self.releases:
self.releases = RELEASES.copy()
if nameless_age:
self.releases.append((
nameless_age,
'Nameless Age'
))
def get_release(self, day):
data = {}
today = datetime.date.today()
if day > today:
# This is just a fuzzy way to decide whether a date is
# likely to be after a future release.
data['certain'] = math.exp((today - day).days / 365.25 )
else:
data['certain'] = 1
if day == today:
data['today'] = 'Today'
else:
data['today'] = day.isoformat()
for r in self.releases:
epoch = r[0]
data['release'] = r[1]
if day > epoch:
break
if day < epoch:
raise OutsideTimeError(
"{day} happened before the beginning of time".format(
day=day,
)
)
if day.year == epoch.year:
data['dody'] = (day - epoch).days
data['yodr'] = 1
else:
data['dody'] = day.timetuple().tm_yday
data['yodr'] = day.year - epoch.year + 1
return data
class OutsideTimeError(ValueError):
"""
Exception raised before the beginning of time
"""
class TestDebDate(unittest.TestCase):
def setUp(self):
self.debdates = [
DebDate(),
DebDate(distro_info_file='/no/such/file'),
]
def testFirstYearRelease(self):
for debdate in self.debdates:
r = debdate.releases[-2]
d = r[0] + relativedelta.relativedelta(days=10)
data = debdate.get_release(d)
self.assertEqual(data['dody'], 10)
self.assertEqual(data['yodr'], 1)
self.assertEqual(data['release'], r[1])
self.assertEqual(data['certain'], 1)
def testSecondYearRelease(self):
for debdate in self.debdates:
r = debdate.releases[-7]
d = r[0] + relativedelta.relativedelta(years=1)
data = debdate.get_release(d)
self.assertEqual(data['dody'], 227)
self.assertEqual(data['yodr'], 2)
self.assertEqual(data['release'], r[1])
self.assertEqual(data['certain'], 1)
def testJanuarySecondYearRelease(self):
for debdate in self.debdates:
r = debdate.releases[-7]
d = datetime.date(r[0].year + 1, 1, 1)
data = debdate.get_release(d)
self.assertEqual(data['dody'], 1)
self.assertEqual(data['yodr'], 2)
self.assertEqual(data['release'], r[1])
self.assertEqual(data['certain'], 1)
def testPastLatestRelease(self):
for debdate in self.debdates:
r = debdate.releases[0]
d = r[0] + relativedelta.relativedelta(years=5)
data = debdate.get_release(d)
# Day Of The Year will change depending on when is the latest
# release, so we do not check it
self.assertEqual(data['yodr'], 6)
self.assertEqual(data['release'], r[1])
self.assertLess(data['certain'], 1)
def testBeforeBeginningOfTime(self):
for debdate in self.debdates:
r = debdate.releases[-1]
d = r[0] + relativedelta.relativedelta(days=-1)
with self.assertRaises(OutsideTimeError):
debdate.get_release(d)
class Command:
def setup_parser(self):
self.parser = argparse.ArgumentParser(
description='''
Convert Gregorian dates to Debian Regnal dates
'''
)
self.date_options = self.parser.add_mutually_exclusive_group()
self.date_options.add_argument(
'-d', '--date',
help='A gregorian date',
default=None,
type=parser.parse,
)
self.date_options.add_argument(
'-s', '--seconds',
help='A date as seconds from the Unix Epoch',
default=None,
type=int,
)
self.parser.set_defaults(func=self.print_date)
self.subparsers = self.parser.add_subparsers()
self.test_parser = self.subparsers.add_parser(
'test',
help='Run unit tests',
)
self.test_parser.set_defaults(func=self.run_tests)
def run_tests(self, args):
suite = unittest.TestLoader().loadTestsFromTestCase(TestDebDate)
unittest.TextTestRunner(verbosity=1).run(suite)
def print_date(self, args):
if args.date:
date = args.date.date()
elif args.seconds:
date = datetime.date.fromtimestamp(args.seconds)
else:
date = datetime.date.today()
debdate = DebDate()
try:
data = debdate.get_release(date)
except OutsideTimeError:
print(ERR_MSG.format(isodate=date.strftime("%Y-%m-%d")))
else:
if data['certain'] > 0.14:
print(MSG.format(**data))
else:
print(FUTURE_MSG.format(**data))
def main(self):
self.setup_parser()
args = self.parser.parse_args()
args.func(args)
if __name__ == '__main__':
Command().main()