timegenerators introduced, and some cleanup

This commit is contained in:
boyska 2014-11-03 02:15:22 +01:00
parent 241eb30c7c
commit 2bd8f56aa3
No known key found for this signature in database
GPG key ID: 7395DCAE58289CA9
7 changed files with 315 additions and 14 deletions

2
.gitignore vendored
View file

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

View file

@ -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)

View file

@ -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):

View file

@ -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
larigira/timegen.py Normal file
View file

@ -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
larigira/timegen_every.py Normal file
View file

@ -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

View file

@ -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',
]
}
)