diff --git a/setup.py b/setup.py index 4d32439..e551663 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name = 'appia', - version = '7.1.2', + version = '7.2.0', author = 'Rich Posert', author_email = 'posert@ohsu.edu', description = 'Chromatography processing made easy', @@ -16,14 +16,13 @@ 'License :: OSI Approved :: MIT License' ], package_dir = {'': 'src'}, - package_data={'appia': ['processors/flow_rates.json', 'plotters/manual_plot_FPLC.R', 'plotters/manual_plot_HPLC.R']}, + package_data={'appia': ['plotters/manual_plot_FPLC.R', 'plotters/manual_plot_HPLC.R']}, include_package_data=True, packages = setuptools.find_packages(where = 'src'), python_requires = ">=3.6", install_requires = [ 'couchdb', 'pandas', - 'slack', 'plotly', 'kaleido' ], diff --git a/src/appia/appia.py b/src/appia/appia.py index ad8997c..5fece71 100755 --- a/src/appia/appia.py +++ b/src/appia/appia.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import argparse import logging -import sys from appia.parsers.process_parser import parser as process_parser from appia.parsers.database_parser import parser as db_parser diff --git a/src/appia/parsers/database_parser.py b/src/appia/parsers/database_parser.py index 94f085e..7cf7392 100644 --- a/src/appia/parsers/database_parser.py +++ b/src/appia/parsers/database_parser.py @@ -1,13 +1,10 @@ import argparse import os -from appia.processors.database import Database, Config from appia.processors.core import three_column_print def main(args): - if args.config == 'env': - db = Database(Config()) - else: - db = Database(Config(args.config)) + + from appia.processors.database import db if args.list or args.check_versions: list = db.update_experiment_list() @@ -50,11 +47,6 @@ def main(args): add_help=False ) parser.set_defaults(func = main) -parser.add_argument( - 'config', - help = 'Config JSON file', - type = str -) parser.add_argument( '-l', '--list', help = 'Print list of all experiments in database', diff --git a/src/appia/parsers/process_parser.py b/src/appia/parsers/process_parser.py index d35603e..2ab5efc 100644 --- a/src/appia/parsers/process_parser.py +++ b/src/appia/parsers/process_parser.py @@ -4,11 +4,11 @@ import logging import shutil from appia.processors import hplc, fplc, experiment, core -from appia.processors.database import Database, Config from appia.plotters import auto_plot from appia.processors.gui import user_input def main(args): + from appia.processors.database import db file_list = core.get_files(args.files) logging.debug(file_list) @@ -146,31 +146,10 @@ def main(args): os.path.join(out_dir, f'{exp.id}_manual-plot-FPLC.R') ) - if args.config: - if args.config == 'env': - db = Database(Config()) - else: - db = Database(Config(args.config)) - + if args.database: exp.reduce_hplc(args.reduce) db.upload_experiment(exp, args.overwrite) - if args.post_to_slack: - config = Config(args.post_to_slack) - - if config.slack: - from processors import slackbot - - client = slackbot.get_client(config) - - if client is not None: - slackbot.send_graphs( - config, - client, - os.path.join(out_dir, 'fsec_traces.pdf') - ) - - parser = argparse.ArgumentParser( description = 'Process chromatography data', add_help = False @@ -205,12 +184,6 @@ def main(args): nargs = '?', const = 'plotters' ) -file_io.add_argument( - '-s', '--post-to-slack', - help = "Send completed plots to Slack. Need a config JSON with slack token and channel.", - nargs = '?', - const = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'config.json') -) process_args = parser.add_argument_group('Processing Options') process_args.add_argument( @@ -260,10 +233,8 @@ def main(args): ) web_up.add_argument( '-d', '--database', - help = '''Upload experiment to couchdb. Optionally, provide config file location. Default config location is "config.json" in appia directory. Enter "env" to use environment variables instead.''', - dest = 'config', - nargs = '?', - const = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'config.json') + help = '''Upload experiment to couchdb. Must have set your parameters using `appia database`.''', + action = 'store_true' ) web_up.add_argument( '--overwrite', diff --git a/src/appia/parsers/user_settings.py b/src/appia/parsers/user_settings.py index 6be7857..6014f1e 100644 --- a/src/appia/parsers/user_settings.py +++ b/src/appia/parsers/user_settings.py @@ -19,19 +19,24 @@ def save_settings(self): with open(self.settings_path, 'w') as f: json.dump(self._user_settings, f) - @property - def flow_rates(self) -> dict: + def access_private_property(self, propname:str, not_found = None): try: - return self._user_settings['flow_rates'] + return self._user_settings[propname] except KeyError: - return {} + return not_found + + # Flow rates -------------------------------------------------------- + + @property + def flow_rates(self) -> dict: + return self.access_private_property('flow_rates', {}) @flow_rates.setter def flow_rates(self, new_flow_rates:dict): if isinstance(new_flow_rates, dict): self._user_settings['flow_rates'] = new_flow_rates else: - raise TypeError + raise TypeError(f'Flow rates must be a dict, not a {type(new_flow_rates)}') def delete_flow_rate(self, method_name): del self._user_settings['flow_rates'][method_name] @@ -49,4 +54,53 @@ def check_flow_rate(self, method_name:str): elif len(matches) > 1: logging.warning(f'More than one match for {method_name}') - return None \ No newline at end of file + return None + + # Database ------------------------------------------------------------- + + @property + def database_host(self): + return self.access_private_property('database_host') + + @database_host.setter + def database_host(self, hostname:str): + if isinstance(hostname, str): + self._user_settings['database_host'] = hostname + else: + raise TypeError(f'Hostname must be a string, not a {type(hostname)}') + + @property + def database_port(self): + return self.access_private_property('database_port', '5984') + + @database_port.setter + def database_port(self, port:int): + if isinstance(port, int): + self._user_settings['database_port'] = str(port) + else: + raise TypeError(f'Port must be an integer, not a {type(port)}') + + @property + def database_user(self): + return self.access_private_property('database_username') + + @database_user.setter + def database_user(self, username:str): + if isinstance(username, str): + self._user_settings['database_username'] = username + else: + raise TypeError(f'Username must be a strong, not a {type(username)}') + + @property + def database_password(self): + return self.access_private_property('database_password') + + @database_password.setter + def database_password(self, password:str): + if isinstance(password, str): + self._user_settings['database_password'] = password + else: + raise TypeError(f'Password must be a string, not a {type(password)}') + + +appia_settings = AppiaSettings() \ No newline at end of file diff --git a/src/appia/parsers/utilities_parser.py b/src/appia/parsers/utilities_parser.py index 39882fa..6e639d7 100644 --- a/src/appia/parsers/utilities_parser.py +++ b/src/appia/parsers/utilities_parser.py @@ -2,11 +2,9 @@ import os import shutil import logging -import json -from appia.parsers.user_settings import AppiaSettings +from appia.parsers.user_settings import appia_settings def main(args): - user_settings = AppiaSettings() if args.copy_manual is not None: script_location = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) @@ -30,19 +28,42 @@ def main(args): logging.error('Bad flow rate. Give as --flow-rate {name} {mL/min number}') raise TypeError - user_settings.update_flow_rates(new_flow_rates) - user_settings.save_settings() + appia_settings.update_flow_rates(new_flow_rates) + appia_settings.save_settings() if args.list_flow_rates: print('User specified flow rates:') - for method, fr in user_settings.flow_rates.items(): + for method, fr in appia_settings.flow_rates.items(): print(f' {method + ":":>20} {fr}') if args.delete_flow_rate: for fr_key in args.delete_flow_rate: - user_settings.delete_flow_rate(fr_key) + appia_settings.delete_flow_rate(fr_key) - user_settings.save_settings() + appia_settings.save_settings() + + if args.database_setup: + print("Setting up database. To leave any of these settings unchanged, leave them blank.") + + new_settings = {} + new_settings['database_host'] = input('Database host (something like blah.domain.edu): ') + new_settings['database_user'] = input('Database username. (You set this during the docker installation): ') + new_settings['database_password'] = input('Database password. (You set this during the docker installation): ') + port = input("Database port number. (If you don't know, it's the default so leave it blank): ") + new_settings['database_port'] = port if len(port) == 0 else int(port) + + for setting, value in new_settings.items(): + logging.debug(f'Setting {setting} is {value}') + if isinstance(value, int) or len(value) > 0: + setattr(appia_settings, setting, value) + + appia_settings.save_settings() + + if args.check_database_login: + print('Host:', appia_settings.database_host) + print('Username:', appia_settings.database_user) + print('Password:', appia_settings.database_password) + print('Port:', appia_settings.database_port) parser = argparse.ArgumentParser( @@ -72,4 +93,14 @@ def main(args): '--delete-flow-rate', help = 'Remove a flow rate from user settings. Give the method names. Can give multiple method names.', nargs='+' +) +parser.add_argument( + '--database-setup', + help = 'Set database access parameters', + action = 'store_true' +) +parser.add_argument( + '--check-database-login', + help = 'Print login info to the terminal', + action = 'store_true' ) \ No newline at end of file diff --git a/src/appia/processors/database.py b/src/appia/processors/database.py index 8c32a71..5014642 100644 --- a/src/appia/processors/database.py +++ b/src/appia/processors/database.py @@ -1,51 +1,23 @@ import couchdb import logging import pandas as pd -import os import sys from appia.processors.experiment import Experiment -import json - -class Config: - def __init__(self, config_file = None) -> None: - - if config_file is None: - try: - self.cuser = os.environ['COUCHDB_USERNAME'] - self.cpass = os.environ['COUCHDB_PASSWORD'] - self.chost = os.environ['COUCHDB_HOST'] - except KeyError: - logging.error('Provide a config file or set $COUCHDB_USERNAME, $COUCHDB_PASSWORD, $COUCHDB_HOST') - sys.exit(1) - else: - with open(config_file) as conf: - config = json.load(conf) - - try: - self.cuser = config['user'] - self.cpass = config['password'] - self.chost = config['host'] - self.couch = True - except KeyError: - logging.warning('Config missing information to connect to CouchDB') - self.couch = False - - try: - self.slack_token = config['token'] - self.slack_channel = config['chromatography_channel'] - self.slack = True - except KeyError: - logging.warning('Config missing information for Slack bot') - self.slack = False - - def __repr__(self) -> str: - return f'config object for host {self.chost}' +from appia.parsers.user_settings import appia_settings class Database: - def __init__(self, config) -> None: - self.config = config + def __init__(self) -> None: self.version = 4 - couchserver = couchdb.Server(f'http://{config.cuser}:{config.cpass}@{config.chost}:5984') + username = appia_settings.database_user + password = appia_settings.database_password + hostname = appia_settings.database_host + port = appia_settings.database_port + + if any([x is None for x in [username, password, hostname, port]]): + logging.error('You have not set your database login information. Please run `appia utils --database-setup`') + sys.exit(10) + + couchserver = couchdb.Server(f'http://{username}:{password}@{hostname}:{port}') dbname = 'traces' if dbname in couchserver: @@ -54,7 +26,7 @@ def __init__(self, config) -> None: self.db = couchserver.create(dbname) def __repr__(self) -> str: - return f'CouchDB at {self.config.chost}' + return f'CouchDB at {appia_settings.database_host}' def update_experiment_list(self): return [x['id'] for x in self.db.view('_all_docs')] @@ -146,9 +118,11 @@ def upload_experiment(self, exp, overwrite = False): self.db.save(doc) def migrate(self): - if input(f'To migrate database hosted at {self.config.chost}, type: I have backed up my db\n').lower() == 'i have backed up my db': + if input(f'To migrate database hosted at {appia_settings.database_host}, type: I have backed up my db\n').lower() == 'i have backed up my db': for exp_name in self.update_experiment_list(): exp = self.pull_experiment(exp_name) self.upload_experiment(exp, overwrite=True) else: logging.warning('Back up your database before migrating it.') + +db = Database() \ No newline at end of file diff --git a/src/appia/processors/hplc.py b/src/appia/processors/hplc.py index c07e705..a6eb86b 100644 --- a/src/appia/processors/hplc.py +++ b/src/appia/processors/hplc.py @@ -1,15 +1,12 @@ import pandas as pd import numpy as np from io import StringIO -import json import os import logging import re from appia.processors.core import loading_bar, normalizer from appia.processors.gui import user_input -from appia.parsers.user_settings import AppiaSettings - -user_settings = AppiaSettings() +from appia.parsers.user_settings import appia_settings def get_flow_rate(flow_rate, method, search = True): # If user provides in argument we don't need to do this @@ -21,9 +18,9 @@ def get_flow_rate(flow_rate, method, search = True): if method and search: logging.debug(f'Looking for fr for method {method}') - flow_rate = user_settings.check_flow_rate(method) + flow_rate = appia_settings.check_flow_rate(method) if flow_rate is not None: - logging.debug(f'Flow rate found in user_settings: {flow_rate}') + logging.debug(f'Flow rate found in appia_settings: {flow_rate}') return flow_rate, False while not flow_rate: diff --git a/src/appia/processors/slackbot.py b/src/appia/processors/slackbot.py deleted file mode 100644 index 80591b5..0000000 --- a/src/appia/processors/slackbot.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging -from slack import WebClient -from slack.errors import SlackApiError - -def get_client(config): - try: - assert config.slack_token != '' - client = WebClient(token = config.slack_token) - client.auth_test() - logging.info('Slack authentication succeeded') - except AssertionError: - logging.error('Your config has a blank bot token. Cannot post to Slack.') - except SlackApiError as e: - if e.response['error'] == 'invalid_auth': - logging.error('Slack bot authentication failed. Check your token.') - return - else: - raise e - - return client - -def send_graphs(config, client, files): - try: - assert config.slack_channel != '' - - client.chat_postMessage( - channel = config.slack_channel, - text = 'A chromatography run has completed!' - ) - for file in files: - client.files_upload( - channels = config.slack_channel, - file = file - ) - - except AssertionError: - logging.error('Your config channel ID is blank. Cannot send messages.') - except SlackApiError as e: - if e.response['error'] == 'channel_not_found': - logging.error('Channel name or ID is not correct. Cannot send messages.') - return - else: - raise e \ No newline at end of file