diff --git a/README.md b/README.md index b059458..b6a5035 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,32 @@ # lixdc User interface for data collection at LIX beam line. + + +## lixdc configuration + +Lixdc requires the following configuration information: + +``` +amostra_host: localhost +amostra_port: 7772 +base_path: /Users/hugoslepicka/data +``` + + +This configuration information can live in up to four different places, as +defined in the docstring of the `load_configuration` function in + `lixdc/conf.py`. In order of increasing precedence: + +1. The conda environment + - CONDA_ENV/etc/{name}.yaml (if CONDA_ETC_env is defined) +1. At the system level + - /etc/{name}.yml +1. In the user's home directory + - ~/.config/{name}/connection.yml +1. Environmental variables + - {PREFIX}_{FIELD} + +where + + - {name} is lixdc + - {PREFIX} is LIXDC and {FIELD} is one of {amostra_host, amostra_port, base_path} diff --git a/lixdc/conf.py b/lixdc/conf.py new file mode 100644 index 0000000..176b694 --- /dev/null +++ b/lixdc/conf.py @@ -0,0 +1,54 @@ +# no yaml, just python or command line for now +import os +import yaml +import logging + +logger = logging.getLogger(__name__) + + +def load_configuration(name, prefix, fields, allow_missing=False): + """ + Load configuration data form a cascading series of locations. + The precedence order is (highest priority last): + 1. CONDA_ENV/etc/{name}.yaml (if CONDA_ETC_ env is defined) + 2. /etc/{name}.yml + 3. ~/.config/{name}/connection.yml + 4. reading {PREFIX}_{FIELD} environmental variables + Parameters + ---------- + name : str + The expected base-name of the configuration files + prefix : str + The prefix when looking for environmental variables + fields : iterable of strings + The required configuration fields + Returns + ------ + conf : dict + Dictionary keyed on ``fields`` with the values extracted + """ + filenames = [os.path.join('/etc', name + '.yml'), + os.path.join(os.path.expanduser('~'), '.config', + name, 'connection.yml'), + ] + if 'CONDA_ETC_' in os.environ: + filenames.insert(0, os.path.join(os.environ['CONDA_ETC_'], + name + '.yml')) + + config = {} + for filename in filenames: + if os.path.isfile(filename): + with open(filename) as f: + config.update(yaml.load(f)) + logger.debug("Using db connection specified in config file. \n%r", + config) + + for field in fields: + var_name = prefix + '_' + field.upper().replace(' ', '_') + config[field] = os.environ.get(var_name, config.get(field, None)) + + missing = [k for k, v in config.items() if v is None] + if not allow_missing and missing: + raise KeyError("The configuration field(s) {0} were not found in any " + "file or environmental variable.".format(missing)) + return config diff --git a/lixdc/db/request.py b/lixdc/db/request.py index 550ea23..9cd3a97 100644 --- a/lixdc/db/request.py +++ b/lixdc/db/request.py @@ -1,8 +1,18 @@ import collections import pandas import amostra.client.commands as acc +from lixdc.conf import load_configuration + +config_params = {k: v for k, v in load_configuration('lixdc', 'LIXDC', + [ + 'amostra_host', + 'amostra_port', + ] + ).items() if v is not None} + +host, port = config_params['amostra_host'], config_params['amostra_port'] +request_ref = acc.RequestReference(host=host, port=port) -request_ref = acc.RequestReference() def upsert_request(req_info): if 'uid' in req_info: @@ -16,9 +26,9 @@ def upsert_request(req_info): def find_requests(**kwargs): return list(request_ref.find(**kwargs)) -def find_request_by_barcode(owner, project, beamline_id, barcode): +def find_request_by_barcode(owner, proposal_id, beamline_id, barcode): try: - req_info = list(container_ref.find(owner=owner, project=project, + req_info = list(container_ref.find(owner=owner, proposal_id=proposal_id, beamline_id=beamline_id, container_barcode=barcode))[0] except IndexError: diff --git a/lixdc/db/sample.py b/lixdc/db/sample.py index 8e0a2c4..a4cc9bb 100644 --- a/lixdc/db/sample.py +++ b/lixdc/db/sample.py @@ -3,6 +3,7 @@ import pandas import amostra.client.commands as acc import lixdc +from lixdc.conf import load_configuration path = os.path.dirname(lixdc.__file__)+"/" @@ -20,8 +21,16 @@ "image": "{}12cells.png".format(path)} } -container_ref = acc.ContainerReference() -sample_ref = acc.SampleReference() +config_params = {k: v for k, v in load_configuration('lixdc', 'LIXDC', + [ + 'amostra_host', + 'amostra_port', + ] + ).items() if v is not None} + +host, port = config_params['amostra_host'], config_params['amostra_port'] +container_ref = acc.ContainerReference(host=host, port=port) +sample_ref = acc.SampleReference(host=host, port=port) def get_container_type_by_id(cont_id): @@ -34,7 +43,6 @@ def upsert_container(cont_info): if 'uid' in cont_info: cont = cont_info['uid'] q = {'uid': cont_info.pop('uid', '')} - print('Update Container with: ', cont_info) container_ref.update(q, cont_info) else: cont = container_ref.create(**cont_info) @@ -51,7 +59,6 @@ def upsert_sample_list(samples): result.append(q['uid']) else: s.pop('uid', '') - print('Will Create sample: ', s) result.append(sample_ref.create(**s)) return result @@ -61,9 +68,10 @@ def find_containers(**kwargs): return list(container_ref.find(**kwargs)) -def find_container_by_barcode(owner, project, beamline_id, barcode, fill=True): +def find_container_by_barcode(owner, proposal_id, beamline_id, barcode, fill=True): + try: - cont_info = list(container_ref.find(owner=owner, project=project, + cont_info = list(container_ref.find(owner=owner, proposal_id=proposal_id, beamline_id=beamline_id, barcode=barcode))[0] except IndexError: diff --git a/lixdc/gui.py b/lixdc/gui.py index 37610f8..2899057 100644 --- a/lixdc/gui.py +++ b/lixdc/gui.py @@ -7,27 +7,36 @@ from .state.general import State from .widgets.main import MainWindow from .widgets.login import Login +from .conf import load_configuration +config_params = {k: v for k, v in load_configuration('lixdc', 'LIXDC', + [ + 'amostra_host', + 'amostra_port', + 'base_path' + ] + ).items() if v is not None} def run(): app_state = State() + app_state.configs = config_params app = QtGui.QApplication(sys.argv) #print("Styles: ", QtGui.QStyleFactory.keys()) app.setStyle(QtGui.QStyleFactory.create("Cleanlooks")) ### TURNS ON LOGIN SCREEN login = Login(app_state=app_state) if login.exec_() == QtGui.QDialog.Accepted: - #if True: - print("After Login App State: ", app_state) main = MainWindow(app_state=app_state) sys.exit(app.exec_()) -def run_ipython(username, project): - if username == '' or project == '': - raise Exception('Illegal username or project. Please check.') +def run_ipython(username, proposal, run): + if username == '' or proposal == '' or run == '': + raise Exception('Illegal username, proposal or run. Please check.') app_state = State() + app_state.configs = config_params app_state.user = username - app_state.project = project + app_state.proposal_id = proposal + app_state.run_id = run app_state.login_timestamp = time.time() params = {'app_state': app_state} main = create_window(MainWindow, **params) diff --git a/lixdc/state/general.py b/lixdc/state/general.py index a7a7ce5..0615fe9 100644 --- a/lixdc/state/general.py +++ b/lixdc/state/general.py @@ -1,13 +1,19 @@ class State(): def __init__(self): self.user = None - self.project = None + self.proposal_id = None + self.run_id = None self.beamline_id = "LIX" self.login_timestamp = None + self.configs = None def __str__(self): - return "User: {}, Project: {}, Beamline: {}, Login Timestamp: {}".format(self.user, self.project, self.beamline_id, self.login_timestamp) + return "User: {}, Proposal: {}, Beamline: {}, Login Timestamp: {}".format(self.user, self.proposal_id, self.beamline_id, self.login_timestamp) def get_default_fields(self): - return {'owner': self.user, 'project': self.project, 'beamline_id': - self.beamline_id} + return {'owner': self.user, 'proposal_id': self.proposal_id, 'beamline_id': + self.beamline_id, 'run_id': self.run_id} + + def get_user_path(self): + return "{}/{}/{}/".format(self.configs['base_path'], self.proposal_id, + self.run_id) diff --git a/lixdc/utils.py b/lixdc/utils.py index 32f818f..f29dc1f 100644 --- a/lixdc/utils.py +++ b/lixdc/utils.py @@ -1,3 +1,4 @@ +import os from PyQt4 import QtGui, QtCore ANSWER_OK = QtGui.QMessageBox.Ok @@ -32,3 +33,21 @@ def show_information(text, informative, title): msg_box.setStandardButtons(QtGui.QMessageBox.Ok) ret = msg_box.exec_() +# xf16id - 5008, lix - 3009 +def makedirs(path, mode=0o777, owner_uid=5008, group=3009): + '''Recursively make directories and set permissions''' + # Permissions not working with os.makedirs - + # See: http://stackoverflow.com/questions/5231901 + if not path or os.path.exists(path): + return [] + + head, tail = os.path.split(path) + ret = makedirs(head, mode) + try: + os.mkdir(path) + except OSError as ex: + if 'File exists' not in str(ex): + raise + os.chmod(path, mode) + ret.append(path) + return ret diff --git a/lixdc/widgets/finder.py b/lixdc/widgets/finder.py index 32f2856..4fb417d 100644 --- a/lixdc/widgets/finder.py +++ b/lixdc/widgets/finder.py @@ -106,7 +106,7 @@ def filter_containers(self): params = dict() params['owner'] = self.app_state.user - params['project'] = self.app_state.project + params['proposal_id'] = self.app_state.proposal_id params['beamline_id'] = self.app_state.beamline_id if self.cmb_type.currentText() != '': diff --git a/lixdc/widgets/login.py b/lixdc/widgets/login.py index c3a61a7..500f6d0 100644 --- a/lixdc/widgets/login.py +++ b/lixdc/widgets/login.py @@ -21,10 +21,16 @@ def init_ui(self): self.textPass.setEchoMode(QtGui.QLineEdit.Password) self.textPass.setText("lix") - self.labelProject = QtGui.QLabel(self) - self.labelProject.setText("Project: ") - self.textProject = QtGui.QLineEdit(self) - self.textProject.setText("") + self.labelProposal = QtGui.QLabel(self) + self.labelProposal.setText("Proposal ID: ") + self.textProposal = QtGui.QLineEdit(self) + self.textProposal.setText("") + + self.labelRun = QtGui.QLabel(self) + self.labelRun.setText("Run #: ") + self.textRun = QtGui.QLineEdit(self) + self.textRun.setText("{}".format( + time.strftime("%Y%m%d"))) self.buttonLogin = QtGui.QPushButton('Login', self) self.buttonLogin.clicked.connect(self.handle_login) @@ -33,20 +39,22 @@ def init_ui(self): form_layout = QtGui.QFormLayout() form_layout.addRow(self.labelName, self.textName) form_layout.addRow(self.labelPass, self.textPass) - form_layout.addRow(self.labelProject, self.textProject) + form_layout.addRow(self.labelProposal, self.textProposal) + form_layout.addRow(self.labelRun, self.textRun) vbox_layout.addLayout(form_layout) vbox_layout.addWidget(self.buttonLogin) def handle_login(self): # TODO: Implement real authentication - if self.textProject.text() == '': - QtGui.QMessageBox.warning(self, 'Error', 'Project is required.') + if self.textProposal.text() == '' or self.textRun.text() == '': + QtGui.QMessageBox.warning(self, 'Error', 'Proposal and Run Number are required.') return if self.textName.text() != '' and self.textPass.text() == 'lix': self.app_state.user = self.textName.text() - self.app_state.project = self.textProject.text() self.app_state.login_timestamp = time.time() + self.app_state.proposal_id = self.textProposal.text() + self.app_state.run_id = self.textRun.text() self.accept() else: QtGui.QMessageBox.warning(self, 'Error', 'Bad user or password') diff --git a/lixdc/widgets/main.py b/lixdc/widgets/main.py index e97af69..e2f87e5 100644 --- a/lixdc/widgets/main.py +++ b/lixdc/widgets/main.py @@ -20,12 +20,12 @@ def init_ui(self): self.setStatusBar(self.status_bar) self.lbl_user_data = QtGui.QLabel() - user_data_str = "Logged in at: {} as {} | Project: {}" + user_data_str = "Logged in at: {} as {} | Proposal: {}" fmt_date = datetime.fromtimestamp(self.app_state.login_timestamp) fmt_date = fmt_date.strftime("%D %H:%M:%S") self.lbl_user_data.text = user_data_str.format(fmt_date, self.app_state.user, - self.app_state.project) + self.app_state.proposal_id) self.status_bar.addWidget(self.lbl_user_data,1) diff --git a/lixdc/widgets/sample_manager.py b/lixdc/widgets/sample_manager.py index 259b001..96f1798 100644 --- a/lixdc/widgets/sample_manager.py +++ b/lixdc/widgets/sample_manager.py @@ -139,7 +139,7 @@ def filter_containers(self): params = dict() params['owner'] = self.app_state.user - params['project'] = self.app_state.project + params['proposal_id'] = self.app_state.proposal_id params['beamline_id'] = self.app_state.beamline_id if self.cmb_type.currentText() != '': @@ -445,7 +445,7 @@ def read_import_file(self, fname): plate_info = { "uid": None, "owner": self.app_state.user, - "project": self.app_state.project, + "proposal_id": self.app_state.proposal_id, "beamline_id": self.app_state.beamline_id, "kind": kind, "name": name, @@ -460,7 +460,7 @@ def read_import_file(self, fname): s_temperature = line[1][9] sample_info = { "uid": None, - "project": self.app_state.project, + "proposal_id": self.app_state.proposal_id, "beamline_id": self.app_state.beamline_id, "owner": self.app_state.user, "name": s_name, diff --git a/lixdc/widgets/tab_hplc_setup.py b/lixdc/widgets/tab_hplc_setup.py index 5d1d515..ccafa12 100644 --- a/lixdc/widgets/tab_hplc_setup.py +++ b/lixdc/widgets/tab_hplc_setup.py @@ -185,7 +185,7 @@ def gen_batch(self): self.create_request() fname = self.compose_file() utils.show_information('Batch file created.', - 'Now copy the file located at: {} to the HPLC computer and execute the batch.'.format(fname), + 'Now copy the file located at:\n\t{}\n\nTo the HPLC computer and execute the batch.'.format(fname), 'Success') def create_request(self): @@ -220,7 +220,9 @@ def compose_file(self): plate['name']) prefix = 'TBD' sep = '\r\n' - with open(fname, 'w') as f: + utils.makedirs(self.app_state.get_user_path()) + full_file_path = "{}{}".format(self.app_state.get_user_path(),fname) + with open(full_file_path, 'w') as f: f.write(''+sep) f.write('[Header]'+sep) f.write('Batch File Name\t{}\{}'.format(batch_path, fname)+sep) @@ -381,4 +383,4 @@ def compose_file(self): multi_injection, barcode, sampler_file, conc_overrides ) ) - return fname + return full_file_path diff --git a/lixdc/widgets/tab_windowless_setup.py b/lixdc/widgets/tab_windowless_setup.py index 289e813..dc690ac 100644 --- a/lixdc/widgets/tab_windowless_setup.py +++ b/lixdc/widgets/tab_windowless_setup.py @@ -162,7 +162,7 @@ def _handle_load_dock_click(self, idx): barcode, ok = QtGui.QInputDialog.getText(self, 'Loading plate', 'Enter your plate barcode:') if ok: barcode = barcode.zfill(13) - plate_info = db_sample.find_plate_by_barcode(state_general.owner, state_general.project, state_general.beamline_id, barcode, fill=True) + plate_info = db_sample.find_plate_by_barcode(state_general.owner, state_general.proposal_id, state_general.beamline_id, barcode, fill=True) if plate_info is None: QtGui.QMessageBox.critical(self, "Not Found", "Plate not found with barcode: {}!".format(barcode)) else: @@ -174,7 +174,7 @@ def _handle_import_dock_click(self, idx): print("Importing new plate to Dock Station #", idx) fname = QtGui.QFileDialog.getOpenFileName(self, 'Select File to import', '~') if fname != "": - plate_info = db_sample.import_plate_from_excel(fname, state_general.owner, state_general.project, state_general.beamline_id, "96wp") + plate_info = db_sample.import_plate_from_excel(fname, state_general.owner, state_general.proposal_id, state_general.beamline_id, "96wp") self._update_screen_with_plate_info(plate_info)