From 717bb15e56a14b4d814054c5bd5d15a9b4c9e20f Mon Sep 17 00:00:00 2001 From: Agnibho Mondal Date: Wed, 23 Dec 2015 15:11:04 +0530 Subject: [PATCH] Major code rearrangement --- __main__.py | 152 ++++--------------------------- _differentials.py | 47 ---------- alias.py | 57 ++++++++---- compile.py | 32 ++++++- conf.py | 44 +++++---- ddstorm.py | 95 +++++++++++++++---- _extras.py => extras.py | 91 +++++++++++++------ gui.py | 192 +++++++++++++++++++++++++++++++++++++++ index.py | 97 ++++++++++++++++---- _symptoms.py => panes.py | 113 +++++++++++++++++++---- 10 files changed, 627 insertions(+), 293 deletions(-) delete mode 100644 _differentials.py rename _extras.py => extras.py (70%) create mode 100644 gui.py rename _symptoms.py => panes.py (56%) diff --git a/__main__.py b/__main__.py index 1b430c2..2d7ca46 100644 --- a/__main__.py +++ b/__main__.py @@ -1,141 +1,29 @@ #! /usr/bin/python3 -# DDStorm -# ------- -# Copyright (c) 2015 Agnibho Mondal -# All rights reserved +''' +Start the application +''' +''' +Copyright (c) 2015 Agnibho Mondal +All rights reserved -# This file is part of DDStorm. +This file is part of DDStorm. -# DDStorm 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. +DDStorm 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. -# DDStorm 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. +DDStorm 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 DDStorm. If not, see . +You should have received a copy of the GNU General Public License +along with DDStorm. If not, see . +''' -import sys, time, subprocess -from PyQt4 import QtGui, QtCore -from _symptoms import Symptoms -from _differentials import Differentials -from ddstorm import DDStorm -from conf import Conf -from _extras import * -from const import * - -conf=False - -class Content(QtGui.QWidget): - change=QtCore.pyqtSignal() - def __init__(self): - super(Content, self).__init__() - self.dd=DDStorm(True,conf) - if(not self.dd.compiler.clean): - ret=QtGui.QMessageBox.warning(self, "Compilation Error", "Error was encountered while compiling the Knowledgebase.", "Ignore", "View Log") - if(ret==1): - x_logfile() - self.initUI() - def initUI(self): - global conf - - grid=QtGui.QGridLayout() - self.setLayout(grid) - - self.symp=Symptoms(self.dd.symptoms()) - self.symp.setFrameShape(QtGui.QFrame.StyledPanel) - self.symp.changed.connect(self.update) - - self.diff=Differentials() - self.diff.setFrameShape(QtGui.QFrame.StyledPanel) - - grid.addWidget(self.symp, 0, 0) - grid.addWidget(self.diff, 0, 1) - grid.setColumnStretch(0, 1) - grid.setColumnStretch(1, 1) - QtGui.QApplication.setStyle(QtGui.QStyleFactory.create("Cleanlooks")) - def update(self, data): - self.diff.update(self.dd.dd(data)) - self.change.emit() - -class Window(QtGui.QMainWindow): - def __init__(self): - super(Window, self).__init__() - self.initUI() - def initUI(self): - global conf - self.con=Content() - self.sett=SettingsDialog(conf) - if(conf.get("status_message")=="on"): - self.con.change.connect(self.showStatus) - - menu=self.menuBar() - menuFile=menu.addMenu("&File") - menuFile.addAction("&Save").triggered.connect(self.savefile) - menuFile.addAction("E&xit").triggered.connect(self.close) - menuEdit=menu.addMenu("&Edit") - menuEdit.addAction("&Add").triggered.connect(self.con.symp.addItem) - menuEdit.addAction("&Browse Symptoms").triggered.connect(self.con.symp.browseSymptoms) - rmAction=QtGui.QAction("&Remove", self) - rmAction.setShortcut("Delete") - rmAction.triggered.connect(self.con.symp.remove) - menuEdit.addAction(rmAction) - menuEdit.addAction("&Clear All").triggered.connect(self.con.symp.removeAll) - menuTool=menu.addMenu("&Tools") - menuTool.addAction("&Library").triggered.connect(x_lib) - menuTool.addAction("&Settings").triggered.connect(self.settings) - menuTool.addAction("&View Log").triggered.connect(x_logfile) - menuHelp=menu.addMenu("&Help") - menuHelp.addAction("&Help").triggered.connect(x_help) - menuHelp.addAction("&About").triggered.connect(self.about) - - self.setCentralWidget(self.con) - self.status=self.statusBar() - self.setGeometry(200, 200, 600, 400) - self.setWindowTitle("D/D Storm") - self.setWindowIcon(QtGui.QIcon("icons/icon.png")) - self.showMaximized() - self.con.symp.new.setFocus() - def showStatus(self): - if(self.con.symp.getList() and self.con.diff.getList()): - self.status.showMessage(str(len(self.con.diff.getList()))+" differential diagnosis for "+str(len(self.con.symp.getList()))+" symptom(s).") - else: - self.status.showMessage("") - def savefile(self): - x_save(self, self.con.symp.getList(), self.con.diff.getList()) - def settings(self): - self.sett.exec_() - def about(self): - QtGui.QMessageBox.about(self, "About", "

