Source code for pyworkflow.gui.project.project

#!/usr/bin/env python
# **************************************************************************
# *
# * Authors:     J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
# *
# * [1] SciLifeLab, Stockholm University
# *
# * 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 <https://www.gnu.org/licenses/>.
# *
# *  All comments concerning this program package may be sent to the
# *  e-mail address 'scipion@cnb.csic.es'
# *
# **************************************************************************
"""
Main Project window implementation.
It is composed by three panels:
1. Left: protocol tree.
2. Right upper: VIEWS (Data/Protocols)
3. Summary/Details
"""

import logging
logger = logging.getLogger(__name__)
import os
import threading
import shlex
import subprocess
import socketserver
import tempfile

import pyworkflow as pw
import pyworkflow.utils as pwutils
from pyworkflow.gui.project.utils import OS
from pyworkflow.project import MenuConfig, ProjectSettings
from pyworkflow.gui import Message, Icon
from pyworkflow.gui.browser import FileBrowserWindow
# Usage commented.
# from pyworkflow.em.viewers import EmPlotter
# Moved to Scipion-app
# from pyworkflow.gui.plugin_manager import PluginManager
from pyworkflow.gui.plotter import Plotter
from pyworkflow.gui.text import _open_cmd, openTextFileEditor
from pyworkflow.webservices import ProjectWorkflowNotifier, WorkflowRepository

from .labels import LabelsDialog
# Import possible Object commands to be handled
from .base import ProjectBaseWindow, VIEW_PROTOCOLS, VIEW_PROJECTS


