Browse Source

timegenerators introduced, and some cleanup

boyska 9 years ago
parent
commit
2bd8f56aa3
7 changed files with 315 additions and 14 deletions
  1. 2 0
      .gitignore
  2. 2 2
      larigira/audiogen.py
  3. 11 11
      larigira/mpc.py
  4. 114 0
      larigira/tests/test_time_every.py
  5. 75 0
      larigira/timegen.py
  6. 105 0
      larigira/timegen_every.py
  7. 6 1
      setup.py

+ 2 - 0
.gitignore

@@ -3,3 +3,5 @@
 /dist/
 /*.egg-info
 build
+/*.egg/
+/*.egg

+ 2 - 2
larigira/audiogen.py

@@ -38,7 +38,7 @@ def check_spec(spec):
         yield "Missing field 'kind'"
 
 
-def generate(spec):
+def audiogenerate(spec):
     gen = get_audiogenerator(spec['kind'])
     return tuple(gen(spec))
 
@@ -53,7 +53,7 @@ def main():
         for err in errors:
             print(err)  # TODO: to stderr
         sys.exit(1)
-    for path in generate(spec):
+    for path in audiogenerate(spec):
         print(path)
 
 

+ 11 - 11
larigira/mpc.py

@@ -17,7 +17,7 @@ from mpd import MPDClient
 
 from eventutils import ParentedLet, Timer
 import rpc
-from audiogen import generate
+from audiogen import audiogenerate
 
 CONTINOUS_AUDIODESC = dict(kind='mpd', howmany=1)
 MPD_HOST = os.getenv('MPD_HOST', 'localhost')
@@ -27,6 +27,7 @@ MPD_PORT = int(os.getenv('MPD_PORT', '6600'))
 class MpcWatcher(ParentedLet):
     def __init__(self, queue, client=None):
         ParentedLet.__init__(self, queue)
+        self.log = logging.getLogger(self.__class__.__name__)
         if client is None:
             self.client = MPDClient()
             # TODO: use config values
@@ -37,7 +38,7 @@ class MpcWatcher(ParentedLet):
     def do_business(self):
         while True:
             status = self.client.idle()[0]
-            logging.info(status)
+            self.log.info(status)
             yield ('mpc', status)
 
 
@@ -45,6 +46,7 @@ class Player(gevent.Greenlet):
     def __init__(self):
         gevent.Greenlet.__init__(self)
         self.min_playlist_length = 10
+        self.log = logging.getLogger(self.__class__.__name__)
         self.q = Queue()
 
     def check_playlist(self):
@@ -54,30 +56,28 @@ class Player(gevent.Greenlet):
         songs = mpd_client.playlist()
         if(len(songs) >= self.min_playlist_length):
             return
-        logging.info('need to add new songs')
-        picker = gevent.Greenlet(generate, CONTINOUS_AUDIODESC)
-        picker.link_value(lambda g: mpd_client.add(next(g.value).strip()))
+        self.log.info('need to add new songs')
+        picker = gevent.Greenlet(audiogenerate, CONTINOUS_AUDIODESC)
+        picker.link_value(lambda g: mpd_client.add(g.value[0].strip()))
         picker.start()
 
     def _run(self):
         MpcWatcher(self.q, client=None).start()
-        Timer(6000, self.q).start()
+        Timer(60 * 1000, self.q).start()
         http_server = WSGIServer(('', 5000), rpc.create_app(self.q))
         http_server.start()
         while True:
             value = self.q.get()
+            self.log.debug('<- %s' % str(value))
             # emitter = value['emitter']
             kind = value['kind']
             args = value['args']
             if kind == 'timer':
-                logging.info('CLOCK')
+                self.log.info('CLOCK')
             if kind == 'timer' or (kind == 'mpc' and args[0] == 'playlist'):
                 gevent.Greenlet.spawn(self.check_playlist)
-            elif value['tracker'] == 'mpd_generate':
-                logging.info("generated! %s" % tuple(args[0]))
             else:
-                logging.warning("Unknown message: %s" % str(value))
-            logging.info(str(value))
+                self.log.warning("Unknown message: %s" % str(value))
 
 
 def on_player_crash(*args, **kwargs):

+ 114 - 0
larigira/tests/test_time_every.py

@@ -0,0 +1,114 @@
+from datetime import timedelta
+from pprint import pprint
+
+import pytest
+
+from larigira.timegen_every import FrequencyAlarm, SingleAlarm
+from larigira.timegen import timegenerate
+
+
+def eq_(a, b, reason=None):
+    '''migrating tests from nose'''
+    if reason is not None:
+        assert a == b, reason
+    else:
+        assert a == b
+
+
+@pytest.fixture
+def now():
+    from datetime import datetime
+    return datetime.now()
+
+
+def days(n):
+    return timedelta(days=n)
+
+
+def test_single_creations(now):
+    return SingleAlarm({
+        'timestamp': now
+    })
+
+
+def test_freq_creations(now):
+    return FrequencyAlarm({
+        'start': now - timedelta(days=1),
+        'interval': 3600,
+        'end': now})
+
+
+@pytest.mark.timeout(1)
+def test_single_ring(now):
+    dt = now + days(1)
+    s = SingleAlarm({'timestamp': dt})
+    eq_(s.next_ring(),  dt)
+    eq_(s.next_ring(now),  dt)
+    assert s.next_ring(dt) is None, "%s - %s" % (str(s.next_ring(dt)), str(dt))
+    assert s.next_ring(now + days(2)) is None
+    assert s.has_ring(dt)
+    assert not s.has_ring(now)
+    assert not s.has_ring(now + days(2))
+
+
+@pytest.mark.timeout(1)
+def test_single_all(now):
+    dt = now + timedelta(days=1)
+    s = SingleAlarm({'timestamp': dt})
+    eq_(list(s.all_rings()),  [dt])
+    eq_(list(s.all_rings(now)),  [dt])
+    eq_(list(s.all_rings(now + days(2))),  [])
+
+
+def test_freq_short(now):
+    f = FrequencyAlarm({
+        'start': now - days(1),
+        'interval': 10,
+        'end': now + days(1)
+    })
+    assert now in f.all_rings(now - days(3))
+    assert f.next_ring(now) is not None
+    assert f.next_ring(now) != now
+    assert f.next_ring(now) > now
+    assert now not in f.all_rings(now)
+    for r in f.all_rings(now):
+        assert r > now
+
+
+@pytest.mark.timeout(1)
+def test_freq_ring(now):
+    f = FrequencyAlarm({
+        'start': now - days(1),
+        'interval': 3600,
+        'end': now + days(1)
+    })
+    assert now in f.all_rings(now - days(3))
+    assert f.next_ring(now) is not None
+    assert f.next_ring(now) != now
+    assert f.next_ring(now) > now
+    assert now not in f.all_rings(now)
+    for r in f.all_rings(now):
+        assert r > now
+    allr = list(f.all_rings(now))
+    eq_(len(allr), 24)
+
+    eq_(len(tuple(f.all_rings(now + days(2)))), 0)
+
+    allr = tuple(f.all_rings(now - days(20)))
+    eq_(f.next_ring(now - days(20)), now - days(1))
+    eq_(len(allr), 49, pprint(allr))
+
+
+def test_single_registered():
+    timegenerate({
+        'kind': 'single',
+        'timestamp': 1234567890
+    })
+
+
+def test_frequency_registered():
+    timegenerate({
+        'kind': 'frequency',
+        'start': 1234567890,
+        'interval': 60*15
+    })

+ 75 - 0
larigira/timegen.py

@@ -0,0 +1,75 @@
+'''
+main module to read and get informations about alarms
+'''
+from __future__ import print_function
+import sys
+import argparse
+from pkg_resources import iter_entry_points
+import json
+import logging
+from datetime import datetime
+
+
+def get_timegenerator(kind):
+    '''Messes with entrypoints to return an timegenerator function'''
+    points = tuple(iter_entry_points(group='larigira.timegenerators',
+                                     name=kind))
+    if not points:
+        raise ValueError('cant find a generator for ', kind)
+    if len(points) > 1:
+        logging.warning("Found more than one timegenerator for '%s'" % kind)
+    gen = points[0]
+    return gen.load()
+
+
+def get_parser():
+    parser = argparse.ArgumentParser(
+        description='Generate "ring times" from a timespec')
+    parser.add_argument('timespec', metavar='TIMESPEC', type=str, nargs=1,
+                        help='filename for timespec, formatted in json')
+    parser.add_argument('--now', metavar='NOW', type=int, nargs=1,
+                        default=None,
+                        help='Set a different "time", in unix epoch')
+    parser.add_argument('--howmany', metavar='N', type=int, nargs=1,
+                        default=[1],
+                        help='Set a different "time", in unix epoch')
+    return parser
+
+
+def read_spec(fname):
+    if fname == '-':
+        return json.load(sys.stdin)
+    with open(fname) as buf:
+        return json.load(buf)
+
+
+def check_spec(spec):
+    if 'kind' not in spec:
+        yield "Missing field 'kind'"
+
+
+def timegenerate(spec, now=None, howmany=1):
+    Alarm = get_timegenerator(spec['kind'])
+    generator = Alarm(spec)
+    if now is not None:
+        if type(now) is not datetime:
+            now = datetime.fromtimestamp(now)
+    for _ in xrange(howmany):
+        now = generator.next_ring(current_time=now)
+        yield now
+
+
+def main():
+    '''Main function for the "larigira-timegen" executable'''
+    args = get_parser().parse_args()
+    spec = read_spec(args.timespec[0])
+    errors = tuple(check_spec(spec))
+    if errors:
+        logging.error("Errors in timespec")
+        for err in errors:
+            print(err)  # TODO: to stderr
+        sys.exit(1)
+    now = None if args.now is None else args.now.pop()
+    howmany = None if args.howmany is None else args.howmany.pop()
+    for time in timegenerate(spec, now=now, howmany=howmany):
+        print(time)

+ 105 - 0
larigira/timegen_every.py

@@ -0,0 +1,105 @@
+from __future__ import print_function
+import logging
+log = logging.getLogger('time-every')
+from datetime import datetime, timedelta
+
+
+def getdate(val):
+    if type(val) is int:
+        return datetime.fromtimestamp(val)
+    return val
+
+
+class Alarm(object):
+    def __init__(self):
+        pass
+
+    def next_ring(self, current_time=None):
+        '''if current_time is None, it is now()
+
+        returns the next time it will ring; or None if it will not anymore
+        '''
+        raise NotImplementedError()
+
+    def has_ring(self, time=None):
+        raise NotImplementedError()
+
+    def all_rings(self, current_time=None):
+        '''
+        all future rings
+        this, of course, is an iterator (they could be infinite)
+        '''
+        ring = self.next_ring(current_time)
+        while ring is not None:
+            yield ring
+            ring = self.next_ring(ring)
+
+
+class SingleAlarm(Alarm):
+    '''
+    rings a single time
+    '''
+
+    def __init__(self, obj):
+        self.dt = getdate(obj['timestamp'])
+
+    def next_ring(self, current_time=None):
+        '''if current_time is None, it is now()'''
+        if current_time is None:
+            current_time = datetime.now()
+        if current_time >= self.dt:
+            return None
+        return self.dt
+
+    def has_ring(self, current_time=None):
+        if current_time is None:
+            current_time = datetime.now()
+        return current_time == self.dt
+
+
+class FrequencyAlarm(Alarm):
+    '''
+    rings on {t | exists a k integer >= 0 s.t. t = start+k*t, start<t<end}
+    '''
+
+    def __init__(self, obj):
+        self.start = getdate(obj['start'])
+        self.interval = obj['interval']
+        self.end = getdate(obj['end']) if 'end' in obj else None
+
+    def next_ring(self, current_time=None):
+        '''if current_time is None, it is now()'''
+        if current_time is None:
+            current_time = datetime.now()
+        if self.end is not None and current_time > self.end:
+            return None
+        if current_time < self.start:
+            return self.start
+        if self.end is not None:
+            assert self.start <= current_time <= self.end
+        else:
+            assert self.start <= current_time
+        n_interval = (
+            (current_time - self.start).total_seconds() // self.interval
+            ) + 1
+        ring = self.start + timedelta(seconds=self.interval * n_interval)
+        if ring == current_time:
+            ring += timedelta(seconds=self.interval)
+        if self.end is not None and ring > self.end:
+            return None
+        return ring
+
+    def has_ring(self, current_time=None):
+        if current_time is None:
+            current_time = datetime.now()
+        if not self.start >= current_time >= self.end:
+            return False
+
+        n_interval = (current_time - self.start).total_seconds() // \
+            self.interval
+        expected_time = self.start + \
+            timedelta(seconds=self.interval * n_interval)
+        return expected_time == current_time
+
+    def __str__(self):
+        return 'FrequencyAlarm(every %ds)' % self.interval

+ 6 - 1
setup.py

@@ -41,16 +41,21 @@ setup(name='larigira',
           'flask',
           'python-mpd2'
       ],
-      tests_require=['pytest'],
+      tests_require=['pytest', 'pytest-timeout'],
       cmdclass={'test': PyTest},
       zip_safe=False,
       entry_points={
           'console_scripts': ['larigira=larigira.mpc:main',
+                              'larigira-timegen=larigira.timegen:main',
                               'larigira-audiogen=larigira.audiogen:main'],
           'larigira.audiogenerators': [
               'mpd = larigira.audiogen_mpdrandom:generate_by_artist',
               'static = larigira.audiogen_static:generate',
               'randomdir = larigira.audiogen_randomdir:generate'
+          ],
+          'larigira.timegenerators': [
+              'frequency = larigira.timegen_every:FrequencyAlarm',
+              'single = larigira.timegen_every:SingleAlarm',
           ]
       }
       )