From 89cbae5389aa531e24f5436a43b0b9e69a5d3c75 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Wed, 28 Jul 2010 11:10:34 +0200 Subject: [PATCH] simple threshold based logic and tests started writing unittests. implemented cleanup command that removes empty sessions added a simple threshold based alarm logic --- .pida-metadata/python/config.py | 85 ++++++++++++ db/alarm.py | 94 ++++++++++++- db/management/commands/cleanup.py | 15 +++ db/models.py | 175 +++++++++++++++++++++--- db/tests.py | 215 ++++++++++++++++++++++++++++-- settings.py | 7 +- tools/__init__.py | 40 ++++++ tools/date.py | 15 +++ 8 files changed, 608 insertions(+), 38 deletions(-) create mode 100644 .pida-metadata/python/config.py create mode 100644 db/management/commands/cleanup.py create mode 100644 tools/date.py diff --git a/.pida-metadata/python/config.py b/.pida-metadata/python/config.py new file mode 100644 index 0000000..ffebcd4 --- /dev/null +++ b/.pida-metadata/python/config.py @@ -0,0 +1,85 @@ +# The default ``config.py`` + + +def set_prefs(prefs): + """This function is called before opening the project""" + + # Specify which files and folders to ignore in the project. + # Changes to ignored resources are not added to the history and + # VCSs. Also they are not returned in `Project.get_files()`. + # Note that ``?`` and ``*`` match all characters but slashes. + # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' + # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' + # '.svn': matches 'pkg/.svn' and all of its children + # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' + # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' + prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', + '.hg', '.svn', '_svn', '.git'] + + # Specifies which files should be considered python files. It is + # useful when you have scripts inside your project. Only files + # ending with ``.py`` are considered to be python files by + # default. + #prefs['python_files'] = ['*.py'] + + # Custom source folders: By default rope searches the project + # for finding source folders (folders that should be searched + # for finding modules). You can add paths to that list. Note + # that rope guesses project source folders correctly most of the + # time; use this if you have any problems. + # The folders should be relative to project root and use '/' for + # separating folders regardless of the platform rope is running on. + # 'src/my_source_folder' for instance. + #prefs.add('source_folders', 'src') + + # You can extend python path for looking up modules + #prefs.add('python_path', '~/python/') + + # Should rope save object information or not. + prefs['save_objectdb'] = True + prefs['compress_objectdb'] = False + + # If `True`, rope analyzes each module when it is being saved. + prefs['automatic_soa'] = True + # The depth of calls to follow in static object analysis + prefs['soa_followed_calls'] = 0 + + # If `False` when running modules or unit tests "dynamic object + # analysis" is turned off. This makes them much faster. + prefs['perform_doa'] = True + + # Rope can check the validity of its object DB when running. + prefs['validate_objectdb'] = True + + # How many undos to hold? + prefs['max_history_items'] = 32 + + # Shows whether to save history across sessions. + prefs['save_history'] = True + prefs['compress_history'] = False + + # Set the number spaces used for indenting. According to + # :PEP:`8`, it is best to use 4 spaces. Since most of rope's + # unit-tests use 4 spaces it is more reliable, too. + prefs['indent_size'] = 4 + + # Builtin and c-extension modules that are allowed to be imported + # and inspected by rope. + prefs['extension_modules'] = [] + + # Add all standard c-extensions to extension_modules list. + prefs['import_dynload_stdmods'] = True + + # If `True` modules with syntax errors are considered to be empty. + # The default value is `False`; When `False` syntax errors raise + # `rope.base.exceptions.ModuleSyntaxError` exception. + prefs['ignore_syntax_errors'] = False + + # If `True`, rope ignores unresolvable imports. Otherwise, they + # appear in the importing namespace. + prefs['ignore_bad_imports'] = False + + +def project_opened(project): + """This function is called after opening the project""" + # Do whatever you like here! diff --git a/db/alarm.py b/db/alarm.py index b64584c..4f85f4b 100644 --- a/db/alarm.py +++ b/db/alarm.py @@ -2,6 +2,14 @@ Alarm clock logic """ +from uberclock.tools import Enumeration +from django.conf import settings + +import datetime + + +ACTIONS = Enumeration("ACTIONS", + (("LIGHTS",1), "WAKEUP")) class Manager(object): """Manages Alarm Programs""" @@ -69,6 +77,7 @@ class BaseAction(object): def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs + self.background = kwargs.get("background", False) def execute(self): raise NotImplemented @@ -80,8 +89,11 @@ class ExecuteAction(BaseAction): """ def execute(self): import subprocess - self.popen = subprocess.Popen(self.args) - self.popen.communicate() + if self.background: + self.popen = subprocess.Popen(self.args) + else: + self.popen = subprocess.Popen(self.args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return self.popen.communicate() class BaseAlarm(object): """ @@ -92,22 +104,30 @@ class BaseAlarm(object): name = None short_name = None - def __init__(self, manager, session): + def __init__(self, manager, session, **kwargs): self.manager = manager self.session = session self.next_alarm = None + self.snooze_time = kwargs.get('snooze_time', settings.DEFAULT_SNOOZE_TIME) def check(self): """ Returns a alarm action to execute """ - return None + return False - def snooze(self): + def snooze(self, snooze_time=None): """ User pressed snooze """ - pass + if snooze_time is None: + snooze_time = self.snooze_time + + self.next_alarm = datetime.datetime.now() + \ + datetime.timedelta(seconds=snooze_time) + + def stop(self): + self.next_alarm = None def feed(self, entry): """ @@ -120,4 +140,64 @@ class BasicAlarm(BaseAlarm): name = "Basic Alarm" short_name = "basic" -manager.register(BasicAlarm) \ No newline at end of file + def check(self, dt=None): + """ + Returns a alarm action to execute + """ + if not dt: + dt = datetime.datetime.now() + if not self.next_alarm: + return False + if self.next_alarm < dt: + return ACTIONS.WAKEUP + return False + +manager.register(BasicAlarm) + +class MovementAlarm(BasicAlarm): + key = "simple_movement" + name = "Simple Movement" + short_name = "smplmv" + + DEFAULT_THRESHHOLDS = { + 0: 20000, # OpenChronos + None: 1000, # Unknown, default + } + + def __init__(self, *args, **kwargs): + super(MovementAlarm, self).__init__(*args, **kwargs) + + th = self.DEFAULT_THRESHHOLDS[None] + if self.session: + if self.session.detector: + th = self.DEFAULT_THRESHHOLDS.get(session.detector.typ, None) + if self.session.program: + th = self.session.program.get_var("movement_threshhold", th) + if "threshhold" in kwargs: + th = kwargs["threshhold"] + self.threshold = th + + def check(self, dt=None): + """ + Returns a alarm action to execute + """ + if not dt: + dt = datetime.datetime.now() + # if the default action is already fireing + rv = super(MovementAlarm, self).check(dt) + if rv: + return rv + if not self.session.window: + return + fwin = dt - datetime.timedelta(minutes=self.session.window) + for entry in self.session.entry_set.order_by('-date')[:3]: + # stop when the entry is befor the considered window + if entry.date < fwin: + break + if entry.value > self.threshold: + if self.session.action_in_window(ACTIONS.WAKEUP, dt): + return ACTIONS.WAKEUP + + +manager.register(MovementAlarm) + diff --git a/db/management/commands/cleanup.py b/db/management/commands/cleanup.py new file mode 100644 index 0000000..b471228 --- /dev/null +++ b/db/management/commands/cleanup.py @@ -0,0 +1,15 @@ +from django.core.management.base import BaseCommand, CommandError +from uberclock.db.models import cleanup_db +from django.conf import settings +from optparse import make_option +import logging + +class Command(BaseCommand): + args = '' + help = 'Cleanup the database' + + def handle(self, *args, **options): + logging.basicConfig(level=logging.DEBUG) + logging.info("cleanup database") + cleanup_db() + logging.info("done") \ No newline at end of file diff --git a/db/models.py b/db/models.py index 7f993ee..7e281aa 100644 --- a/db/models.py +++ b/db/models.py @@ -1,5 +1,8 @@ from django.db import models + from uberclock.tools import ez_chronos +from uberclock.tools.date import time_to_next_datetime + from django.contrib.auth.models import User from django.contrib import admin from django.conf import settings @@ -10,6 +13,12 @@ import datetime import time import random +import base64 +#import simplejson +try: + import cPickle as pickle +except ImportError: + import pickle # Create your models here. DETECTOR_TYPES = ( @@ -67,15 +76,21 @@ def get_users_default(self, user): class UserProgram(models.Model): """ Maps a alarm program to user and his choosen id + + Object can be used to save settings that are serialsable by simplejson """ user = models.ForeignKey(User, null=True, db_index=True) users_id = models.IntegerField(null=False, db_index=True) default = models.BooleanField(default=False) alarm_key = models.CharField(null=False, max_length=30, choices=alarm.manager.choices_programs) - rname = models.CharField(max_length=30, null=True, blank=True) + rname = models.CharField("name", max_length=30, null=True, blank=True) short_name = models.CharField(max_length=5, null=True, blank=True) - settings = models.TextField() + default_wakeup = models.TimeField("Wakeup", null=True) + default_sleep_time = models.IntegerField(null=True, help_text="Minutes of wanted sleep") + default_window = models.IntegerField(null=True, help_text="Window of Minutes how many minutes before wakeup can be alarmed") + + settings = models.TextField(default=None, editable=False, null=True) objects = UserProgramManager() @@ -92,10 +107,60 @@ def _set_name(self, val): name = property(_get_name, _set_name) + def get_var(self, name, *args): + if not hasattr(self, "_settings"): + if self.settings: + try: + #self._settings = simplejson.loads(self.settings) + self._settings = pickle.loads(base64.b64decode(self.settings)) + except Exception, e: + logging.warning("could not unpickle settings: %s" %e) + print self.settings + self._settings = {} + else: + self._settings = {} + + if name and len(args): + return self._settings.get(name, args[0]) + elif name: + return self._settings.get(name) + + if not self.settings or not isinstance(self.settings, dict): + self.settings = {} + + def set_var(self, name, value): + if not hasattr(self, "_settings"): + self.get_var(None) + self._settings[name] = value + + def del_var(self, name): + if not hasattr(self, "_settings"): + self.get_var(None) + del self._settings[name] + + + def save(self, *args, **kwargs): + if hasattr(self, "_settings"): + #self.settings = simplejson.dumps(self._settings, ensure_ascii=False) + self.settings = base64.b64encode(pickle.dumps(self._settings)) + return super(UserProgram, self).save(*args, **kwargs) + @property def program_class(self): return alarm.manager.get_program(self.alarm_key) + def set_program(self, program): + if program: + self.alarm_key = program.key + else: + self.alarm_key = None + + def get_program(self, session): + if hasattr(self, "_program") and self._program.key == self.alarm_key: + return self._program + self._program = self.program_class(alarm.manager, session) + return self._program + def __unicode__(self): return u"%s of %s" %(self.name, self.user) @@ -149,6 +214,21 @@ def get_new_session(self, user): # create a new session pass + def cleanup_sessions(self): + """ + Clean up session garbage + """ + self.delete_empty_sessions() + + def delete_empty_sessions(self): + # delete empty sessions older then one day + datelimit = datetime.datetime.now() - datetime.timedelta(days=1) + for session in Session.objects.annotate(entry_count=models.Count('entry')).filter(entry_count=0, stop__lt=datelimit): + logging.info("delete %s" %session) + session.learndata.delete() + session.delete() + + class Session(models.Model): """ One Sleep Session @@ -159,6 +239,9 @@ class Session(models.Model): detector = models.ForeignKey(Detector, null=True) program = models.ForeignKey(UserProgram, null=True) wakeup = models.DateTimeField("Wakeup", null=True) + #FIXME messure real slept length + sleep_time = models.IntegerField(null=True, help_text="Minutes of wanted sleep") + window = models.IntegerField(null=True, help_text="Window of Minutes how many minutes before wakeup can be alarmed") rating = models.IntegerField("Rating", null=True) deleted = models.BooleanField("Deleted", default=False) rf_id = models.IntegerField("RF Id", null=True) @@ -167,19 +250,54 @@ class Session(models.Model): objects = SessionManager() - # Do we need this ? - #alone = models.BooleanField("Alone", null=True, default=True, help_text="Sleeping with someone else in the bed") -# def __init__(self, *args, **kwargs): -# if not "user" in kwargs: -# if "detector" in kwargs: -# kwargs["user"] = get_user_or_default(kwargs["device"].default_user) -# else: -# kwargs["user"] = -# if not "program" in kwargs: -# self.kwargs["program"] = UserProgram.objects.get_users_default(kwargs["user"]) -# -# return super(Session, self).__init__(*args, **kwargs) + def __init__(self, *args, **kwargs): + # copy default values from program + if "program" in kwargs: + prog = kwargs["program"] + if not "wakeup" in kwargs: + kwargs["wakeup"] = time_to_next_datetime(prog.default_wakeup) + if not "sleep_time" in kwargs: + kwargs["sleep_time"] = prog.default_sleep_time + if not "window" in kwargs: + kwargs["window"] = prog.default_window + return super(Session, self).__init__(*args, **kwargs) + + + def action_in_window(self, action, dt=None): + """ + Checks if a action is allowed to run + """ + if self.closed: + return False + if self.wakeup: + if not dt: + dt = datetime.datetime.now() + if dt > self.wakeup: + return True + elif self.window: + if action == alarm.ACTIONS.LIGHTS: + # lights get an aditional larger timedelta + if dt >= self.wakeup - datetime.timedelta(minutes=self.window+15): + return True + else: + if dt >= self.wakeup - datetime.timedelta(minutes=self.window): + return True + return False + + + def get_param(self, name): + """ + Returns paramenter of the Sessions alarm + """ + # get the value of the program first + if name in ["wakeup", "sleep_time", "window"]: + if getattr(self, name): + return getattr(self, name) + # lookup default values of the program + if self.program: + return self.program.get_var(name) + def save(self, *args, **kwargs): super(Session, self).save(*args, **kwargs) @@ -222,7 +340,7 @@ def __unicode__(self): return u"Session from %s %s (%s:%0.2d) (%s Entries)" %(self.user, format(self.start, settings.DATETIME_FORMAT), length[0], length[1], entries) def merge(self, source): - # FIXME: add a zero datapoint maybe if the time + # FIXME: add a zero datapoint if the time between entries is to long source.entry_set.all().update(session=self) source.learndata_set.all().delete() @@ -245,6 +363,8 @@ def cifn(key): @property def length(self): + if self.start > self.stop: + return (0, 0, 0) s = (self.stop - self.start).seconds hours, remainder = divmod(s, 3600) minutes, seconds = divmod(remainder, 60) @@ -257,11 +377,22 @@ class Entry(models.Model): counter = models.IntegerField(max_length=3, null=True) session = models.ForeignKey(Session, null=True, db_index=True) + def save(self, *args, **kwargs): + super(Entry, self).save(*args, **kwargs) + # update the session new flag to false if any entry is saved to it. + # it is used then + if self.session and self.session.new: + self.session = False + self.session.save() + def __repr__(self): return "" %(self.date, self.value) def __unicode__(self): - return u"Entry at %s: %s" %(self.date, time_format(self.date, settings.TIME_FORMAT)) + return u"Entry at %s: %s" %(format(self.date, settings.DATETIME_FORMAT), self.value) + + class Meta: + ordering = ('date',) class LearnData(models.Model): @@ -279,6 +410,18 @@ class LearnData(models.Model): help_text="When sleep stopped", null=True) learned = models.BooleanField(default=False) + @property + def placed(self): + if any([self.lights, self.wake, self.start, self.stop]): + return True + return False + + +def cleanup_db(): + # cleanup sessions + Session.objects.cleanup_sessions() + + SIMPLICITI_PHASE_CLOCK_START_RESPONSE = 0x54 diff --git a/db/tests.py b/db/tests.py index 2247054..8acb670 100644 --- a/db/tests.py +++ b/db/tests.py @@ -7,17 +7,206 @@ from django.test import TestCase -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.failUnlessEqual(1 + 1, 2) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} +from models import * +import alarm +import time, datetime + +class DBTest(TestCase): + def test_cleanup(self): + dt = datetime.datetime.now() - datetime.timedelta(days=2) + # emulate an old session + session = Session() + session.save() + session.stop = dt + session.save() + sid = session.id + Session.objects.cleanup_sessions() + self.assertEqual(Session.objects.filter(id=sid).count(), 0) + + def test_learndata(self): + session = Session() + session.save() + ld = session.learndata + ent = Entry(session=session, value=10) + ent.save() + self.assertEqual(ld.placed, False) + for key in ["wake", "start", "stop", "lights"]: + setattr(ld, key, ent) + self.assertEqual(ld.placed, True) + setattr(ld, key, None) + self.assertEqual(ld.placed, False) + + def test_userprogram(self): + up = UserProgram(users_id=0) + up.save() + uid = up.id + self.assertEqual(up.get_var("bla"), None) + self.assertEqual(up.get_var("bla",1), 1) + up.set_var("bla", 23) + self.assertEqual(up.get_var("bla",1), 23) + self.assertEqual(up.get_var("bla"), 23) + up.set_var("wakeup", datetime.time(7, 30)) + up.save() + up = UserProgram.objects.get(id=uid) + # json is not used anymore + #self.assertEqual(up.settings, '{"bla": 23}') + self.assertEqual(up.get_var("bla"), 23) + self.assertEqual(up.get_var("wakeup"), datetime.time(7, 30)) + + + + up.delete() + + def test_session_params(self): + prog = UserProgram(users_id=123) + #prog.set_var("wakeup", datetime.time(6,30)) + prog.default_wakeup = datetime.time(3, 45) + prog.default_window = 29 + #prog.default_sleep_time = None + + prog.save() + session = Session(program=prog) + self.assertEqual(session.wakeup, time_to_next_datetime(prog.default_wakeup)) + self.assertEqual(session.window, prog.default_window) + self.assertEqual(session.sleep_time, None) + + prog.default_window = 24 + prog.default_wakeup = datetime.time(3, 23) + prog.save() + + self.assertEqual(session.wakeup, time_to_next_datetime(datetime.time(3, 45))) + self.assertEqual(session.window, 29) + self.assertEqual(session.sleep_time, None) + + session.program = prog + + self.assertEqual(session.wakeup, time_to_next_datetime(datetime.time(3, 45))) + self.assertEqual(session.window, 29) + self.assertEqual(session.sleep_time, None) + + prog.default_sleep_time = 300 + + session = Session(program=prog) + self.assertEqual(session.sleep_time, 300) + + prog.delete() + + def test_session_functions(self): + prog = UserProgram(users_id=123) + #prog.set_var("wakeup", datetime.time(6,30)) + prog.default_wakeup = datetime.time(3, 45) + prog.default_window = 29 + prog.save() + + session = Session(program=prog) + + now = datetime.datetime.now() + session.window = 15 + session.wakeup = now + datetime.timedelta(minutes=20) + self.assertEqual(session.action_in_window(alarm.ACTIONS.WAKEUP), False) + self.assertEqual(session.action_in_window(alarm.ACTIONS.LIGHTS), True) + session.window = 21 + self.assertEqual(session.action_in_window(alarm.ACTIONS.WAKEUP), True) + self.assertEqual(session.action_in_window(alarm.ACTIONS.LIGHTS), True) + + + +class AlarmTest(TestCase): + def test_basic_alarm(self): + session = Session() + session.save() + basic = alarm.BasicAlarm(alarm.manager, session) + self.assertEqual(basic.check(), False) + basic.snooze(0.001) + time.sleep(0.01) + self.assertEqual(basic.check(), alarm.ACTIONS.WAKEUP) + + + def test_action(self): + ea = alarm.ExecuteAction("echo", "-n", "testrun") + rv = ea.execute() + self.assertEqual(rv[0], "testrun") + + def test_movement(self): + prog = UserProgram(users_id=2) + #prog.set_var("wakeup", datetime.time(6,30)) + prog.default_wakeup = datetime.time(5, 00) + prog.default_window = 10 + prog.save() + + session = Session(program=prog) + + now = datetime.datetime.now() + start = datetime.datetime.now() + session.window = 15 + session.wakeup = now + datetime.timedelta(minutes=20) + self.assertEqual(prog.alarm_key, "") + self.assertEqual(prog.program_class, alarm.BasicAlarm) + prog.set_program(alarm.manager.get_program("simple_movement")) + self.assertEqual(prog.alarm_key, "simple_movement") + self.assertEqual(prog.program_class, alarm.MovementAlarm) + ai = prog.get_program(session) + ai.threshold = 3000 + self.assertTrue(isinstance(ai, prog.program_class)) + + ok = session.wakeup - datetime.timedelta(minutes=session.window) + end = now + datetime.timedelta(minutes=15) + import random + random.seed(0) + ctime = now - datetime.timedelta(minutes=15) + for i in xrange(4000): + # we should stop here with random tests, they should never have stopped + # yet + jit = random.randrange(0, 30) + ctime += datetime.timedelta(seconds=20+0.3*jit) + var = random.randrange(0,3242) + # emulate packet drop + if jit < 5: + continue + if ctime > end: + var = 3102 + + ent = Entry(value=var, date=ctime, session=session) + ent.save() + ent.date = ctime + ent.save() + ai.feed(ent) + res = ai.check(ctime) + if ctime >= ok and var >= 3000 : + self.assertEquals(res, alarm.ACTIONS.WAKEUP) + # we can stop now + break + else: + self.assertFalse(res) + + if ctime > end: + break + + + + + + def test_neuro1(self): + pass + + + +from uberclock.tools.date import time_to_next_datetime +# test some code from tools +class ToolsTest(TestCase): + def test_time_to_next_datetime(self): + now = datetime.datetime.now() + prev = now - datetime.timedelta(minutes=1) + nd = time_to_next_datetime(prev.time()) + # past test + self.assertTrue(nd > now) + self.assertTrue(nd - datetime.timedelta(hours=23, minutes=55) > now) + + # futur test + next = now + datetime.timedelta(minutes=1) + nd = time_to_next_datetime(next.time()) + + self.assertTrue(nd > now) + self.assertTrue(nd - datetime.timedelta(minutes=2) < now) + diff --git a/settings.py b/settings.py index dab8031..9bce7d0 100644 --- a/settings.py +++ b/settings.py @@ -2,7 +2,7 @@ import os, os.path -CONFIG_PATH = os.path.expanduser("~/.uberclock") +CONFIG_PATH = os.path.expanduser("~/.config/uberclock") if not os.path.exists(CONFIG_PATH): os.mkdir(CONFIG_PATH) @@ -20,7 +20,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': os.path.expanduser('~/.uberclock/db.sqlite'), # Or path to database file if using sqlite3. + 'NAME': os.path.join(CONFIG_PATH, "db.sqlite"), # Or path to database file if using sqlite3. 'USER': '', # Not used with sqlite3. 'PASSWORD': '', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. @@ -137,6 +137,9 @@ # hight in meters above normal hight CLOCK_ALTITUDE = None +# 5 minutes snooze time +DEFAULT_SNOOZE_TIME = 5 * 60 + CHUMBY_URLS = { 'default' : '' } diff --git a/tools/__init__.py b/tools/__init__.py index e69de29..e05dd46 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -0,0 +1,40 @@ +class Enumeration(object): + """ + Enumeration class for constants + """ + __slots__ = '_name', '_lookup' + + def __init__(self, name, enumlist): + self._name = name + lookup = {} + i = 0 + for x in enumlist: + if type(x) is tuple: + x, i = x + if type(x) is not str: + raise TypeError, "enum name is not a string: " + x + if type(i) is not int: + raise TypeError, "enum value is not an integer: " + i + if x in lookup: + raise ValueError, "enum name is not unique: " + x + if i in lookup: + raise ValueError, "enum value is not unique for " + x + lookup[x] = i + lookup[i] = x + i = i + 1 + self._lookup = lookup + + def __getattr__(self, attr): + try: + return self._lookup[attr] + except KeyError: + raise AttributeError, attr + + def whatis(self, value): + """ + Return the name that represents this value + """ + return self._lookup[value] + + def __repr__(self): + return '' %self._name diff --git a/tools/date.py b/tools/date.py new file mode 100644 index 0000000..111ba40 --- /dev/null +++ b/tools/date.py @@ -0,0 +1,15 @@ +import datetime + +def time_to_next_datetime(time, dt=None): + """ + Returns the next valid datetime object for a given + datetime.time object + """ + if not dt: + dt = datetime.datetime.now() + + res = datetime.datetime.combine(dt.date(), time) + if res < datetime.datetime.now(): + res = res + datetime.timedelta(days=1) + return res +