Changeset - a631e075a5bf
[Not reviewed]
default
0 6 0
drewp@bigasterisk.com - 12 years ago 2012-07-19 04:23:06
drewp@bigasterisk.com
KC big rewrites, now multiple KC instances can sync with rdfdb
Ignore-this: 8c75ec3e2bd360c6eb87f7f4d4b3dcc4
6 files changed with 321 insertions and 120 deletions:
0 comments (0 inline, 0 general)
bin/keyboardcomposer
Show inline comments
 
#!bin/python
 

	
 
from __future__ import division, nested_scopes
 
import cgi, os, sys, time, subprocess, logging
 
import cgi, os, sys, time, subprocess, logging, random
 
from optparse import OptionParser
 
import webcolors, colorsys
 
from louie import dispatcher
 
from twisted.internet import reactor, tksupport
 
from twisted.web import xmlrpc, server, resource
 
from rdflib import URIRef, Literal, RDF
 
from Tix import *
 
import Tix as tk
 
import pickle
 

	
 
import run_local
 
from light9.Fadable import Fadable
 
@@ -17,12 +18,13 @@ from light9.Submaster import Submasters,
 
from light9.subclient import SubClient
 
from light9 import dmxclient, showconfig, networking, prof
 
from light9.uihelpers import toplevelat, bindkeys
 
from light9.namespaces import L9
 
from light9.tkdnd import initTkdnd, dragSourceRegister
 
from light9.rdfdb.syncedgraph import SyncedGraph
 
from light9.rdfdb.patch import Patch
 

	
 
from bcf2000 import BCF2000
 

	
 