[docs]class ProjectWindow(ProjectBaseWindow): """ Main window for working in a Project. """ _OBJECT_COMMANDS = {} def __init__(self, path, master=None): # Load global configuration self.projName = os.path.basename(path) try: projTitle = '%s (%s on %s)' % (self.projName, pwutils.getLocalUserName(), pwutils.getLocalHostName()) except Exception: projTitle = self.projName self.projPath = path self.project = self.loadProject() # TODO: put the menu part more nicely. From here: menu = MenuConfig() projMenu = menu.addSubMenu('Project') projMenu.addSubMenu('Browse files', 'browse', icon=Icon.FOLDER_OPEN) projMenu.addSubMenu('Remove temporary files', 'delete', icon=Icon.ACTION_DELETE) projMenu.addSubMenu('Toggle color mode', 'color_mode', shortCut="Ctrl+t", icon=Icon.ACTION_VISUALIZE) projMenu.addSubMenu('Select all protocols', 'select all', shortCut="Ctrl+a", icon=Icon.SELECT_ALL) projMenu.addSubMenu('Add a protocol', 'find protocol', shortCut="Ctrl+f", icon=Icon.FIND) projMenu.addSubMenu('Locate a protocol', 'locate protocol', shortCut="Ctrl+l") projMenu.addSubMenu('', '') # add separator projMenu.addSubMenu('Import workflow', 'load_workflow', icon=Icon.DOWNLOAD) projMenu.addSubMenu('Search workflow', 'search_workflow', icon=Icon.ACTION_SEARCH) if pw.Config.debugOn(): projMenu.addSubMenu('Export tree graph', 'export_tree') projMenu.addSubMenu('', '') # add separator projMenu.addSubMenu('Debug Mode', 'debug mode', shortCut="Ctrl+D", icon=Icon.DEBUG) projMenu.addSubMenu('', '') # add separator projMenu.addSubMenu('Notes', 'notes', icon=Icon.ACTION_EDIT) projMenu.addSubMenu('', '') # add separator projMenu.addSubMenu('Exit', 'exit', icon=Icon.ACTION_OUT) helpMenu = menu.addSubMenu('Help') helpMenu.addSubMenu('Online help', 'online_help', icon=Icon.ACTION_EXPORT) helpMenu.addSubMenu('About', 'about', icon=Icon.ACTION_HELP) helpMenu.addSubMenu('Contact support', 'contact_us', icon=Icon.ACTION_HELP) self.menuCfg = menu if self.project.openedAsReadOnly(): self.projName += "<READ ONLY>" # Notify about the workflow in this project self.selectedProtocol = None self.showGraph = False Plotter.setBackend('TkAgg') ProjectBaseWindow.__init__(self, projTitle, master, minsize=(90, 50), icon=Icon.SCIPION_ICON_PROJ, _class=self.projName) OS.handler().maximizeWindow(self.root) self.switchView(VIEW_PROTOCOLS) self.initProjectTCPServer() # Socket thread to communicate with clients ProjectWorkflowNotifier(self.project).notifyWorkflow()
[docs] def createHeaderFrame(self, parent): """Create the header and add the view selection frame at the right.""" header = ProjectBaseWindow.createHeaderFrame(self, parent) self.addViewList(header) return header
[docs] def getSettings(self): return self.settings
[docs] def saveSettings(self): try: self.settings.write() except Exception as ex: logger.error(Message.NO_SAVE_SETTINGS, exc_info=ex)
def _onClosing(self): if not self.project.openedAsReadOnly(): self.saveSettings() ProjectBaseWindow._onClosing(self)
[docs] def loadProject(self): proj = pw.project.Project(pw.Config.getDomain(), self.projPath) proj.load() # Check if we have settings.sqlite, generate if not settingsPath = os.path.join(proj.path, proj.settingsPath) if os.path.exists(settingsPath): self.settings = proj.getSettings() else: logger.info('Warning: settings.sqlite not found! ' 'Creating default settings..') self.settings = proj.createSettings() return proj
# The next functions are callbacks from the menu options. # See how it is done in pyworkflow/gui/gui.py:Window._addMenuChilds() #
[docs] def onBrowseFiles(self): # Project -> Browse files FileBrowserWindow("Browse Project files", self, self.project.getPath(''), selectButton=None # we will select nothing ).show()
[docs] def onDebugMode(self): pw.Config.toggleDebug()
[docs] def onNotes(self): notes_program = pw.Config.SCIPION_NOTES_PROGRAM notes_args = pw.Config.SCIPION_NOTES_ARGS args = [] notes_file = self.project.getPath('Logs', pw.Config.SCIPION_NOTES_FILE) # If notesFile does not exist, it is created and an explanation/documentation comment is added at the top. if not os.path.exists(notes_file): f = open(notes_file, 'a') f.write(pw.genNotesHeading()) f.close() # Then, it will be opened as specified in the conf if notes_program: args.append(notes_program) # Custom arguments if notes_args: args.append(notes_args) args.append(notes_file) subprocess.Popen(args) # nonblocking else: # if no program has been selected # xdg-open will try to guess but # if the file does not exist it # will return an error so If the file does # not exist I will create an empty one # 'a' will avoid accidental truncation openTextFileEditor(notes_file)
[docs] def onRemoveTemporaryFiles(self): # Project -> Remove temporary files tmpPath = os.path.join(self.project.path, self.project.tmpPath) n = 0 try: for fname in os.listdir(tmpPath): fpath = "%s/%s" % (tmpPath, fname) if os.path.isfile(fpath): os.remove(fpath) n += 1 # TODO: think what to do with directories. Delete? Report? self.showInfo("Deleted content of %s -- %d file(s)." % (tmpPath, n)) except Exception as e: self.showError(str(e))
def _loadWorkflow(self, obj): try: self.getViewWidget().info('Importing workflow %s' % obj.getPath()) self.project.loadProtocols(obj.getPath()) self.getViewWidget().updateRunsGraph(True) self.getViewWidget().cleanInfo() except Exception as ex: self.showError(str(ex), exception=ex)
[docs] def onImportWorkflow(self): FileBrowserWindow("Select workflow .json file", self, self.project.getPath(''), onSelect=self._loadWorkflow, selectButton='Import' ).show()
[docs] def onSearchWorkflow(self): WorkflowRepository().search()
[docs] def onExportTreeGraph(self): runsGraph = self.project.getRunsGraph() useId = not pwutils.envVarOn('SCIPION_TREE_NAME') dotStr = runsGraph.printDot(useId=useId) with tempfile.NamedTemporaryFile(suffix='.gv', mode="w") as dotFile: dotFile.write(dotStr) dotFile.flush() openTextFileEditor(dotFile.name) if useId: logger.info("\nexport SCIPION_TREE_NAME=1 # to use names instead of ids") else: logger.info("\nexport SCIPION_TREE_NAME=0 # to use ids instead of names")
[docs] def onToggleColorMode(self): self.getViewWidget()._toggleColorScheme(None)
[docs] def onSelectAllProtocols(self): self.getViewWidget()._selectAllProtocols(None)
[docs] def onAddAProtocol(self): self.getViewWidget()._findProtocol(None)
[docs] def onLocateAProtocol(self): self.getViewWidget()._locateProtocol(None)
[docs] def manageLabels(self): labels = self.project.settings.getLabels() dialog = LabelsDialog(self.root, labels, allowSelect=True) # Scan for renamed labels to update node info... labelsRenamed = dict() for label in labels: if label.hasOldName(): oldName = label.getOldName() newName = label.getName() logger.info("Label %s renamed to %s" % (oldName, newName)) labelsRenamed[oldName] = newName label.clearOldName() # If there are labels renamed if labelsRenamed: logger.info("Updating labels of protocols after renaming.") labels.updateDict() for node in self.project.settings.getNodes(): nodeLabels = node.getLabels() for index, nodeLabel in enumerate(nodeLabels): newLabel = labelsRenamed.get(nodeLabel, None) if newLabel is not None: logger.info("Label %s found in %s. Updating it to %s" % (nodeLabel,node, newLabel)) nodeLabels[index] = newLabel return dialog
[docs] def initProjectTCPServer(self): server = ProjectTCPServer((self.project.address, self.project.port), ProjectTCPRequestHandler) server.project = self.project server.window = self server_thread = threading.Thread(name="projectTCPserver", target=server.serve_forever) # Exit the server thread when the main thread terminates server_thread.daemon = True server_thread.start()
# Seems it is not used and should be in scipion-em # Not within scipion but used from ShowJ
[docs] def schedulePlot(self, path, *args): # FIXME: This import should not be here from pwem.viewers import EmPlotter self.enqueue(lambda: EmPlotter.createFromFile(path, *args).show())
[docs] @classmethod def registerObjectCommand(cls, cmd, func): """ Register an object command to be handled when receiving the action from showj. """ cls._OBJECT_COMMANDS[cmd] = func
[docs] def runObjectCommand(self, cmd, inputStrId, objStrId): try: objId = int(objStrId) project = self.project if os.path.isfile(inputStrId) and os.path.exists(inputStrId): from pwem.utils import loadSetFromDb inputObj = loadSetFromDb(inputStrId) else: inputId = int(inputStrId) inputObj = project.mapper.selectById(inputId) func = self._OBJECT_COMMANDS.get(cmd, None) if func is None: logger.info("Error, command '%s' not found. " % cmd) else: def myfunc(): func(inputObj, objId) inputObj.close() self.enqueue(myfunc) except Exception as ex: logger.error("There was an error executing object command !!!:", exc_info=ex)
[docs] def recalculateCTF(self, inputObjId, sqliteFile): """ Load the project and launch the protocol to create the subset. """ # Retrieve project, input protocol and object from db project = self.project inputObj = project.mapper.selectById(int(inputObjId)) parentProtId = inputObj.getObjParentId() parentProt = project.mapper.selectById(parentProtId) protDep = project._getProtocolsDependencies([parentProt]) if protDep: prot = project.copyProtocol(parentProt) prot.continueRun.set(parentProt) else: prot = parentProt prot.isFirstTime.set(True) # Define the input params of the new protocol prot.recalculate.set(True) prot.sqliteFile.set(sqliteFile) # Launch the protocol self.getViewWidget().executeProtocol(prot)
[docs]class ProjectManagerWindow(ProjectBaseWindow): """ Windows to manage all projects. """ # To allow plugins to add their own menus _pluginMenus = dict() def __init__(self, **kwargs): # TODO: put the menu part more nicely. From here: menu = MenuConfig() fileMenu = menu.addSubMenu('File') fileMenu.addSubMenu('Browse files', 'browse', icon=Icon.FOLDER_OPEN) fileMenu.addSubMenu('Exit', 'exit', icon=Icon.ACTION_OUT) confMenu = menu.addSubMenu('Configuration') if os.path.exists(pw.Config.SCIPION_CONFIG): confMenu.addSubMenu('General', 'general') confMenu.addSubMenu('Hosts', 'hosts') if os.path.exists(pw.Config.SCIPION_PROTOCOLS): confMenu.addSubMenu('Protocols', 'protocols') if os.path.exists(pw.Config.SCIPION_LOCAL_CONFIG): confMenu.addSubMenu('User', 'user') helpMenu = menu.addSubMenu('Help') helpMenu.addSubMenu('Online help', 'online_help', icon=Icon.ACTION_EXPORT) helpMenu.addSubMenu('About', 'about', icon=Icon.ACTION_HELP) self.menuCfg = menu try: title = '%s (%s on %s)' % (Message.LABEL_PROJECTS, pwutils.getLocalUserName(), pwutils.getLocalHostName()) except Exception: title = Message.LABEL_PROJECTS ProjectBaseWindow.__init__(self, title, minsize=(750, 500), icon=Icon.SCIPION_ICON_PROJS, **kwargs) self.manager = pw.project.Manager() self.switchView(VIEW_PROJECTS) # # The next functions are callbacks from the menu options. # See how it is done in pyworkflow/gui/gui.py:Window._addMenuChilds() #
[docs] def onBrowseFiles(self): # File -> Browse files FileBrowserWindow("Browse files", self, pw.Config.SCIPION_USER_DATA, selectButton=None).show()
[docs] def onGeneral(self): # Config -> General self._openConfigFile(pw.Config.SCIPION_CONFIG)
@staticmethod def _openConfigFile(configFile): """ Open an Scipion configuration file, if the user have one defined, also open that one with the defined text editor. """ _open_cmd(configFile)
[docs] @staticmethod def onHosts(): # Config -> Hosts ProjectManagerWindow._openConfigFile(pw.Config.SCIPION_HOSTS)
[docs] @staticmethod def onProtocols(): ProjectManagerWindow._openConfigFile(pw.Config.SCIPION_PROTOCOLS)
[docs] @staticmethod def onUser(): ProjectManagerWindow._openConfigFile(pw.Config.SCIPION_LOCAL_CONFIG)
[docs]class ProjectTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass
[docs]class ProjectTCPRequestHandler(socketserver.BaseRequestHandler):
[docs] def handle(self): try: project = self.server.project window = self.server.window msg = self.request.recv(1024) msg = msg.decode() tokens = shlex.split(msg) if msg.startswith('run protocol'): logger.debug("run protocol messaged arrived: %s" % msg) protocolName = tokens[2] protocolClass = pw.Config.getDomain().getProtocols()[protocolName] # Create the new protocol instance and set the input values protocol = project.newProtocol(protocolClass) for token in tokens[3:]: param, value = token.split('=') attr = getattr(protocol, param, None) if param == 'label': protocol.setObjLabel(value) elif attr.isPointer(): obj = project.getObject(int(value)) attr.set(obj) elif value: attr.set(value) # project.launchProtocol(protocol) # We need to enqueue the action of execute a new protocol # to be run in the same GUI thread and avoid concurrent # access to the project sqlite database window.getViewWidget().executeProtocol(protocol) elif msg.startswith('run function'): functionName = tokens[2] functionPointer = getattr(window, functionName) functionPointer(*tokens[3:]) else: answer = b'no answer available\n' self.request.sendall(answer) except Exception as e: print(e) import traceback traceback.print_stack()