class Sketch (EventEmitter): """ This object is the representation of the persistent sketch, stored in the database and in the event store. """ # We will use a file based event store for now, then migrate to EventStore later. db = None dataDir = None @classmethod def createId (cls): id = str(uuid.uuid4()) created_date = now() # Check that the UUID is not already used. I am sure this is probably unnecessary by definition... #rows = cls.db.runQuery("SELECT guid FROM sketches WHERE guid = ?", (id,)) #if len(rows) == 0: # break # Insert the new sketch into the DB def _done (result): return id return cls.db.runOperation(""" INSERT INTO sketches (guid, title, user_id, created_date, modified_date, deleted) VALUES (?, ?, ?, ?, ?, 0) """, (id, "New Sketch", 1, created_date, created_date) ).addCallback(_done) @classmethod def exists (cls, id): d = cls.db.runQuery("SELECT guid FROM sketches WHERE guid = ?", (id,)) d.addCallback(lambda r: len(r) > 0) return d @classmethod def delete (cls, id): return cls.db.runOperation("UPDATE sketches SET deleted = 1 WHERE guid = ?", (id, )) @classmethod def restore (cls, id): return cls.db.runOperation("UPDATE sketches SET deleted = 0 WHERE guid = ?", (id, )) def __init__ (self, id): self.id = id self.title = "" self.user_id = 1 self.loaded = False self.workspace = Workspace() self.experiment = None self.subscribers = {} self._eventIndex = 0 self._sketchDir = FilePath(self.dataDir).child(id) if not self._sketchDir.exists(): self._sketchDir.createDirectory() eventFile = self._sketchDir.child("events.log") if not eventFile.exists(): eventFile.create() self._eventsLog = eventFile.open('a') def load (self): return self._loadFrom(self.id) def copyFrom (self, id): return self._loadFrom(id, copy = True) @defer.inlineCallbacks def _loadFrom (self, id, copy = False): sketch = yield self.db.runQuery( "SELECT title FROM sketches WHERE guid = ?", (id, ) ) if len(sketch) == 0: raise Error("Sketch %s not found." % id) self.title = sketch[0][0] self.loaded = True # Find the most recent snapshot file try: sketchDir = FilePath(self.dataDir).child(id) max_snap = max(map( lambda fp: int( os.path.splitext(fp.basename())[0].split('.')[1] ), sketchDir.globChildren('snapshot.*.log') )) log.msg( "Found snapshot {:d} for sketch {:s}".format( max_snap, id ) ) except ValueError: self._eventIndex = 0 self._snapEventIndex = 0 else: snapFile = sketchDir.child('snapshot.' + str(max_snap) + '.log') if max_snap > 0: snapshot = yield threads.deferToThread(snapFile.getContent) events = map( json.loads, filter(lambda e: e.strip() != "", snapshot.split("\n")) ) self.workspace.fromEvents(events) if copy: self._eventIndex = len(events) self._snapEventIndex = 0 else: self._eventIndex = max_snap self._snapEventIndex = max_snap # Rename if a copy if copy: self.rename(self.title + " Copy") def close (self): log.msg("Closing sketch {:s}".format(self.id)) # If anything has changed... if self._eventIndex > self._snapEventIndex: # Write a snapshot snapFile = self._sketchDir.child("snapshot." + str(self._eventIndex) + ".log") if not snapFile.exists(): snapFile.create() with snapFile.open('w') as fp: fp.write("\n".join(map(json.dumps, self.workspace.toEvents()))) # Set the modified date self.db.runOperation(''' UPDATE sketches SET modified_date = ? WHERE guid = ? ''', (now(), self.id)) # Close the events log self._eventsLog.close() self.emit("closed") def rename (self, title): self._writeEvent("RenameSketch", { "from": self.title, "to": title }) self.db.runOperation("UPDATE sketches SET title = ? WHERE guid = ?", (title, self.id)) self.title = title # # Subscribers # def subscribe (self, subscriber, notifyFn): self.subscribers[subscriber] = notifyFn def unsubscribe (self, subscriber): if subscriber in self.subscribers: del self.subscribers[subscriber] if len(self.subscribers) is 0: self.close() def notifySubscribers (self, protocol, topic, payload, source = None): for subscriber, notifyFn in self.subscribers.iteritems(): if subscriber is not source: notifyFn(protocol, topic, payload) # # Experiment # def runExperiment (self, context): if self.experiment is not None: raise ExperimentAlreadyRunning self.experiment = Experiment(self) self.notifySubscribers("experiment", "state-started", { "sketch": self.id, "experiment": self.experiment.id }, self.experiment) def _done (result): self.notifySubscribers("experiment", "state-stopped", { "sketch": self.id, "experiment": self.experiment.id }, self.experiment) self.experiment = None def _cancelled (failure): f = failure.trap(Aborted, Cancelled) if f is not Aborted: _done(failure) else: _error(Aborted("Manual stop")) def _error (failure): log.err("Sketch.runExperiment: Received error message") log.err(failure) try: errorMessage = failure.getErrorMessage() except AttributeError: errorMessage = str(failure) self.notifySubscribers("experiment", "state-error", { "sketch": self.id, "experiment": self.experiment.id, "error": errorMessage }, self.experiment) self.experiment = None d = self.experiment.run() d.addCallbacks(_done, _cancelled) d.addErrback(_error) def pauseExperiment (self, context): if self.experiment is None: raise NoExperimentRunning def _notify (result): self.notifySubscribers("experiment", "state-paused", { "sketch": self.id, "experiment": self.experiment.id }, self.experiment) def _error (failure): self.notifySubscribers("experiment", "state-error", { "sketch": self.id, "experiment": self.experiment.id, "error": str(failure) }, self.experiment) self.experiment.pause().addCallbacks(_notify, _error) def resumeExperiment (self, context): if self.experiment is None: raise NoExperimentRunning def _notify (result): self.notifySubscribers("experiment", "state-resumed", { "sketch": self.id, "experiment": self.experiment.id }, self.experiment) def _error (failure): self.notifySubscribers("experiment", "state-error", { "sketch": self.id, "experiment": self.experiment.id, "error": str(failure) }, self.experiment) self.experiment.resume().addCallbacks(_notify, _error) def stopExperiment (self, context): if self.experiment is None: raise NoExperimentRunning self.experiment.stop() # # Operations # def renameSketch (self, payload, context): self.rename(payload['title']) self.notifySubscribers("sketch", "renamed", { "event": self._eventIndex, "title": payload['title'] }, context) def processEvent (self, event, context): event.apply(self.workspace) eid = self._writeEvent(event.type, event.values) self.notifySubscribers(event.jsProtocol, event.jsTopic, event.valuesWithEventId(eid), context) def runtimeCancelBlock (self, id): try: block = self.workspace.getBlock(id) except KeyError: return try: block.cancel() except NotRunning: pass def _writeEvent (self, eventType, data): if not self.loaded: raise Error("Sketch is not loaded") self._eventIndex += 1 event = { "index": self._eventIndex, "type": eventType, "data": data } self._eventsLog.write(json.dumps(event) + "\n") return self._eventIndex