Changeset - 771f50f19b4b
[Not reviewed]
default
0 6 1
Drew Perttula - 11 years ago 2014-06-09 07:10:39
drewp@bigasterisk.com
single-line effect code now evals by changing <uri> into a suitable python object
Ignore-this: cde829f021be54bc7cfd63bddde43aa9
7 files changed with 165 insertions and 34 deletions:
0 comments (0 inline, 0 general)
bin/effecteval
Show inline comments
 
@@ -44,25 +44,25 @@ class SongEffects(PrettyErrorHandler, cy
 
        with graph.currentState(
 
                tripleFilter=(dropped, RDFS.label, None)) as g:
 
            droppedSubLabel = g.label(dropped)
 
            
 
        cr = CurveResource(graph, curve)
 
        cr.newCurve(ctx, label=Literal('sub %s' % droppedSubLabel))
 
        cr.saveCurve()
 
        graph.patch(Patch(addQuads=[
 
            (song, L9['curve'], curve, ctx),
 
            (song, L9['effect'], effect, ctx),
 
            (effect, RDF.type, L9['Effect'], ctx),
 
            (effect, L9['code'],
 
             Literal('out = sub(%s, intensity=%s)' % (dropped.n3(), curve.n3())),
 
             Literal('out = %s * %s' % (dropped.n3(), curve.n3())),
 
             ctx),
 
            ]))
 
        
 
        
 
class SongEffectsUpdates(cyclone.websocket.WebSocketHandler):
 
    def connectionMade(self, *args, **kwargs):
 
        self.graph = self.settings.graph
 
        self.graph.addHandler(self.updateClient)
 
        
 
    def updateClient(self):
 
        # todo: abort if client is gone
 
        playlist = self.graph.value(showconfig.showUri(), L9['playList'])
 
@@ -93,31 +93,33 @@ class EffectUpdates(cyclone.websocket.We
 
        
 
    def connectionLost(self, reason):
 
        log.info("websocket closed")
 

	
 
    def messageReceived(self, message):
 
        log.info("got message %s" % message)
 
        # write a patch back to the graph
 

	
 
class Code(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def put(self):
 
        effect = URIRef(self.get_argument('uri'))
 
        code = Literal(self.get_argument('code'))
 
        with self.settings.graph.currentState(tripleFilter=(None, L9['effect'], effect)) as g:
 
        with self.settings.graph.currentState(
 
                tripleFilter=(None, L9['effect'], effect)) as g:
 
            song = g.subjects(L9['effect'], effect).next()
 
        self.settings.graph.patchObject(
 
            context=song,
 
            subject=effect,
 
            predicate=L9['code'],
 
            newObject=code)
 
        # right here we could tell if the code has a python error and return it
 
        self.send_error(202)
 
        
 
class EffectEval(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    @inlineCallbacks
 
    def get(self):
 
        # return dmx list for that effect
 
        uri = URIRef(self.get_argument('uri'))
 
        response = yield cyclone.httpclient.fetch(
 
            networking.musicPlayer.path('time'))
 
        songTime = json.loads(response.body)['t']
 

	
 
        node = EffectNode(self.settings.graph, uri)
light9/curvecalc/curve.py
Show inline comments
 
@@ -13,25 +13,26 @@ log = logging.getLogger()
 
introPad = 4
 
postPad = 4
 

	
 
class Curve(object):
 
    """curve does not know its name. see Curveset"""
 
    def __init__(self, uri, pointsStorage='graph'):
 
        self.uri = uri
 
        self.pointsStorage = pointsStorage
 
        self.points = [] # x-sorted list of (x,y)
 
        self._muted = False
 

	
 
    def __repr__(self):
 
        return "<%s (%s points)>" % (self.__class__.__name__, len(self.points))
 
        return "<%s %s (%s points)>" % (self.__class__.__name__, self.uri,
 
                                        len(self.points))
 

	
 
    def muted():
 
        doc = "Whether to currently send levels (boolean, obviously)"
 
        def fget(self):
 
            return self._muted
 
        def fset(self, val):
 
            self._muted = val
 
            dispatcher.send('mute changed', sender=self)
 
        return locals()
 
    muted = property(**muted())
 

	
 
    def toggleMute(self):
 
@@ -174,34 +175,38 @@ class CurveResource(object):
 
        Pass the ctx where the main curve data (not the points) will go.
 
        """
 
        if hasattr(self, 'curve'):
 
            raise ValueError('CurveResource already has a curve %r' %
 
                             self.curve)
 
        self.graph.patch(Patch(addQuads=[
 
            (self.uri, RDF.type, L9['Curve'], ctx),
 
            (self.uri, RDFS.label, label, ctx),
 
            ]))
 
        self.curve = Curve(self.uri)
 
        self.curve.points.extend([(0, 0)])
 
        self.saveCurve()
 
        self.watchChanges()
 
        self.watchCurvePointChanges()
 
        
 
    def loadCurve(self):
 
        if hasattr(self, 'curve'):
 
            raise ValueError('CurveResource already has a curve %r' %
 
                             self.curve)
 
        pointsFile = self.graph.value(self.uri, L9['pointsFile'])
 
        self.curve = Curve(self.uri,
 
                           pointsStorage='file' if pointsFile else 'graph')
 
        self.graph.addHandler(self.pointsFromGraph)
 
        if hasattr(self.graph, 'addHandler'):
 
            self.graph.addHandler(self.pointsFromGraph)
 
        else:
 
            # given a currentState graph
 
            self.pointsFromGraph()
 
        
 
    def pointsFromGraph(self):
 
        pts = self.graph.value(self.uri, L9['points'])
 
        if pts is not None:
 
            self.curve.set_from_string(pts)
 
        else:
 
            diskPts = self.graph.value(self.uri, L9['pointsFile'])
 
            if diskPts is not None:
 
                self.curve.load(os.path.join(showconfig.curvesDir(), diskPts))
 
            else:
 
                log.warn("curve %s has no points", self.uri)
 
        self.watchCurvePointChanges()
light9/effecteval/effect.py
Show inline comments
 
from run_local import log
 
import re
 
import re, logging
 
from rdflib import URIRef
 
from light9.namespaces import L9
 
from light9.curvecalc.curve import Curve
 
from light9.namespaces import L9, RDF
 
from light9.curvecalc.curve import CurveResource
 
from light9 import Submaster
 
log = logging.getLogger('effect')
 

	
 
def uriFromCode(s):
 
    # i thought this was something a graph could do with its namespace manager
 
    if s.startswith('sub:'):
 
        return URIRef('http://light9.bigasterisk.com/show/dance2014/sub/' + s[4:])
 
    if s.startswith('song1:'):
 
        return URIRef('http://ex/effect/song1/' + s[6:])
 
    if (s[0], s[-1]) == ('<', '>'):
 
        return URIRef(s[1:-1])
 
    raise NotImplementedError
 

	
 
# consider http://waxeye.org/ for a parser that can be used in py and js
 

	
 
class CodeLine(object):
 
    """code string is immutable"""
 
    def __init__(self, graph, code):
 
        self.graph, self.code = graph, code
 

	
 
        self.outName, self.expr, self.resources = self._asPython()
 

	
 
    def _asPython(self):
 
        """
 
        out = sub(<uri1>, intensity=<curveuri2>)
 
        becomes
 
          'out',
 
          'sub(_u1, intensity=curve(_u2, t))',
 
          {'_u1': URIRef('uri1'), '_u2': URIRef('uri2')}
 
        """
 
        lname, expr = [s.strip() for s in self.code.split('=', 1)]
 
        self.uriCounter = 0
 
        resources = {}
 

	
 
        def alreadyInCurveFunc(s, i):
 
            prefix = 'curve('
 
            return i >= len(prefix) and s[i-len(prefix):i] == prefix
 

	
 
        def repl(m):
 
            v = '_res%s' % self.uriCounter
 
            self.uriCounter += 1
 
            r = resources[v] = URIRef(m.group(1))
 
            if self._uriIsCurve(r):
 
                if not alreadyInCurveFunc(m.string, m.start()):
 
                    return 'curve(%s, t)' % v
 
            return v
 
        outExpr = re.sub(r'<(http\S*?)>', repl, expr)
 
        return lname, outExpr, resources
 

	
 
    def _uriIsCurve(self, uri):
 
        # this result could vary with graph changes (rare)
 
        return self.graph.contains((uri, RDF.type, L9['Curve']))
 
        
 
class EffectNode(object):
 
    def __init__(self, graph, uri):
 
        self.graph, self.uri = graph, uri
 
        # this is not expiring at the right time, when an effect goes away
 
        self.graph.addHandler(self.prepare)
 

	
 
    def prepare(self):
 
        self.code = self.graph.value(self.uri, L9['code'])
 
        if self.code is None:
 
        log.info("prepare effect %s", self.uri)
 
        # maybe there can be multiple lines of code as multiple
 
        # objects here, and we sort them by dependencies
 
        codeStr = self.graph.value(self.uri, L9['code'])
 
        if codeStr is None:
 
            raise ValueError("effect %s has no code" % self.uri)
 
        m = re.match(r'^out = sub\((.*?), intensity=(.*?)\)', self.code)
 
        if not m:
 
            raise NotImplementedError
 
        subUri = uriFromCode(m.group(1))
 
        subs = Submaster.get_global_submasters(self.graph)
 
        self.sub = subs.get_sub_by_uri(subUri)
 

	
 
        self.code = CodeLine(self.graph, codeStr)
 

	
 
        self.resourceMap = self.resourcesAsPython()
 
        
 
        intensityCurve = uriFromCode(m.group(2))
 
        self.curve = Curve(uri=intensityCurve)
 
    def resourcesAsPython(self):
 
        """
 
        mapping of the local names for uris in the code to high-level
 
        objects (Submaster, Curve)
 
        """
 
        out = {}
 
        subs = Submaster.get_global_submasters(self.graph)
 
        for localVar, uri in self.code.resources.items():
 
            for rdfClass in self.graph.objects(uri, RDF.type):
 
                if rdfClass == L9['Curve']:
 
                    cr = CurveResource(self.graph, uri)
 
                    cr.loadCurve()
 
                    out[localVar] = cr.curve
 
                elif rdfClass == L9['Submaster']:
 
                    out[localVar] = subs.get_sub_by_uri(uri)
 
                else:
 
                    out[localVar] = uri
 

	
 
        pts = self.graph.value(intensityCurve, L9['points'])
 
        if pts is None:
 
            log.info("curve %r has no points" % intensityCurve)
 
        else:
 
            self.curve.set_from_string(pts)
 

	
 
        return out
 
        
 
    def eval(self, songTime):
 
        # consider http://waxeye.org/ for a parser that can be used in py and js
 
        level = self.curve.eval(songTime)
 
        scaledSubs = self.sub * level
 
        return scaledSubs
 
        ns = {'t': songTime}
 

	
 
        ns.update(dict(
 
            curve=lambda c, t: c.eval(t),
 
            ))
 
        # loop over lines in order, merging in outputs 
 
        # merge in named outputs from previous lines
 
        
 
        ns.update(self.resourceMap)
 
        return eval(self.code.expr, ns)
 

	
 

	
 
class GraphAwareFunction(object):
 
    def __init__(self, graph):
 
        self.graph = graph
 

	
light9/effecteval/effectloop.py
Show inline comments
 
from __future__ import division
 
import time, json, logging, traceback
 
from twisted.internet import reactor
 
from twisted.internet.defer import inlineCallbacks, returnValue
 
from rdflib import URIRef, Literal
 
import cyclone.httpclient
 
from light9.namespaces import L9, RDF, RDFS
 
from light9.effecteval.effect import EffectNode
 
from light9 import networking
 
from light9 import Submaster
 
from light9 import dmxclient
 
log = logging.getLogger('effectloop')
 

	
 
class EffectLoop(object):
 
    """maintains a collection of the current EffectNodes, gets time from
 
    music player, sends dmx"""
 
    def __init__(self, graph, stats):
 
        self.graph, self.stats = graph, stats
 
        self.currentSong = None
 
        self.currentEffects = []
 
        self.lastLogTime = 0
 
        self.lastLogMsg = ""
 
        self.lastErrorLog = 0
 
        self.graph.addHandler(self.setEffects)
 
        self.period = 1 / 30
 
        self.coastSecs = .3 # main reason to keep this low is to notice play/pause
 

	
 
        self.songTimeFromRequest = 0
 
        self.requestTime = 0 # unix sec for when we fetched songTime
 
        reactor.callLater(self.period, self.sendLevels)
 

	
 
    def setEffects(self):
 
        self.currentEffects = []
 
        if self.currentSong is None:
 
            return
 
@@ -64,25 +66,30 @@ class EffectLoop(object):
 
                    self.currentSong = song
 
                    # this may be piling on the handlers
 
                    self.graph.addHandler(self.setEffects)
 

	
 
                if song is None:
 
                    return
 

	
 
                outSubs = []
 
                for e in self.currentEffects:
 
                    try:
 
                        outSubs.append(e.eval(songTime))
 
                    except Exception as exc:
 
                        log.error("effect %s: %s" % (e.uri, exc))
 
                        now = time.time()
 
                        if now > self.lastErrorLog + 5:
 
                            log.error("effect %s: %s" % (e.uri, exc))
 
                            log.error("  expr: %s", e.code.expr)
 
                            log.error("  resources: %r", e.resourcesAsPython())
 
                            self.lastErrorLog = now
 
                out = Submaster.sub_maxes(*outSubs)
 

	
 
                self.logLevels(t1, out)
 
                dmx = out.get_dmx_list()
 
                with self.stats.writeDmx.time():
 
                    yield dmxclient.outputlevels(dmx, twisted=True)
 

	
 
                elapsed = time.time() - t1
 
                dt = max(0, self.period - elapsed)
 
        except Exception:
 
            self.stats.errors += 1
 
            traceback.print_exc()
light9/effecteval/test_effect.py
Show inline comments
 
new file 100644
 
import unittest
 
import mock
 
import sys
 
sys.path.insert(0, 'bin') # for run_local
 

	
 
from effect import CodeLine
 
from rdflib import URIRef
 

	
 
def isCurve(self, uri):
 
    return 'curve' in uri
 

	
 
@mock.patch('light9.effecteval.effect.CodeLine._uriIsCurve', new=isCurve)
 
class TestAsPython(unittest.TestCase):
 
    def test_gets_lname(self):
 
        ec = CodeLine(graph=None, code='x = y+1')
 
        self.assertEqual('x', ec._asPython()[0])
 

	
 
    def test_gets_simple_code(self):
 
        ec = CodeLine(graph=None, code='x = y+1')
 
        self.assertEqual('y+1', ec._asPython()[1])
 
        self.assertEqual({}, ec._asPython()[2])
 
        
 
    def test_converts_uri_to_var(self):
 
        ec = CodeLine(graph=None, code='x = <http://example.com/>')
 
        _, expr, uris = ec._asPython()
 
        self.assertEqual('_res0', expr)
 
        self.assertEqual({'_res0': URIRef('http://example.com/')}, uris)
 

	
 
    def test_converts_multiple_uris(self):
 
        ec = CodeLine(graph=None, code='x = <http://example.com/> + <http://other>')
 
        _, expr, uris = ec._asPython()
 
        self.assertEqual('_res0 + _res1', expr)
 
        self.assertEqual({'_res0': URIRef('http://example.com/'),
 
                          '_res1': URIRef('http://other')}, uris)
 
        
 
    def test_doesnt_fall_for_brackets(self):
 
        ec = CodeLine(graph=None, code='x = 1<2>3< h')
 
        _, expr, uris = ec._asPython()
 
        self.assertEqual('1<2>3< h', expr)
 
        self.assertEqual({}, uris)
 
        
 
    def test_curve_uri_expands_to_curve_eval_func(self):
 
        ec = CodeLine(graph=None, code='x = <http://example/curve1>')
 
        _, expr, uris = ec._asPython()
 
        self.assertEqual('curve(_res0, t)', expr)
 
        self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris)
 

	
 
    def test_curve_doesnt_double_wrap(self):
 
        ec = CodeLine(graph=None, code='x = curve(<http://example/curve1>, t+.01)')
 
        _, expr, uris = ec._asPython()
 
        self.assertEqual('curve(_res0, t+.01)', expr)
 
        self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris)
makefile
Show inline comments
 
NOSEARGS="--no-path-adjustment light9.rdfdb.rdflibpatch light9.rdfdb.patch"
 
NOSEARGS="--no-path-adjustment light9.rdfdb.rdflibpatch light9.rdfdb.patch light9.effecteval.test_effect"
 

	
 
tests:
 
	eval env/bin/nosetests -x $(NOSEARGS)
 

	
 
tests_watch:
 
	eval env/bin/nosetests --with-watch $(NOSEARGS)
 
	eval env/bin/nosetests --with-watcher $(NOSEARGS)
 

	
 

	
 
# needed packages: python-gtk2 python-imaging
 

	
 
install_python_deps: link_to_sys_packages
 
	env/bin/pip install -r pydeps
 

	
 
DP=/usr/lib/python2.7/dist-packages
 
SP=env/lib/python2.7/site-packages
 

	
 
link_to_sys_packages:
 
	# http://stackoverflow.com/questions/249283/virtualenv-on-ubuntu-with-no-site-packages
pydeps
Show inline comments
 
rdflib==4.1.2
 
Twisted==14.0.0
 
webcolors==1.4
 
Louie==1.1
 
cyclone==1.1
 

	
 
web.py==0.37
 
restkit==4.2.2
 
ipython==2.1.0
 
nose==1.3.3
 
nose-alert==0.9.0
 
nose-watcher==0.1.2
 
watchdog==0.7.1
 
ipdb==0.8
 
coloredlogs==0.5
 
genshi==0.7
 
pyjade==2.2.0
 
python-dateutil==2.2
 
txosc==0.2.0
 
service_identity==0.2
 
Pillow==2.4.0
 
faulthandler==2.3
 
treq==0.2.1
 
\ No newline at end of file
 
treq==0.2.1
 
mock==1.0.1
 
\ No newline at end of file
0 comments (0 inline, 0 general)