DDStorm

\nBrainstorm Medicine") - -def main(): - app=QtGui.QApplication(sys.argv) - - global conf - conf=Conf() - if(conf.get("clean_log")=="yes"): - open(LOG_FILE, "w").close() - if(conf.get("splash_screen")=="yes"): - ss=True - else: - ss=False - if(ss): - splash=QtGui.QSplashScreen(QtGui.QPixmap("icons/splash.png")) - splash.show() - time.sleep(0.1) - app.processEvents() - splash.showMessage("Loading...") - - w=Window() - if(ss): - splash.finish(w) - - sys.exit(app.exec_()) +import gui if(__name__=="__main__"): - main() + gui.main() diff --git a/_differentials.py b/_differentials.py deleted file mode 100644 index 16f86d3..0000000 --- a/_differentials.py +++ /dev/null @@ -1,47 +0,0 @@ -# DDStorm -# ------- -# Copyright (c) 2015 Agnibho Mondal -# All rights reserved - -# This file is part of DDStorm. - -# DDStorm 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. - -# DDStorm 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 DDStorm. If not, see . - -from PyQt4 import QtGui - -class Differentials(QtGui.QFrame): - data=[] - def __init__(self): - super(Differentials, self).__init__() - self.initUI() - def initUI(self): - self.label=QtGui.QLabel("Differential Diagnosis") - self.label.setStyleSheet("font-size:18px") - self.listWidget=QtGui.QListWidget(self) - self.listWidget.setStyleSheet("font-size:14px") - self.listWidget.setSelectionMode(0) - box=QtGui.QVBoxLayout() - box.addWidget(self.label) - box.addWidget(self.listWidget) - self.setLayout(box) - - def update(self, data): - self.data=data - self.listWidget.clear() - if(self.data): - for d in self.data: - QtGui.QListWidgetItem(d, self.listWidget) - - def getList(self): - return self.data diff --git a/alias.py b/alias.py index d4e013e..a1b9d10 100644 --- a/alias.py +++ b/alias.py @@ -1,32 +1,41 @@ #! /usr/bin/python3 -# DDStorm -# ------- -# Copyright (c) 2015 Agnibho Mondal -# All rights reserved +''' This module handles the aliases of the symptoms. ''' +''' +Copyright (c) 2015 Agnibho Mondal +All rights reserved -# This file is part of DDStorm. +This file is part of DDStorm. -# DDStorm 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. +DDStorm 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. -# DDStorm 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. +DDStorm 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 DDStorm. If not, see . +You should have received a copy of the GNU General Public License +along with DDStorm. If not, see . +''' -import sys, os +import sys +import os from fnmatch import fnmatch + from conf import Conf from const import * class Alias: + ''' Provides the class to handle symptom aliases ''' + def __init__(self, conf=False): + ''' + Initiates the alias object + Accepts a Conf object as parameter + ''' self.data={} if(conf): self.conf=conf @@ -35,19 +44,25 @@ class Alias: self.compile() def compile(self): + ''' Compile the plaintext index files to a program usable format ''' + # Loop over the alias files for path, subdirs, files in os.walk(self.conf.get("alias_path")): for name in files: if(fnmatch(name, "*.txt")): + # Open the *.txt files with open(self.conf.get("alias_path")+name, "r") as f: for line in f: + # Ignore lines starting with # line=line.rstrip().split("#")[0] if(len(line)==0): pass else: terms=[] + # Split words separated by ; and add to the terms for i in line.split(";"): if(i.strip()): terms.append(i.strip()) + # If alias present, add terms to the data if(len(terms)==2): self.data[terms[-1]]=terms[0] elif(len(terms)>2): @@ -56,12 +71,22 @@ class Alias: self.data[i]=t def get(self, term): + ''' + Return the alias of the queried symptom + + Parameter: + term - Queried string + + Return value: + String containing the alias of the term + ''' if(term in self.data): return self.data[term] else: return term def main(): + ''' Print the alias of the command line argument ''' a=Alias() if(len(sys.argv)>1): print(a.get(sys.argv[1])) diff --git a/compile.py b/compile.py index acb5ec7..80bb4d1 100644 --- a/compile.py +++ b/compile.py @@ -57,41 +57,57 @@ class Compile: self._conf=conf else: self._conf=Conf() + self.clean=True def compile(self): ''' Compile the text files to DDStorm modules. ''' self.source=set() self.custom=set() self.alias=Alias(self._conf) - self.clean=True + + # Loop over library files and add *.txt files to source for path, subdirs, files in os.walk(self._conf.get("library_path")): for name in files: if(fnmatch(name, "*.txt")): self.source.add(os.path.join(path, name)) + + # Loop over custom files and add *.txt files to custom for path, subdirs, files in os.walk(self._conf.get("custom_path")): for name in files: if(fnmatch(name, "*.txt")): self.custom.add(os.path.join(path, name)) + + # Create module directory if not already present and delete all module files if(not os.path.isdir(self._conf.get("module_path"))): os.makedirs(self._conf.get("module_path")) for f in os.listdir(self._conf.get("module_path")): if(fnmatch(f, "*.module")): os.unlink(self._conf.get("module_path")+f) + + # Create a regex for calculating priority from filename self.priorityRegex=re.compile("(?<=\.)\d+$") + + # First sort files by priority then compile them to module for src in self._sortPriority(self.source): self._makeModule(src) for src in self._sortPriority(self.custom): self._makeModule(src) def _sortPriority(self, files): + ''' Sort data files based on their priority settings. ''' ls=[] + # Loop over the files for addr in files: + # Format the file name name=os.path.splitext(os.path.basename(addr))[0].lower().replace("_"," ").replace("-", " ") + # Search for priority tag on file name m=re.search(self.priorityRegex, name) + # Add to ls as (symptom name, priority number, file name) with default priority of 100 if(m): ls.append((name.replace("."+m.group(), ""), int(m.group()), addr)) else: ls.append((name, 100, addr)) + # Sort the file list, first by the symptom name, then by the priority number ls.sort(reverse=True) if(ls): return(list(zip(*ls))[2]) @@ -99,13 +115,19 @@ class Compile: return ls def _makeModule(self, src): + ''' Create application usable modules from data files. ''' + # Format the file name module=os.path.splitext(os.path.basename(src))[0].lower().replace("_"," ").replace("-", " ") + # Remove the priority tag from file name m=re.search(self.priorityRegex, module) if(m): module=module.replace("."+m.group(), "") + # Create the module file name modFile=self._conf.get("module_path")+module+".module" modFlag=False + # Loop over both files, the source data file and the target module file with open(src, "r") as sf, open(modFile, "a") as tf: + # Ignore lines starting with ! or #, + and - has special meaning, write other lines to module. Log the errors. for line in sf: line=line.strip().split("#")[0] if(len(line)==0): @@ -123,17 +145,20 @@ class Compile: else: self.clean=False logging.warning("Syntax error in file '"+src+"': "+line) + # Deal with special lines if(modFlag): modFlag=False with open(src, "r") as f: for line in f: line=line.strip().split("#")[0] if(line[1:].replace(" ","").replace("-","").replace("_","").replace("'","").isalnum()): + # If line starts with + add it to the module file if(line.startswith("+")): with open(modFile, "r") as fn: text=fn.read() with open(modFile, "w") as fn: print(self.alias.get(line[1:]).capitalize()+"\n"+text, file=fn) + # If line starts with - remove corresponding item from the module file elif(line.startswith("-")): with open(modFile, "r") as fn: text=fn.read() @@ -141,7 +166,12 @@ class Compile: with open(modFile, "w") as fn: print(text, file=fn) + def is_clean(self): + '''Report if compilation ended successfully''' + return self.clean + def main(): + ''' Compile the data files into formatted module files ''' c=Compile().compile() if(__name__=="__main__"): diff --git a/conf.py b/conf.py index 5b84851..03ee5e8 100644 --- a/conf.py +++ b/conf.py @@ -25,7 +25,7 @@ along with DDStorm. If not, see . import os import logging -from const import * # Import constants +from const import * logging.basicConfig(filename=LOG_FILE) @@ -40,7 +40,7 @@ class Conf: function calls. ''' - _conf={} #Initiates configuration dictionary + _conf={} def __init__(self, filename=CONF_FILE): ''' @@ -65,47 +65,59 @@ class Conf: def read(self): ''' Read the configuration file and collect the values ''' - if(os.path.isfile(self.filename)): # If file is actually present + # If file is actually present + if(os.path.isfile(self.filename)): try: - with open(self.filename) as f: # Open file + with open(self.filename) as f: for line in f: - line="".join(line.split()) # Removes any stray whitespaces - if(line.startswith("#")): # Ignores comments + # Removes any stray whitespaces + line="".join(line.split()) + # Ignores comments starting with # + if(line.startswith("#")): pass - elif(line.startswith("library_path=")): # Library files path + # Library path + elif(line.startswith("library_path=")): self._conf["library_path"]=line[13:] if(os.path.isdir(self._conf["library_path"])): if(not self._conf["library_path"].endswith("/")): self._conf["library_path"]+="/" - elif(line.startswith("custom_path=")): # Custom files path + # Custom path + elif(line.startswith("custom_path=")): self._conf["custom_path"]=line[12:] if(os.path.isdir(self._conf["custom_path"])): if(not self._conf["custom_path"].endswith("/")): self._conf["custom_path"]+="/" - elif(line.startswith("index_path=")): # Index files path + # Index path + elif(line.startswith("index_path=")): self._conf["index_path"]=line[11:] if(os.path.isdir(self._conf["index_path"])): if(not self._conf["index_path"].endswith("/")): self._conf["index_path"]+="/" - elif(line.startswith("alias_path=")): # Alias files path + # Alias path + elif(line.startswith("alias_path=")): self._conf["alias_path"]=line[11:] if(os.path.isdir(self._conf["alias_path"])): if(not self._conf["alias_path"].endswith("/")): self._conf["alias_path"]+="/" - elif(line.startswith("module_path=")): # Path to save compiled modules + # Module path + elif(line.startswith("module_path=")): self._conf["module_path"]=line[12:] if(os.path.isdir(self._conf["module_path"])): if(not self._conf["module_path"].endswith("/")): self._conf["module_path"]+="/" - elif(line.startswith("splash_screen=")): # Whether to show a splash screen + # Splash screen + elif(line.startswith("splash_screen=")): self._conf["splash_screen"]=line[14:] - elif(line.startswith("clean_log=")): # Whether to clean logs before exit + # Clean log + elif(line.startswith("clean_log=")): self._conf["clean_log"]=line[10:] - elif(line.startswith("status_message=")): # Whether to show status messages + # Status message + elif(line.startswith("status_message=")): self._conf["status_message"]=line[15:] + # Unknown option else: - logging.warning("Unrecognized configuration: "+line) # Log a warning if unrecognized option found - except: # Go with default if file could not be read and log an error + logging.warning("Unrecognized configuration: "+line) + except: logging.error("Configuration file "+self.filename+" could not be read. Using default configurations.") def get(self, key=False): diff --git a/ddstorm.py b/ddstorm.py index 7fb300a..2b6664c 100644 --- a/ddstorm.py +++ b/ddstorm.py @@ -1,53 +1,94 @@ #! /usr/bin/python3 -# DDStorm -# ------- -# Copyright (c) 2015 Agnibho Mondal -# All rights reserved +''' +DDStorm is a python application for finding differential diagnosis for +a given list of symptoms. +''' +''' +Copyright (c) 2015 Agnibho Mondal +All rights reserved -# This file is part of DDStorm. +This file is part of DDStorm. -# DDStorm 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. +DDStorm 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. -# DDStorm 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. +DDStorm 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 DDStorm. If not, see . +You should have received a copy of the GNU General Public License +along with DDStorm. If not, see . +''' + +import sys +import os -import sys, os from compile import Compile from conf import Conf from index import Index class DDStorm: + ''' Provides the class for finding differential diagnosis. ''' conf=False + def __init__(self, comp=False, conf=False): + ''' + Initiate the diagnosis finder. + + Parameters: + comp - Recompiles the data files if set to True + conf - Supply a Conf object + ''' if(conf): self.conf=conf else: self.conf=Conf() + self.compiler=Compile(conf) if(comp): - self.compiler=Compile(conf).compile() + self.compiler.compile() self.index=Index(conf) def dd(self, symptoms): + ''' + Find the differential diagnosis list. + + Parameter: + symptom - list of strings containing symptoms + + Return value: + List of strings containing the differential diagnosis + ''' + + # Return empty list if symptom list is empty if(not symptoms): return + + # Find DD of first symptom and discard it diff1=self._getDiff(symptoms.pop(0)) + + # Loop through the rest of the list for s in symptoms: + + # Find DD of the current item in the list diff2=self._getDiff(s) + + # List for temporary holding the DDs temp=[] + + # Make both lists the same length by appending empty strings to the end if(len(diff1)>len(diff2)): diff2+=[""]*(len(diff1)-len(diff2)) elif(len(diff2)>len(diff1)): diff1+=[""]*(len(diff2)-len(diff1)) + + # Loop over both lists for (s1, s2) in zip(diff1, diff2): + + # Add s1 to temp if s1 or any of its upstream ancestor is common to both list if((s1 not in temp) and (len(s1)>0)): if(s1 in diff2): temp.append(s1) @@ -56,6 +97,8 @@ class DDStorm: for i in us: if(i in diff2): temp.append(i) + + # Add s2 to temp if s2 or any of its upstream ancestor is common to both list if((s2 not in temp) and (len(s2)>0)): if(s2 in diff1): temp.append(s2) @@ -64,10 +107,14 @@ class DDStorm: for i in us: if(i in diff1): temp.append(i) + + # Copy temp to first list diff1=list(temp) + return diff1 def _getDiff(self, symptom): + ''' Return differential diagnosis for a single symptom ''' diff=[] symptom=symptom.lower().replace("_"," ").replace("-", " ") if(os.path.isfile(self.conf.get("module_path")+symptom+".module")): @@ -77,12 +124,26 @@ class DDStorm: return diff def symptoms(self): + ''' + Return a full list of available symptoms + + Return value: + List of string containing symptoms + ''' symp=[] for n in os.listdir(self.conf.get("module_path")): symp.append(os.path.splitext(os.path.basename(n))[0].capitalize()) return symp def main(): + ''' + Find differential diagnosis in command line mode. + Accepts symptoms as command line arguments. Prints a list of + avialable symptoms if called without any argument. + + Command line arguments: + A list of symptoms separated by space + ''' s=DDStorm() if(len(sys.argv)>1): for d in s.dd(sys.argv[1:]): diff --git a/_extras.py b/extras.py similarity index 70% rename from _extras.py rename to extras.py index d15e627..8021012 100644 --- a/_extras.py +++ b/extras.py @@ -1,31 +1,37 @@ -# DDStorm -# ------- -# Copyright (c) 2015 Agnibho Mondal -# All rights reserved - -# This file is part of DDStorm. - -# DDStorm 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. - -# DDStorm 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 DDStorm. If not, see . - -import subprocess, os +''' +This module provides some extra functionalities to the main window. +''' +''' +Copyright (c) 2015 Agnibho Mondal +All rights reserved + +This file is part of DDStorm. + +DDStorm 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. + +DDStorm 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 DDStorm. If not, see . +''' + +import subprocess +import os from PyQt4 import QtGui, QtCore from const import * def x_settings(): + ''' Open the configuration file ''' subprocess.Popen(["xdg-open", CONF_FILE]) def x_lib(): + ''' Open the library path ''' if(os.path.isfile(CONF_FILE)): with open(CONF_FILE) as conf: for line in conf: @@ -35,6 +41,7 @@ def x_lib(): subprocess.Popen(["xdg-open", library_path]) def x_save(w, symp, diff): + ''' Save data to a file ''' fname=QtGui.QFileDialog.getSaveFileName(w, "Save File", "~", "HTML files('*.html')") if(not fname.endswith(".html")): fname=fname+".html" @@ -52,25 +59,40 @@ def x_save(w, symp, diff): print("", file=f) def x_help(): + ''' Show help ''' if(os.path.isfile(HELP_FILE)): subprocess.Popen(["xdg-open", HELP_FILE]) else: subprocess.Popen(["xdg-open", "http://www.agnibho.com"]) def x_logfile(): + ''' Open log file ''' subprocess.Popen(["xdg-open", LOG_FILE]) + class SettingsDialog(QtGui.QDialog): + ''' + Provides a dialog box to configure application settings with a + graphical user interface. + ''' + def __init__(self, conf): + ''' Initiate the dialog ''' super(SettingsDialog, self).__init__() self.setWindowTitle("Settings") self.conf=conf self.initUI() + def initUI(self): + ''' Initiate the user interface ''' self.lpLabel=QtGui.QLabel("Libary Path:") self.lpEdit=QtGui.QLineEdit(self.conf.get("library_path")) self.lpBrowse=QtGui.QPushButton("Browse") self.lpBrowse.clicked.connect(self.lpUpdate) + self.cpLabel=QtGui.QLabel("Custom Path:") + self.cpEdit=QtGui.QLineEdit(self.conf.get("custom_path")) + self.cpBrowse=QtGui.QPushButton("Browse") + self.cpBrowse.clicked.connect(self.cpUpdate) self.mpLabel=QtGui.QLabel("Module Path:") self.mpEdit=QtGui.QLineEdit(self.conf.get("module_path")) self.mpBrowse=QtGui.QPushButton("Browse") @@ -99,24 +121,33 @@ class SettingsDialog(QtGui.QDialog): layout.addWidget(self.lpLabel, 0, 0) layout.addWidget(self.lpEdit, 0, 1) layout.addWidget(self.lpBrowse, 0, 2) - layout.addWidget(self.mpLabel, 1, 0) - layout.addWidget(self.mpEdit, 1, 1) - layout.addWidget(self.mpBrowse, 1, 2) - layout.addWidget(self.splash, 2, 0) - layout.addWidget(self.clean, 3, 0) - layout.addWidget(self.status, 4, 0) - layout.addLayout(ctrl, 5, 1) + layout.addWidget(self.cpLabel, 1, 0) + layout.addWidget(self.cpEdit, 1, 1) + layout.addWidget(self.cpBrowse, 1, 2) + layout.addWidget(self.mpLabel, 2, 0) + layout.addWidget(self.mpEdit, 2, 1) + layout.addWidget(self.mpBrowse, 2, 2) + layout.addWidget(self.splash, 3, 0) + layout.addWidget(self.clean, 4, 0) + layout.addWidget(self.status, 5, 0) + layout.addLayout(ctrl, 6, 1) self.cancel.setFocus() def lpUpdate(self): + ''' Updates the library path ''' self.lpEdit.setText(self.getFolder()) + def cpUpdate(self): + ''' Updates the custom path ''' self.cpEdit.setText(self.getFolder()) + def mpUpdate(self): + ''' Updates the module path ''' self.mpEdit.setText(self.getFolder()) def getFolder(self): + ''' Returns the selected directory ''' dn=QtGui.QFileDialog.getExistingDirectory() if(dn.startswith(QtCore.QDir.currentPath())): dn="."+dn[len(QtCore.QDir.currentPath()):]+"/" @@ -125,8 +156,9 @@ class SettingsDialog(QtGui.QDialog): return dn def save(self): + ''' Saves the configuration to disk ''' self.conf.set("library_path", self.lpEdit.text()) - self.conf.set("class_path", self.cpEdit.text()) + self.conf.set("custom_path", self.cpEdit.text()) self.conf.set("module_path", self.mpEdit.text()) if(self.splash.isChecked()): self.conf.set("splash_screen", "yes") @@ -145,6 +177,7 @@ class SettingsDialog(QtGui.QDialog): self.conf.write() def reset(self): + ''' Resests the settings to the default factory value ''' self.conf.default() self.lpEdit.setText(self.conf.get("library_path")) self.cpEdit.setText(self.conf.get("class_path")) diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..55320cb --- /dev/null +++ b/gui.py @@ -0,0 +1,192 @@ +#! /usr/bin/python3 + +''' +This module provides the main graphical user interface for DDStorm +''' +''' +Copyright (c) 2015 Agnibho Mondal +All rights reserved + +This file is part of DDStorm. + +DDStorm 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. + +DDStorm 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 DDStorm. If not, see . +''' + +import sys +import time +import subprocess + +from PyQt4 import QtGui, QtCore + +from panes import Symptoms, Differentials +from ddstorm import DDStorm +from conf import Conf +from extras import * +from const import * + +conf=False + +class Content(QtGui.QWidget): + ''' + Provides the main content widget. Contains the sysmptoms and + the diagnosis panes. Also creates the DDStorm object and performs + the main operation. + ''' + + # Signal to detect when + change=QtCore.pyqtSignal() + + def __init__(self): + ''' Initiate the content widget ''' + super(Content, self).__init__() + + # Create DDStorm object with the global configuration + self.dd=DDStorm(True, conf) + + # Show warning if any error happened during data compilation + if(not self.dd.compiler.is_clean()): + ret=QtGui.QMessageBox.warning(self, "Compilation Error", "Error was encountered while compiling the Knowledgebase.", "Ignore", "View Log") + if(ret==1): + x_logfile() + + self.initUI() + + def initUI(self): + ''' Create the user interface of the widget ''' + + global conf + + grid=QtGui.QGridLayout() + self.setLayout(grid) + + self.symp=Symptoms(self.dd.symptoms()) + self.symp.setFrameShape(QtGui.QFrame.StyledPanel) + self.symp.changed.connect(self.update) + + self.diff=Differentials() + self.diff.setFrameShape(QtGui.QFrame.StyledPanel) + + grid.addWidget(self.symp, 0, 0) + grid.addWidget(self.diff, 0, 1) + grid.setColumnStretch(0, 1) + grid.setColumnStretch(1, 1) + QtGui.QApplication.setStyle(QtGui.QStyleFactory.create("Cleanlooks")) + + def update(self, data): + ''' Update the inteface with refreshed information ''' + self.diff.update(self.dd.dd(data)) + self.change.emit() + + +class Window(QtGui.QMainWindow): + ''' + Provides main application window. Acts as a container for the + content widget. Also contains the menubar and the status bar. + ''' + + def __init__(self): + ''' Initiate the main window ''' + super(Window, self).__init__() + self.initUI() + + def initUI(self): + ''' Create the user interface ''' + global conf + self.con=Content() + self.sett=SettingsDialog(conf) + if(conf.get("status_message")=="on"): + self.con.change.connect(self.showStatus) + + menu=self.menuBar() + menuFile=menu.addMenu("&File") + menuFile.addAction("&Save").triggered.connect(self.savefile) + menuFile.addAction("E&xit").triggered.connect(self.close) + menuEdit=menu.addMenu("&Edit") + menuEdit.addAction("&Add").triggered.connect(self.con.symp.addItem) + menuEdit.addAction("&Browse Symptoms").triggered.connect(self.con.symp.browseSymptoms) + rmAction=QtGui.QAction("&Remove", self) + rmAction.setShortcut("Delete") + rmAction.triggered.connect(self.con.symp.remove) + menuEdit.addAction(rmAction) + menuEdit.addAction("&Clear All").triggered.connect(self.con.symp.removeAll) + menuTool=menu.addMenu("&Tools") + menuTool.addAction("&Library").triggered.connect(x_lib) + menuTool.addAction("&Settings").triggered.connect(self.settings) + menuTool.addAction("&View Log").triggered.connect(x_logfile) + menuHelp=menu.addMenu("&Help") + menuHelp.addAction("&Help").triggered.connect(x_help) + menuHelp.addAction("&About").triggered.connect(self.about) + + self.setCentralWidget(self.con) + self.status=self.statusBar() + self.setGeometry(200, 200, 600, 400) + self.setWindowTitle("D/D Storm") + self.setWindowIcon(QtGui.QIcon("icons/icon.png")) + self.showMaximized() + self.con.symp.new.setFocus() + + def showStatus(self): + ''' Show status message ''' + if(self.con.symp.getList() and self.con.diff.getList()): + self.status.showMessage(str(len(self.con.diff.getList()))+" differential diagnosis for "+str(len(self.con.symp.getList()))+" symptom(s).") + else: + self.status.showMessage("") + + def savefile(self): + ''' Save data to a file ''' + x_save(self, self.con.symp.getList(), self.con.diff.getList()) + + def settings(self): + ''' Open the settings dialog ''' + self.sett.exec_() + + def about(self): + ''' Show information about this application ''' + QtGui.QMessageBox.about(self, "About", "