nudge_keys = {
 
    'up'   : list('qwertyui'),
 
    'down' : list('asdfghjk')
 
@@ -52,24 +54,28 @@ class SubScale(Scale, Fadable):
 
    def draw_indicator_colors(self):
 
        if self.scale_var.get() == 0:
 
            self['troughcolor'] = 'black'
 
        else:
 
            self['troughcolor'] = 'blue'
 

	
 
class SubmasterTk(Frame):
 
    def __init__(self, master, sub, current_level):
 
class SubmasterBox(Frame):
 
    """
 
    this object owns the level of the submaster (the rdf graph is the
 
    real authority)
 
    """
 
    def __init__(self, master, sub):
 
        self.sub = sub
 
        bg = sub.graph.value(sub.uri, L9.color, default='#000000')
 
        rgb = webcolors.hex_to_rgb(bg)
 
        hsv = colorsys.rgb_to_hsv(*[x/255 for x in rgb])
 
        darkBg = webcolors.rgb_to_hex(tuple([x * 255 for x in colorsys.hsv_to_rgb(
 
            hsv[0], hsv[1], .3)]))
 
        Frame.__init__(self, master, bd=1, relief='raised', bg=bg)
 
        self.name = sub.name
 
        self.slider_var = DoubleVar()
 
        self.slider_var.set(current_level)
 
        self.pauseTrace = False
 
        self.scale = SubScale(self, variable=self.slider_var, width=20)
 
        
 
        self.namelabel = Label(self, font="Arial 7", bg=darkBg,
 
            fg='white', pady=0)
 
        self.sub.graph.addHandler(self.updateName)
 
        
 
@@ -80,36 +86,90 @@ class SubmasterTk(Frame):
 
        self.scale.pack(side=BOTTOM, expand=1, fill=BOTH)
 
        bindkeys(self, "<Control-Key-l>", self.launch_subcomposer)
 

	
 
        for w in [self, self.namelabel, levellabel]:
 
            dragSourceRegister(w, 'copy', 'text/uri-list', sub.uri)
 

	
 
        self._slider_var_trace = self.slider_var.trace('w', self.slider_changed)
 

	
 
        sub.graph.addHandler(self.updateLevelFromGraph)
 

	
 
        # initial position
 
#        self.send_to_hw(sub.name, col + 1) # needs fix
 

	
 
    def cleanup(self):
 
        self.slider_var.trace_vdelete('w', self._slider_var_trace)
 
    
 
    def slider_changed(self, *args):
 
        self.scale.draw_indicator_colors()
 

	
 
        if self.pauseTrace:
 
            return
 
        self.updateGraphWithLevel(self.sub.uri, self.slider_var.get())
 
        # dispatcher.send("level changed") # in progress
 
        ###self.send_levels() # use dispatcher? 
 

	
 
        # needs fixing: plan is to use dispatcher or a method call to tell a hardware-mapping object who changed, and then it can make io if that's a current hw slider
 
        #if rowcount == self.current_row:
 
        #    self.send_to_hw(sub.name, col + 1)
 

	
 
    def updateGraphWithLevel(self, uri, level):
 
        """in our per-session graph, we maintain SubSetting objects like this:
 

	
 
           [a :SubSetting; :sub ?s; :level ?l]
 
        """
 
        ctx = URIRef("http://example.com/kc/session1")
 
        with self.sub.graph.currentState(context=ctx) as graph:
 
            adds = set([])
 
            for setting in graph.subjects(RDF.type, L9['SubSetting']):
 
                if graph.value(setting, L9['sub']) == uri:
 
                    break
 
            else:
 
                setting = URIRef("http://example.com/subsetting/%s" %
 
                                 random.randrange(999999))
 
                adds.update([
 
                    (setting, RDF.type, L9['SubSetting'], ctx),
 
                    (setting, L9['sub'], uri, ctx),
 
                    ])
 
            dels = set([])
 
            for prev in graph.objects(setting, L9['level']):
 
                dels.add((setting, L9['level'], prev, ctx))
 
            adds.add((setting, L9['level'], Literal(level), ctx))
 

	
 
            if adds != dels:
 
                self.sub.graph.patch(Patch(delQuads=dels, addQuads=adds))
 

	
 
    def updateLevelFromGraph(self):
 
        """read rdf level, write it to subbox.slider_var"""
 
        graph = self.sub.graph
 
        for setting in graph.subjects(RDF.type, L9['SubSetting']):
 
            if graph.value(setting, L9['sub']) == self.sub.uri:
 
                self.pauseTrace = True # don't bounce this update back to server
 
                try:
 
                    self.slider_var.set(graph.value(setting, L9['level']))
 
                finally:
 
                    self.pauseTrace = False
 

	
 
    def updateName(self):
 
        self.namelabel.config(text=self.sub.graph.label(self.sub.uri))
 

	
 
    def launch_subcomposer(self, *args):
 
        subprocess.Popen(["bin/subcomposer", "--no-geometry", self.name])
 

	
 
class KeyboardComposer(Frame, SubClient):
 
    def __init__(self, root, graph, current_sub_levels=None,
 
    def __init__(self, root, graph,
 
                 hw_sliders=True):
 
        Frame.__init__(self, root, bg='black')
 
        SubClient.__init__(self)
 
        self.graph = graph
 
        self.submasters = Submasters(graph)
 
        self.name_to_subtk = {}
 
        self.current_sub_levels = {}
 
        self.current_row = 0
 
        if current_sub_levels is not None:
 
            self.current_sub_levels = current_sub_levels
 
        else:
 
            try:
 
                self.current_sub_levels, self.current_row = \
 
                    pickle.load(file('.keyboardcomposer.savedlevels'))
 
            except IOError:
 
                pass
 
        self.subbox = {} # sub uri : SubmasterBox
 
        self.slider_table = {} # coords : SubmasterBox
 
        self.rows = [] # this holds Tk Frames for each row
 
        
 
        self.current_row = 0 # should come from session graph
 

	
 
        self.use_hw_sliders = hw_sliders
 
        self.connect_to_hw(hw_sliders)
 

	
 
        self.make_key_hints()
 
        self.make_buttons()
 
@@ -138,16 +198,12 @@ class KeyboardComposer(Frame, SubClient)
 
            bg='black', fg='white')
 
        self.save_stage_button.pack(side=LEFT)
 
        self.sub_name = Entry(self.buttonframe, bg='black', fg='white')
 
        self.sub_name.pack(side=LEFT)
 

	
 
    def redraw_sliders(self):
 
        self.slider_vars = {} # this holds suburi : sub Tk vars
 
        self.slider_table = {} # this holds coords:sub Tk vars
 
        self.name_to_subtk.clear() # subname : SubmasterTk instance
 

	
 
        self.graph.addHandler(self.draw_sliders)
 
        if len(self.rows):
 
            self.change_row(self.current_row)
 
            self.rows[self.current_row].focus()
 

	
 
        self.stop_frequent_update_time = 0
 
@@ -158,72 +214,54 @@ class KeyboardComposer(Frame, SubClient)
 

	
 
    def onLostSub(self, subUri):
 
        log.info("lost %s", subUri)
 
        self.graph.addHandler(self.draw_sliders)
 
    
 
    def draw_sliders(self):
 

	
 

	
 
        if hasattr(self, 'rows'):
 
            for r in self.rows:
 
                r.destroy()
 
        self.rows = [] # this holds Tk Frames for each row
 

	
 
        for r in self.rows:
 
            r.destroy()
 
        self.rows = []
 
        for b in self.subbox.values():
 
            b.cleanup()
 
        self.subbox.clear()
 
        self.slider_table.clear()
 
        
 
        self.tk_focusFollowsMouse()
 

	
 
        rowcount = -1
 
        col = 0
 
        last_group = None
 

	
 
        # there are unlikely to be any subs at startup because we
 
        # probably haven't been called back with the graph data yet
 
        
 
        #read get_all_subs then watch 'new submaster' 'lost submaster' signals
 
        withgroups = sorted((self.graph.value(sub.uri, L9['group']), 
 
                             self.graph.value(sub.uri, L9['order']), 
 
                             sub)
 
            for sub in self.submasters.get_all_subs())
 
        dispatcher.connect(self.onNewSub, "new submaster")
 
        dispatcher.connect(self.onLostSub, "lost submaster")
 
        log.info("withgroups %s", withgroups)
 

	
 
        for group, order, sub in withgroups:
 
            group = self.graph.value(sub.uri, L9['group'])
 

	
 
            if col == 0 or group != last_group: # make new row
 
            if col == 0 or group != last_group:
 
                row = self.make_row()
 
                rowcount += 1
 
                col = 0
 
            current_level = self.current_sub_levels.get(sub.name, 0)
 
            subtk = self.draw_sub_slider(row, col, sub, current_level)
 
            self.slider_table[(rowcount, col)] = subtk
 
            self.name_to_subtk[sub.name] = subtk
 

	
 
            def slider_changed(x, y, z, subtk=subtk,
 
                               col=col, sub=sub, rowcount=rowcount):
 
                subtk.scale.draw_indicator_colors()
 
                self.send_levels()
 
                if rowcount == self.current_row:
 
                    self.send_to_hw(sub.name, col + 1)
 
            subbox = SubmasterBox(row, sub)
 
            subbox.place(relx=col / 8, rely=0, relwidth=1 / 8, relheight=1)
 
            self.subbox[sub.uri] = self.slider_table[(rowcount, col)] = subbox
 

	
 
            subtk.slider_var.trace('w', slider_changed)
 
            self.setup_key_nudgers(subbox.scale)
 

	
 
            # initial position
 
            self.send_to_hw(sub.name, col + 1)
 
            col = (col + 1) % 8
 
            last_group = group
 

	
 
    def draw_sub_slider(self, row, col, sub, current_level):
 
        subtk = SubmasterTk(row, sub, current_level)
 
        subtk.place(relx=col / 8, rely=0, relwidth=1 / 8, relheight=1)
 
        self.setup_key_nudgers(subtk.scale)
 

	
 
        self.slider_vars[sub.uri] = subtk.slider_var
 
        return subtk
 

	
 
            
 
    def toggle_slider_connectedness(self):
 
        self.use_hw_sliders = not self.use_hw_sliders
 
        if self.use_hw_sliders:
 
            self.sliders.reopen()
 
        else:
 
            self.sliders.close()
 
@@ -298,46 +336,46 @@ class KeyboardComposer(Frame, SubClient)
 
        self.highlight_row(self.current_row)
 
        row = self.rows[self.current_row]
 
        self.keyhints.pack_configure(before=row)
 

	
 
        for col in range(1, 9):
 
            try:
 
                subtk = self.slider_table[(self.current_row, col - 1)]
 
                subbox = self.slider_table[(self.current_row, col - 1)]
 
                self.sliders.valueOut("button-upper%d" % col, True)
 
            except KeyError:
 
                # unfilled bottom row has holes (plus rows with incomplete
 
                # groups
 
                self.sliders.valueOut("button-upper%d" % col, False)
 
                self.sliders.valueOut("slider%d" % col, 0)
 
                continue
 
            self.send_to_hw(subtk.name, col)
 
            self.send_to_hw(subbox.name, col)
 
            
 
    def got_nudger(self, number, direction, full=0):
 
        try:
 
            subtk = self.slider_table[(self.current_row, number)]
 
            subbox = self.slider_table[(self.current_row, number)]
 
        except KeyError:
 
            return
 

	
 
        if direction == 'up':
 
            if full:
 
                subtk.scale.fade(1)
 
                subbox.scale.fade(1)
 
            else:
 
                subtk.scale.increase()
 
                subbox.scale.increase()
 
        else:
 
            if full:
 
                subtk.scale.fade(0)
 
                subbox.scale.fade(0)
 
            else:
 
                subtk.scale.decrease()
 
                subbox.scale.decrease()
 

	
 
    def hw_slider_moved(self, col, value):
 
        value = int(value * 100) / 100
 
        try:
 
            subtk = self.slider_table[(self.current_row, col)]
 
            subbox = self.slider_table[(self.current_row, col)]
 
        except KeyError:
 
            return # no slider assigned at that column
 
        subtk.scale.set(value)
 
        subbox.scale.set(value)
 

	
 
    def send_to_hw(self, subUri, hwNum):
 
        if isinstance(self.sliders, DummySliders):
 
            return
 
            
 
        v = round(127 * self.slider_vars[subUri].get())
 
@@ -364,14 +402,14 @@ class KeyboardComposer(Frame, SubClient)
 

	
 
    def unhighlight_row(self, row):
 
        row = self.rows[row]
 
        row['bg'] = 'black'
 

	
 
    def get_levels(self):
 
        return dict([(uri, slidervar.get()) 
 
            for uri, slidervar in self.slider_vars.items()])
 
        return dict([(uri, box.slider_var.get()) 
 
            for uri, box in self.subbox.items()])
 

	
 
    def get_levels_as_sub(self):
 
        scaledsubs = [self.submasters.get_sub_by_uri(sub) * level
 
            for sub, level in self.get_levels().items() if level > 0.0]
 
        maxes = sub_maxes(*scaledsubs)
 
        return maxes
 
@@ -383,22 +421,23 @@ class KeyboardComposer(Frame, SubClient)
 
        sub.temporary = 0
 
        sub.save()
 

	
 
    def save(self):
 
        pickle.dump((self.get_levels(), self.current_row),
 
                    file('.keyboardcomposer.savedlevels', 'w'))
 

	
 
    def send_frequent_updates(self):
 
        """called when we get a fade -- send events as quickly as possible"""
 
        if time.time() <= self.stop_frequent_update_time:
 
            self.send_levels()
 
            self.after(10, self.send_frequent_updates)
 

	
 
    def alltozero(self):
 
        for name, subtk in self.name_to_subtk.items():
 
            if subtk.scale.scale_var.get() != 0:
 
                subtk.scale.fade(value=0.0, length=0)
 
        for uri, subbox in self.subbox.items():
 
            if subbox.scale.scale_var.get() != 0:
 
                subbox.scale.fade(value=0.0, length=0)
 

	
 
# move to web lib
 
def postArgGetter(request):
 
    """return a function that takes arg names and returns string
 
    values. Supports args encoded in the url or in postdata. No
 
    support for repeated args."""
 
@@ -413,21 +452,21 @@ def postArgGetter(request):
 
            return fields[n].value
 
    return getArg
 

	
 

	
 
class LevelServerHttp(resource.Resource):
 
    isLeaf = True
 
    def __init__(self,name_to_subtk):
 
        self.name_to_subtk = name_to_subtk
 
    def __init__(self,name_to_subbox):
 
        self.name_to_subbox = name_to_subbox
 

	
 
    def render_POST(self, request):
 
        arg = postArgGetter(request)
 
        
 
        if request.path == '/fadesub':
 
            # fadesub?subname=scoop&level=0&secs=.2
 
            self.name_to_subtk[arg('subname')].scale.fade(
 
            self.name_to_subbox[arg('subname')].scale.fade(
 
                float(arg('level')),
 
                float(arg('secs')))
 
            return "set %s to %s" % (arg('subname'), arg('level'))
 
        else:
 
            raise NotImplementedError(repr(request))
 

	
 
@@ -474,14 +513,12 @@ class Sliders(BCF2000):
 

	
 
            kc.change_row(kc.current_row + diff)
 
            self.valueOut(name, 0)
 

	
 
if __name__ == "__main__":
 
    parser = OptionParser()
 
    parser.add_option('--nonpersistent', action="store_true",
 
                      help="don't load or save levels")
 
    parser.add_option('--no-sliders', action='store_true',
 
                      help="don't attach to hardware sliders")
 
    parser.add_option('-v', action='store_true', help="log info level")
 
    opts, args = parser.parse_args()
 

	
 
    logging.basicConfig(level=logging.INFO if opts.v else logging.WARN)
 
@@ -491,34 +528,30 @@ if __name__ == "__main__":
 

	
 
    root = Tk()
 
    initTkdnd(root.tk, 'tkdnd/trunk/')
 
    
 
    tl = toplevelat("Keyboard Composer", existingtoplevel=root)
 

	
 
    startLevels = None
 
    if opts.nonpersistent:
 
        startLevels = {}
 
    kc = KeyboardComposer(tl, graph, startLevels,
 
    kc = KeyboardComposer(tl, graph, 
 
                          hw_sliders=not opts.no_sliders)
 
    kc.pack(fill=BOTH, expand=1)
 

	
 
    for helpline in ["Bindings: B3 mute; C-l edit levels in subcomposer"]:
 
        tk.Label(root,text=helpline, font="Helvetica -12 italic",
 
                 anchor='w').pack(side='top',fill='x')
 

	
 
    import twisted.internet
 
    try:
 
        reactor.listenTCP(networking.keyboardComposer.port,
 
                          server.Site(LevelServerHttp(kc.name_to_subtk)))
 
    except twisted.internet.error.CannotListenError, e:
 
        log.warn("Can't (and won't!) start level server:")
 
        log.warn(e)
 
    if 0: # needs fixing, or maybe it's obsolete because other progs can just patch the rdf graph
 
        import twisted.internet
 
        try:
 
            reactor.listenTCP(networking.keyboardComposer.port,
 
                              server.Site(LevelServerHttp(kc.name_to_subbox)))
 
        except twisted.internet.error.CannotListenError, e:
 
            log.warn("Can't (and won't!) start level server:")
 
            log.warn(e)
 

	
 
    root.protocol('WM_DELETE_WINDOW', reactor.stop)
 
    if not opts.nonpersistent:
 
        reactor.addSystemEventTrigger('after', 'shutdown', kc.save)
 
    
 
    tksupport.install(root,ms=10)
 

	
 

	
 
#    prof.watchPoint("/usr/lib/python2.4/site-packages/rdflib-2.3.3-py2.4-linux-i686.egg/rdflib/Graph.py", 615)
 

	
bin/rdfdb
Show inline comments
 
@@ -92,13 +92,13 @@ Ways to display patches, using labels an
 
"""
 
from twisted.internet import reactor
 
import twisted.internet.error
 
import sys, optparse, logging, json, os
 
import cyclone.web, cyclone.httpclient, cyclone.websocket
 
sys.path.append(".")
 
from light9 import networking, showconfig
 
from light9 import networking, showconfig, prof
 
from rdflib import ConjunctiveGraph, URIRef, Graph
 
from light9.rdfdb.graphfile import GraphFile
 
from light9.rdfdb.patch import Patch, ALLSTMTS
 
from light9.rdfdb.rdflibpatch import patchQuads
 
from light9.rdfdb import syncedgraph
 

	
 
@@ -164,15 +164,19 @@ class Db(object):
 
        """
 
        apply this patch to the master graph then notify everyone about it
 
        """
 
        log.info("patching graph -%d +%d" % (len(p.delQuads), len(p.addQuads)))
 

	
 
        patchQuads(self.graph, p.delQuads, p.addQuads, perfect=True)
 
        
 
        senderUpdateUri = getattr(p, 'senderUpdateUri', None)
 
        self.summarizeToLog()
 
        for c in self.clients:
 
            print "send to %s? %s %s" % (c, c.updateUri, senderUpdateUri)
 
            if c.updateUri == senderUpdateUri:
 
                # this client has self-applied the patch already
 
                continue
 
            d = c.sendPatch(p)
 
            d.addErrback(self.clientErrored, c)
 
        sendToLiveClients(asJson=p.jsonRepr)
 

	
 
    def clientErrored(self, err, c):
 
        err.trap(twisted.internet.error.ConnectError)
 
@@ -290,7 +294,7 @@ if __name__ == "__main__":
 

	
 
        (r"/(jquery-1\.7\.2\.min\.js)", cyclone.web.StaticFileHandler,
 
         dict(path='lib')),
 
        
 
        ], db=db))
 
    log.info("serving on %s" % port)
 
    reactor.run()
 
    prof.run(reactor.run, profile=False)
light9/Submaster.py
Show inline comments
 
@@ -147,24 +147,24 @@ class PersistentSubmaster(Submaster):
 
        oldLevels = getattr(self, 'levels', {}).copy()
 
        self.setLevelsFromGraph()
 
        if oldLevels != self.levels:
 
            log.info("sub %s changed" % self.name)
 
        
 
    def setLevelsFromGraph(self):
 
        patchGraph = showconfig.getGraph() # going away
 
        if hasattr(self, 'levels'):
 
            self.levels.clear()
 
        else:
 
            self.levels = {}
 
        for lev in self.graph.objects(self.uri, L9['lightLevel']):
 
            chan = self.graph.value(lev, L9['channel'])
 
            val = self.graph.value(lev, L9['level'])
 
            name = patchGraph.label(chan)
 

	
 
            name = self.graph.label(chan)
 
            if not name:
 
                log.error("sub %r has channel %r with no name- leaving out that channel" % (self.name, chan))
 
                continue
 
            val = self.graph.value(lev, L9['level'])
 
            self.levels[name] = float(val)
 

	
 
    def save(self):
 
        if self.temporary:
 
            log.info("not saving temporary sub named %s",self.name)
 
            return
light9/rdfdb/patch.py
Show inline comments
 
import json
 
from rdflib import ConjunctiveGraph
 
import json, unittest
 
from rdflib import ConjunctiveGraph, URIRef
 
from light9.rdfdb.rdflibpatch import graphFromNQuad, graphFromQuads, serializeQuad
 

	
 
ALLSTMTS = (None, None, None)
 

	
 
def graphFromQuads(q):
 
    g = ConjunctiveGraph()
 
    #g.addN(q) # no effect on nquad output
 
    for s,p,o,c in q:
 
        g.get_context(c).add((s,p,o))
 
        #g.store.add((s,p,o), c) # no effect on nquad output
 
    return g
 

	
 
class Patch(object):
 
    """
 
    immutable
 
    
 
    the json representation includes the {"patch":...} wrapper
 
    """
 
    def __init__(self, jsonRepr=None, addQuads=None, delQuads=None,
 
                 addGraph=None, delGraph=None):
 
        self._jsonRepr = jsonRepr
 
        self._addQuads, self._delQuads = addQuads, delQuads
 
        self._addGraph, self._delGraph = addGraph, delGraph
 

	
 
        if self._jsonRepr is not None:
 
            body = json.loads(self._jsonRepr)
 
            self._delGraph = ConjunctiveGraph()
 
            self._delGraph.parse(data=body['patch']['deletes'], format='nquads')
 
            self._addGraph = ConjunctiveGraph()
 
            self._addGraph.parse(data=body['patch']['adds'], format='nquads')
 
            self._delGraph = graphFromNQuad(body['patch']['deletes'])
 
            self._addGraph = graphFromNQuad(body['patch']['adds'])
 
            if 'senderUpdateUri' in body:
 
                self.senderUpdateUri = body['senderUpdateUri']
 

	
 
    @property
 
    def addQuads(self):
 
        if self._addQuads is None:
 
            if self._addGraph is not None:
 
                self._addQuads = list(self._addGraph.quads(ALLSTMTS))
 
@@ -58,11 +53,42 @@ class Patch(object):
 
            self._delGraph = graphFromQuads(self._delQuads)
 
        return self._delGraph
 

	
 
    @property
 
    def jsonRepr(self):
 
        if self._jsonRepr is None:
 
            self._jsonRepr = json.dumps({"patch": {
 
                'adds':self.addGraph.serialize(format='nquads'),
 
                'deletes':self.delGraph.serialize(format='nquads'),
 
                }})
 
            self._jsonRepr = self.makeJsonRepr()
 
        return self._jsonRepr
 

	
 
    def makeJsonRepr(self, extraAttrs={}):
 
        d = {"patch" : {
 
            'adds' : serializeQuad(self.addGraph),
 
            'deletes' : serializeQuad(self.delGraph),
 
            }}
 
        if len(self.addGraph) > 0 and d['patch']['adds'].strip() == "":
 
            # this is the bug that graphFromNQuad works around
 
            raise ValueError("nquads serialization failure")
 
        if '[<' in d['patch']['adds']:
 
            raise ValueError("[< found in %s" % d['patch']['adds'])
 
        d.update(extraAttrs)
 
        return json.dumps(d)
 

	
 
    def concat(self, more):
 
        """
 
        new Patch with the result of applying this patch and the
 
        sequence of other Patches
 
        """
 
        # not working yet
 
        adds = set(self.addQuads)
 
        dels = set(self.delQuads)
 
        for p2 in more:
 
            for q in p2.delQuads:
 
                if q in adds:
 
                    adds.remove(q)
 
                else:
 
                    dels.add(q)
 
            for q in p2.addQuads:
 
                if q in dels:
 
                    dels.remove(q)
 
                else:
 
                    adds.add(q)
 
        return Patch(delQuads=dels, addQuads=adds)
light9/rdfdb/rdflibpatch.py
Show inline comments
 
"""
 
this is a proposal for a ConjunctiveGraph method in rdflib
 
"""
 
import unittest
 
from rdflib import ConjunctiveGraph, URIRef as U
 

	
 
def patchQuads(graph, deleteQuads, addQuads, perfect=False):
 
    """
 
    Delete the sequence of given quads. Then add the given quads just
 
    like addN would. If perfect is True, we'll error and not touch the
 
    graph if any of the deletes isn't in the graph or if any of the
 
@@ -26,14 +28,63 @@ def patchQuads(graph, deleteQuads, addQu
 
        addQuads = list(addQuads)
 
        for spoc in addQuads:
 
            if spoc in graph:
 
                raise ValueError("%r already in %r" % (spoc[:3], spoc[3]))
 
    graph.addN(addQuads)
 

	
 
import unittest
 
from rdflib import ConjunctiveGraph, URIRef as U
 

	
 

	
 
def graphFromQuads(q):
 
    g = ConjunctiveGraph()
 
    #g.addN(q) # no effect on nquad output
 
    for s,p,o,c in q:
 
        #g.get_context(c).add((s,p,o)) # kind of works with broken rdflib nquad serializer code
 
        g.store.add((s,p,o), c) # no effect on nquad output
 
    return g
 

	
 
def graphFromNQuad(text):
 
    """
 
    g.parse(data=self.nqOut, format='nquads')
 
    makes a graph that serializes to nothing
 
    """
 
    g1 = ConjunctiveGraph()
 
    g1.parse(data=text, format='nquads')
 
    g2 = ConjunctiveGraph()
 
    for s,p,o,c in g1.quads((None,None,None)):
 
        #g2.get_context(c).add((s,p,o))
 
        g2.store.add((s,p,o), c)
 
    #import pprint; pprint.pprint(g2.store.__dict__)
 
    return g2
 

	
 
from rdflib.plugins.serializers.nt import _xmlcharref_encode
 
def serializeQuad(g):
 
    """replacement for graph.serialize(format='nquads')"""
 
    out = ""
 
    for s,p,o,c in g.quads((None,None,None)):
 
        out += u"%s %s %s %s .\n" % (s.n3(),
 
                                p.n3(),
 
                                _xmlcharref_encode(o.n3()), 
 
                                c.n3())
 
    return out
 

	
 
class TestGraphFromQuads(unittest.TestCase):
 
    nqOut = '<http://example.com/> <http://example.com/> <http://example.com/> <http://example.com/> .\n'
 
    def testSerializes(self):
 
        n = U("http://example.com/")
 
        g = graphFromQuads([(n,n,n,n)])
 
        out = serializeQuad(g)
 
        self.assertEqual(out.strip(), self.nqOut.strip())
 

	
 
    def testNquadParserSerializes(self):
 
        g = graphFromNQuad(self.nqOut)
 
        self.assertEqual(len(g), 1)
 
        out = serializeQuad(g)
 
        self.assertEqual(out.strip(), self.nqOut.strip())
 
        
 

	
 

	
 
stmt1 = U('http://a'), U('http://b'), U('http://c'), U('http://ctx1')
 
stmt2 = U('http://a'), U('http://b'), U('http://c'), U('http://ctx2')
 
class TestPatchQuads(unittest.TestCase):
 
    def testAddsToNewContext(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [], [stmt1])
light9/rdfdb/syncedgraph.py
Show inline comments
 
from rdflib import ConjunctiveGraph, RDFS, RDF
 
from rdflib import ConjunctiveGraph, RDFS, RDF, Graph
 
import logging, cyclone.httpclient, traceback, urllib
 
from twisted.internet import reactor
 
log = logging.getLogger('syncedgraph')
 
from light9.rdfdb.patch import Patch, ALLSTMTS
 
from light9.rdfdb.rdflibpatch import patchQuads
 

	
 
def sendPatch(putUri, patch):
 
    # this will take args for sender, etc
 
    body = patch.jsonRepr
 
    log.debug("send body: %r" % body)
 
def sendPatch(putUri, patch, **kw):
 
    """
 
    kwargs will become extra attributes in the toplevel json object
 
    """
 
    body = patch.makeJsonRepr(kw)
 
    log.debug("send body: %r", body)
 
    def ok(done):
 
        if not str(done.code).startswith('2'):
 
            raise ValueError("sendPatch request failed %s: %s" % (done.code, done.body))
 
        log.debug("sendPatch finished, response: %s" % done.body)
 
        return done
 

	
 
@@ -61,13 +63,13 @@ class GraphWatchers(object):
 

	
 
    def whoCares(self, patch):
 
        """what handler functions would care about the changes in this patch?
 

	
 
        this removes the handlers that it gives you
 
        """
 
        self.dependencies()
 
        #self.dependencies()
 
        affectedSubjPreds = set([(s, p) for s, p, o, c in patch.addQuads]+
 
                                [(s, p) for s, p, o, c in patch.delQuads])
 
        affectedPredObjs = set([(p, o) for s, p, o, c in patch.addQuads]+
 
                                [(p, o) for s, p, o, c in patch.delQuads])
 
        
 
        ret = set()
 
@@ -91,12 +93,64 @@ class GraphWatchers(object):
 
        """
 
        log.info("whocares:")
 
        from pprint import pprint
 
        pprint(self._handlersSp)
 
        
 

	
 
class PatchSender(object):
 
    """
 
    SyncedGraph may generate patches faster than we can send
 
    them. This object buffers and may even collapse patches before
 
    they go the server
 
    """
 
    def __init__(self, target, myUpdateResource):
 
        self.target = target
 
        self.myUpdateResource = myUpdateResource
 
        self._patchesToSend = []
 
        self._currentSendPatchRequest = None
 

	
 
    def sendPatch(self, p):
 
        self._patchesToSend.append(p)
 
        self._continueSending()
 

	
 
    def _continueSending(self):
 
        if not self._patchesToSend or self._currentSendPatchRequest:
 
            return
 
        if len(self._patchesToSend) > 1:
 
            log.info("%s patches left to send", len(self._patchesToSend))
 
            # this is where we could concatenate little patches into a
 
            # bigger one. Often, many statements will cancel each
 
            # other out. not working yet:
 
            if 0:
 
                p = self._patchesToSend[0].concat(self._patchesToSend[1:])
 
                print "concat down to"
 
                print 'dels'
 
                for q in p.delQuads: print q
 
                print 'adds'
 
                for q in p.addQuads: print q
 
                print "----"
 
            else:
 
                p = self._patchesToSend.pop(0)
 
        else:
 
            p = self._patchesToSend.pop(0)
 
            
 
        self._currentSendPatchRequest = sendPatch(
 
            self.target, p, senderUpdateUri=self.myUpdateResource)
 
        self._currentSendPatchRequest.addCallbacks(self._sendPatchDone,
 
                                                   self._sendPatchErr)
 

	
 
    def _sendPatchDone(self, result):
 
        self._currentSendPatchRequest = None
 
        self._continueSending()
 

	
 
    def _sendPatchErr(self, e):
 
        self._currentSendPatchRequest = None
 
        log.error(e)
 
        self._continueSending()
 
        
 

	
 
class SyncedGraph(object):
 
    """
 
    graph for clients to use. Changes are synced with the master graph
 
    in the rdfdb process. 
 
    
 
    This api is like rdflib.Graph but it can also call you back when
 
@@ -108,13 +162,16 @@ class SyncedGraph(object):
 
        with your connection
 
        """
 
        _graph = self._graph = ConjunctiveGraph()
 
        self._watchers = GraphWatchers()
 
        
 
        def onPatch(p):
 
            patchQuads(_graph, p.delQuads, p.addQuads)
 
            """
 
            central server has sent us a patch
 
            """
 
            patchQuads(_graph, p.delQuads, p.addQuads, perfect=True)
 
            log.info("graph now has %s statements" % len(_graph))
 
            try:
 
                self.updateOnPatch(p)
 
            except Exception:
 
                # don't reflect this back to the server; we did
 
                # receive its patch correctly.
 
@@ -125,12 +182,14 @@ class SyncedGraph(object):
 
        ]))
 
        port = listen._realPortNumber  # what's the right call for this?
 
        self.updateResource = 'http://localhost:%s/update' % port
 
        log.info("listening on %s" % port)
 
        self.register(label)
 
        self.currentFuncs = [] # stack of addHandler callers
 
        self._sender = PatchSender('http://localhost:8051/patches',
 
                                   self.updateResource)
 

	
 
    def register(self, label):
 

	
 
        def done(x):
 
            log.debug("registered with rdfdb")
 

	
 
@@ -141,16 +200,27 @@ class SyncedGraph(object):
 
            postdata=urllib.urlencode([('clientUpdate', self.updateResource),
 
                                       ('label', label)]),
 
            ).addCallbacks(done, log.error)
 
        log.info("registering with rdfdb")
 

	
 
    def patch(self, p):
 
        """send this patch to the server and apply it to our local graph and run handlers"""
 
        # currently this has to round-trip. But I could apply the
 
        # patch here and have the server not bounce it back to me
 
        return sendPatch('http://localhost:8051/patches', p)
 
        """send this patch to the server and apply it to our local
 
        graph and run handlers"""
 
        patchQuads(self._graph, p.delQuads, p.addQuads, perfect=True)
 
        self.updateOnPatch(p)
 
        self._sender.sendPatch(p)
 

	
 
    def patchObject(self, context, subject, predicate, newObject):
 
        """send a patch which removes existing values for (s,p,*,c)
 
        and adds (s,p,newObject,c). Values in other graphs are not affected"""
 
        raise NotImplementedError
 
        existing = []
 
        
 
        self.patch(Patch(
 
            delQuads=[],
 
            addQuads=[(subject, predicate, newObject, context)]))
 

	
 
    def addHandler(self, func):
 
        """
 
        run this (idempotent) func, noting what graph values it
 
        uses. Run it again in the future if there are changes to those
 
        graph values. The func might use different values during that
 
@@ -181,12 +251,29 @@ class SyncedGraph(object):
 
        might care, and then notice what data they depend on now
 
        """
 
        for func in self._watchers.whoCares(p):
 
            # todo: forget the old handlers for this func
 
            self.addHandler(func)
 

	
 
    def currentState(self, context=None):
 
        """
 
        a graph you can read without being in an addHandler
 
        """
 
        class Mgr(object):
 
            def __enter__(self2):
 
                # this should be a readonly view of the existing graph
 
                g = Graph()
 
                for s in self._graph.triples((None, None, None), context):
 
                    g.add(s)
 
                return g
 
            
 
            def __exit__(self, type, val, tb):
 
                return
 

	
 
        return Mgr()
 

	
 
    def _getCurrentFunc(self):
 
        if not self.currentFuncs:
 
            # this may become a warning later
 
            raise ValueError("asked for graph data outside of a handler")
 

	
 
        # we add the watcher to the deepest function, since that
0 comments (0 inline, 0 general)