diff --git a/.gitignore b/.gitignore index 288d0f7..1a12488 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /dist/ /*.egg-info build +/*.egg/ +/*.egg diff --git a/larigira/audiogen.py b/larigira/audiogen.py index 85b00b9..95d5894 100644 --- a/larigira/audiogen.py +++ b/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) diff --git a/larigira/mpc.py b/larigira/mpc.py index 26b7536..0b3e04f 100644 --- a/larigira/mpc.py +++ b/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): diff --git a/larigira/tests/test_time_every.py b/larigira/tests/test_time_every.py new file mode 100644 index 0000000..4f66bc5 --- /dev/null +++ b/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 + }) diff --git a/larigira/timegen.py b/larigira/timegen.py new file mode 100644 index 0000000..8c500f1 --- /dev/null +++ b/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) diff --git a/larigira/timegen_every.py b/larigira/timegen_every.py new file mode 100644 index 0000000..a0bd820 --- /dev/null +++ b/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 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 diff --git a/setup.py b/setup.py index 45cd458..17128fe 100644 --- a/setup.py +++ b/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', ] } )