diff --git a/dstat_interface/db.py b/dstat_interface/db.py new file mode 100644 index 0000000000000000000000000000000000000000..3e1fda80e3c69ec7b85a85162fa412c663ad97b4 --- /dev/null +++ b/dstat_interface/db.py @@ -0,0 +1,194 @@ +import logging +from time import sleep, time +from os.path import expanduser +from uuid import uuid4 + +from BTrees.OOBTree import OOBTree + +import mr_db.mr_db as db + +logger = logging.getLogger("dstat.db") + +current_db = None + +def start_db(path=None): + global current_db + current_db = Database(data_dir=path) + +def restart_db(object, path): + logger.info("Restarting database") + global current_db + if current_db is None: + logger.info("No database running") + start_db(path=path) + else: + stop_db() + start_db(path=path) + +def stop_db(): + global current_db + if not current_db is None: + logger.info("Stopping ZEO") + current_db.disconnect() + current_db = None + db.stop_server() + else: + logger.warning("Tried to disconnect ZEO when not connected") + +class Database(object): + def __init__(self, name='dstat', data_dir=None): + if data_dir is None: + data_dir = expanduser('~/.mr_db') + self.connected = False + self.name = name + + self.db_connect(data_dir) + + self.db = self.connection.databases + + # Make sure database exists + if not self.db.has_key(name): + self.db[name] = OOBTree() + db.transaction.commit() + + def disconnect(self): + if self.connected is True: + self.connection.db.close() + else: + logger.war("Tried to disconnect ZEO when not connected") + + def db_connect(self, root_dir): + """Connects to ZEO process. Starts ZEO if not running. Returns + connection object. + """ + if root_dir == '': + root_dir = None + + while self.connected is False: + try: + self.connection = db.DbConnection(root_dir=root_dir) + self.connected = True + logger.info("Connected to ZEO server") + except db.ClientStorage.ClientDisconnected: + db.stop_server() + logger.info("Starting ZEO server -- root_dir = %s", root_dir) + db_proc = db.start_server(root_dir=root_dir) + sleep(3) + + def add_results(self, measurement_uuid=None, measurement_name=None, + experiment_uuid=None, experiment_metadata=None, + patient_id=None, + timestamp=None, + data=None): + + """Add a measurement""" + + if experiment_metadata is None: + experiment_metadata = {} + + try: + logger.info("Starting DB transaction") + db.transaction.begin() + + logger.info("Creating Experiment with id: %s", experiment_uuid) + exp_db, exp_id = self.add_experiment( + experiment_uuid=experiment_uuid, + timestamp=timestamp, + **experiment_metadata) + + logger.info("Adding Measurement with id: %s", measurement_uuid) + name = self.add_dstat_measurement(experiment=exp_db[exp_id], + measurement_uuid=measurement_uuid, + name=measurement_name, + timestamp=timestamp, + data=data) + + if patient_id is not None: + if not patient_id in self.db['patients']: + logger.info("Creating patient with id: %s", patient_id) + patient = db.Patient(pid=patient_id) + self.db['patients'][patient_id] = patient + + if not exp_id in self.db['patients'][patient_id].experiments: + logger.info("Linking experiment into patient with id: %s", + patient_id) + self.db['patients'][patient_id].link_experiment(exp_db, + exp_id) + + logger.info("Committing DB transaction") + db.transaction.commit() + + return name + + except: + logger.error("Aborting DB transaction") + db.transaction.abort() + raise + + def add_experiment(self, experiment_uuid=None, timestamp=None, **kwargs): + """Add a new experiment. Will raise KeyExistsError if id is already + in db to avoid unintended collisions. + ---- + Arguments: + id: experiment id---UUID will be generated if not supplied + timestamp: current time---Will be generated if not supplied + kwargs: additional keyword arguments that will be saved in experiment. + """ + + if experiment_uuid is None: + experiment_uuid = uuid4().hex + if timestamp is None: + timestamp = time() + + kwargs.update({'id':experiment_uuid, 'timestamp':timestamp}) + + if not experiment_uuid in self.db[self.name]: + self.db[self.name][experiment_uuid] = db.PersistentMapping() + else: + logger.info("Experiment already exists, appending") + + self.db[self.name][experiment_uuid].update(kwargs) + + return (self.db[self.name], experiment_uuid) + + def add_dstat_measurement(self, experiment, measurement_uuid=None, + data=None, timestamp=None, name=None): + + if measurement_uuid is None: + measurement_uuid = uuid4().hex + if timestamp is None: + timestamp = time() + if data is None: + data = {} + + if not 'measurements' in experiment: + experiment['measurements'] = db.PersistentMapping() + if not 'measurements_by_name' in experiment: + experiment['measurements_by_name'] = db.PersistentMapping() + + if measurement_uuid in experiment['measurements']: + raise db.KeyExistsError(measurement_uuid, + "Measurement ID already exists. Access directly to update") + + data['timestamp'] = timestamp + data['id'] = measurement_uuid + + if name is not None: + data['name'] = name + + experiment['measurements'][measurement_uuid] = data + + if name is not None: + while name in experiment['measurements_by_name']: + first, sep, last = name.rpartition('-') + + if last.isdigit(): + index = int(last) + index += 1 + name = sep.join((first,str(index))) + else: + name += '-1' + + experiment['measurements_by_name'][name] = data + + return name \ No newline at end of file diff --git a/dstat_interface/interface/db.glade b/dstat_interface/interface/db.glade new file mode 100644 index 0000000000000000000000000000000000000000..20a833aeea2b6f2974bc7fbaa4a0b10b385d0881 --- /dev/null +++ b/dstat_interface/interface/db.glade @@ -0,0 +1,219 @@ + + + + + + False + Database + center-on-parent + True + + + True + False + + + Enable Database saving + True + True + False + True + True + + + + False + False + 0 + + + + + True + False + 5 + 3 + + + + + + + + + True + False + Measurement ID + + + 1 + 2 + + + + + True + False + Experiment ID + + + + + True + True + + False + False + True + True + + + 1 + 2 + + + + + True + True + False + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + True + True + False + + False + False + True + True + + + 1 + 2 + 3 + 4 + + + + + True + False + Patient ID + + + 3 + 4 + + + + + Generate New + True + True + True + + + + 2 + 3 + + + + + True + False + DB Path + + + 4 + 5 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 4 + 5 + + + + + Apply + True + True + True + + + + 2 + 3 + 4 + 5 + + + + + + + + True + False + Measurement Name + + + 2 + 3 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 2 + 3 + + + + + True + True + 1 + + + + + + diff --git a/dstat_interface/interface/db.py b/dstat_interface/interface/db.py new file mode 100755 index 0000000000000000000000000000000000000000..94125c0bc490ca298ac3a9bacc86f3af77c914e8 --- /dev/null +++ b/dstat_interface/interface/db.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# DStat Interface - An interface for the open hardware DStat potentiostat +# Copyright (C) 2014 Michael D. M. Dryden - +# Wheeler Microfluidics Laboratory +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import logging +from uuid import uuid4 + +import gtk +import gobject + +logger = logging.getLogger('dstat.interface.db') + +class DB_Window(gobject.GObject): + __gsignals__ = { + 'db-reset' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,) + ) + } + def __init__(self): + gobject.GObject.__init__(self) + + self.builder = gtk.Builder() + self.builder.add_from_file('interface/db.glade') + self.builder.connect_signals(self) + + self.window = self.builder.get_object('db_window') + # Make sure window isn't destroyed if checkbox hit. + self.window.connect('delete-event', self.on_delete_event) + self.db_control_table = self.builder.get_object('db_control_table') + + ui_keys = ['exp_id_entry', + 'measure_id_entry', + 'patient_id_entry', + 'measure_name_entry', + 'db_path_entry', + 'db_apply_button', + 'exp_id_autogen_button', + 'db_enable_checkbutton' + ] + self.persistent_keys = ['db_path_entry','db_enable_checkbutton'] + self.ui = {} + for i in ui_keys: + self.ui[i] = self.builder.get_object(i) + + self.metadata_map = [('experiment_uuid', 'exp_id_entry'), + ('patient_id', 'patient_id_entry'), + ('name', 'measure_name_entry')] + + self._params = {} + + @property + def persistent_params(self): + """Dict of parameters that should be saved.""" + self._get_params() + return {k:self._params[k] for k in self.persistent_keys} + + @property + def params(self): + """Dict of parameters.""" + logger.info("getter") + self._get_params() + logger.info("params: %s", self._params) + return self._params + + def _get_params(self): + """Updates self._params from UI.""" + for i in self.ui: + if i.endswith('checkbutton'): + self._params[i] = self.ui[i].get_active() + elif i.endswith('entry'): + text = self.ui[i].get_text() + logger.info("%s=%s",i,text) + if text == "": + text = None + self._params[i] = text + + @params.setter + def params(self, params): + logger.info("Setter Params: %s", params) + if self._params is {}: + self._params = dict.fromkeys(self.ui.keys()) + + for i in self._params: + if i in params: + self._params[i] = params[i] + self._set_params() + logger.info("params: %s", self._params) + + def _set_params(self): + """Updates UI with new parameters.""" + for i in self.ui: + if i.endswith('checkbutton'): + self.ui[i].set_active(self._params[i]) + elif i.endswith('entry'): + if self._params[i] is not None: + self.ui[i].set_text(self._params[i]) + else: + self.ui[i].set_text("") + + def update_from_metadata(self, metadata): + params = {k : metadata[j] for j, k in self.metadata_map} + # if j in metadata} + self.params = params + + def show(self): + self.window.show_all() + self.on_db_enable_checkbutton_toggled() + + def on_db_enable_checkbutton_toggled(self, *args): + if self.ui['db_enable_checkbutton'].get_active(): + self.db_control_table.show() + else: + self.db_control_table.hide() + + def on_exp_id_autogen_button_clicked(self, *args): + self.ui['exp_id_entry'].set_text(uuid4().hex) + + def on_db_apply_button_clicked(self, widget=None, *data): + self.emit('db-reset', self.params['db_path_entry']) + + def on_delete_event(self, widget=None, *data): + widget.hide() + return True + +gobject.type_register(DB_Window) \ No newline at end of file