DDStorm

\nBrainstorm Medicine") + + +def main(): + ''' Start the main application interface ''' + app=QtGui.QApplication(sys.argv) + + # Initiate the global configuration + global conf + conf=Conf() + + # Clean the log file + if(conf.get("clean_log")=="yes"): + open(LOG_FILE, "w").close() + + # Show splash-screen + if(conf.get("splash_screen")=="yes"): + ss=True + else: + ss=False + if(ss): + splash=QtGui.QSplashScreen(QtGui.QPixmap("icons/splash.png")) + splash.show() + time.sleep(0.1) + app.processEvents() + splash.showMessage("Loading...") + + # Create main window + w=Window() + if(ss): + splash.finish(w) + + # Start application + sys.exit(app.exec_()) + +if(__name__=="__main__"): + main() diff --git a/index.py b/index.py index f1da749..d7e7b67 100644 --- a/index.py +++ b/index.py @@ -1,32 +1,51 @@ #! /usr/bin/python3 -# DDStorm -# ------- -# Copyright (c) 2015 Agnibho Mondal -# All rights reserved +''' +This module handles the upstream ancestors of symptoms. -# This file is part of DDStorm. +The symptoms can be classified in different levels, with the more +generalized symtoms at upstream and the more specialized symptoms at +the downstream. -# DDStorm 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. +For example: +'Acute peritonitis' is a special type of 'peritonitis' +Hence 'peritionitis' will be at upstream and 'acute peritonitis' will be +at downstream. +''' +''' +Copyright (c) 2015 Agnibho Mondal +All rights reserved -# DDStorm 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. +This file is part of DDStorm. -# You should have received a copy of the GNU General Public License -# along with DDStorm. If not, see . +DDStorm 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. -import sys, os +DDStorm 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 DDStorm. If not, see . +''' + +import sys +import os from fnmatch import fnmatch + from conf import Conf from const import * class Index: + ''' Provides an index of the upstream ancestors of symptoms ''' def __init__(self, conf=False): + ''' + Initiate the index object. Accepts a Conf object as an + optional parameter. + ''' if(conf): self.conf=conf else: @@ -35,55 +54,101 @@ class Index: self.compile() def compile(self): + ''' + Compile the text index files to a application usable format. + ''' + # Loop over all files under index_path for path, subdirs, files in os.walk(self.conf.get("index_path")): for name in files: + # Only files with *.txt suffix if(fnmatch(name, "*.txt")): with open(self.conf.get("index_path")+name, "r") as f: + # Two dimensional list buffer to hold the index buff=[] buff.append([]) buff.append([]) + # Loop over all lines as separate entries for line in f: + # Ignore commnents starting with # line=line.rstrip().split("#")[0] if(len(line)==0): pass else: + # Find number of leading whitespaces ws=len(line)-len(line.lstrip()) + # Format the entry line=line.lstrip().capitalize() + + # No leading whitespace means a top level entry i.e. no upstream ancestor if(ws==0): + # Empty the buffer and add the entry del buff[0][:] buff[0].append(line.lstrip()) + # Reset the whitespace index del buff[1][:] buff[1].append(0) + # If leading whitespace > indexed whitespace, the entry is a subcategory of previous entry elif(ws>buff[1][-1]): + # Append entry to buffer as new item buff[0].append(line.lstrip()) + # Record leading whitespace to buffer buff[1].append(ws) + # Copy buffer to data list self.data[buff[0][-1]]=list(reversed(buff[0])) + # If leading whitespace == indexed whitespace, the entry is at the same level with the previous entry elif(ws==buff[1][-1]): + # Append entry to last item of buffer buff[0][-1]=line.lstrip() buff[1][-1]=ws + # Copy buffer to data list self.data[buff[0][-1]]=list(reversed(buff[0])) + # If leading whitespace < indexed whitespace, the entry is at an upper category than the previous entry elif(ws=ws): buff[0].pop() buff[1].pop() + #Append the entry to buffer else: buff[0].append(line.lstrip()) buff[1].append(ws) break + # Copy buffer to data list self.data[buff[0][-1]]=list(reversed(buff[0])) def upstream(self, name): + ''' + Return the upstream list of a symptom + + Parameter: + name - the name of the symptom as string + + Return value: + List of strings containing the upstream items + ''' if(len(name)>0): if name in self.data: return self.data[name] else: return [] + def names(self): + ''' Return all indexed symptoms name ''' + return list(self.data.keys()) + def main(): + ''' + Prints upstream items of a symptom provided as command line + argument. + If no argument provided, returns a list of all indexed symptoms. + ''' i=Index() if(len(sys.argv)>1): print(i.upstream(sys.argv[1])) + else: + print(i.names()) if(__name__=="__main__"): main() diff --git a/_symptoms.py b/panes.py similarity index 56% rename from _symptoms.py rename to panes.py index 2d7d8d6..79636a4 100644 --- a/_symptoms.py +++ b/panes.py @@ -1,34 +1,54 @@ -# DDStorm -# ------- -# Copyright (c) 2015 Agnibho Mondal -# All rights reserved - -# This file is part of DDStorm. - -# DDStorm 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. - -# DDStorm 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 DDStorm. If not, see . +''' +This module provides the application panes containing essential +functionalities. +''' +''' +Copyright (c) 2015 Agnibho Mondal +All rights reserved + +This file is part of DDStorm. + +DDStorm 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. + +DDStorm 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 DDStorm. If not, see . +''' from PyQt4 import QtGui, QtCore class Symptoms(QtGui.QFrame): + ''' Provides the widget for symptoms input ''' + + # List to hold the symptoms sympList=[] + + # Signal to notify any change in input changed=QtCore.pyqtSignal(list) + def __init__(self, auto): + ''' + Initiate the symptom input pane. Takes a list of string as + argument. The list is used as the auto-complete list during + user input. + + Parameter: + auto - A list of string containing all available symptoms for + autocomplete. + ''' super(Symptoms, self).__init__() self.auto=auto self.initUI() def initUI(self): + ''' Initiate the user interface ''' self.label=QtGui.QLabel("Symptoms") self.label.setStyleSheet("font-size:18px") @@ -73,6 +93,7 @@ class Symptoms(QtGui.QFrame): self.setLayout(vbox) def addItem(self, text=""): + ''' Add a new symptom ''' if(not text): text=self.new.text() if(len(text)>0): @@ -87,6 +108,7 @@ class Symptoms(QtGui.QFrame): QtGui.QMessageBox.warning(self, "Symptom Unvailable", "'"+text+"' is not available in the current Library.") def remove(self, all=False): + ''' Remove selected symptoms ''' if(len(self.listWidget.selectedItems())>0): for item in self.listWidget.selectedItems(): self.sympList.remove(item.text()) @@ -94,25 +116,36 @@ class Symptoms(QtGui.QFrame): self.changed.emit(list(self.sympList)) def removeAll(self): + ''' Clear all symptoms ''' self.listWidget.clear() del self.sympList[:] self.changed.emit(list(self.sympList)) def browseSymptoms(self): + ''' Open the symptom browser ''' self.browser.exec_() def getList(self): + ''' Return a list of all symptoms ''' return self.sympList + class SymptomBrowser(QtGui.QDialog): + ''' + Provides a dialog with a list of symptoms for alternative user + input. + ''' added=QtCore.pyqtSignal(str) + def __init__(self, items): + ''' Initiate the input dialog ''' super(SymptomBrowser, self).__init__() self.setWindowTitle("Choose Symptom") self.items=items self.initUI() def initUI(self): + ''' Initiate the dialog interface ''' self.search=QtGui.QLineEdit() self.search.textChanged.connect(self.refresh) self.listItems=QtGui.QListWidget() @@ -126,6 +159,7 @@ class SymptomBrowser(QtGui.QDialog): self.search.setFocus() def refresh(self): + ''' Refresh the symptom list based on search term ''' term=self.search.text() buff=[] for i in self.items: @@ -135,5 +169,46 @@ class SymptomBrowser(QtGui.QDialog): self.listItems.addItems(buff) def sendUp(self): + ''' Emit signal and close when a symptom is selected ''' self.added.emit(self.listItems.currentItem().text()) self.close() + + +class Differentials(QtGui.QFrame): + ''' Provides the widget for differential diagnosis output ''' + data=[] + + def __init__(self): + ''' Initiate the diagnosis output pane ''' + super(Differentials, self).__init__() + self.initUI() + + def initUI(self): + ''' Initiate the user interface ''' + self.label=QtGui.QLabel("Differential Diagnosis") + self.label.setStyleSheet("font-size:18px") + self.listWidget=QtGui.QListWidget(self) + self.listWidget.setStyleSheet("font-size:14px") + self.listWidget.setSelectionMode(0) + box=QtGui.QVBoxLayout() + box.addWidget(self.label) + box.addWidget(self.listWidget) + self.setLayout(box) + + def update(self, data): + ''' + Update the outut pane with updated diagnosis list. + + Parameter: + data - List of strings containing new list of differential + diagnosis + ''' + self.data=data + self.listWidget.clear() + if(self.data): + for d in self.data: + QtGui.QListWidgetItem(d, self.listWidget) + + def getList(self): + ''' Return a list of current differential diagnosis ''' + return self.data -- 2.39.2