# -*- coding: utf-8 -*-
#!/usr/bin/env python
# **************************************************************************
# *
# * Authors: Pablo Conesa (pconesa@cnb.csic.es)
# *
# * Unidad de Bioinformatica of Centro Nacional de Biotecnologia , CSIC
# *
# * 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, write to the Free Software
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
# * 02111-1307 USA
# *
# * All comments concerning this program package may be sent to the
# * e-mail address 'scipion@cnb.csic.es'
# *
# **************************************************************************
"""
Creates a scipion workflow file (json formatted) base on a template.
The template may have some ~placeholders~ that will be overwritten with values
Template may look like this, separator is "~" and within it you can define:
~title|value|type~
Template string sits at the end of the file ready for a running streaming demo.
"""
import subprocess
import sys
import os
import re
import tkinter as tk
import tkinter.font as tkFont
import traceback
import time
import pyworkflow as pw
from pyworkflow.config import VarTypes
import pyworkflow.utils as pwutils
from pyworkflow.gui import Message, dialog, askPath
from pyworkflow.project import Project
import pyworkflow.gui as pwgui
from pyworkflow.gui.project.base import ProjectBaseWindow
from pyworkflow.gui.widgets import HotButton, Button
from pyworkflow.template import TemplateList, LocalTemplate, Template
from scipion.constants import SCIPION_EP, MODE_PROJECT
# Custom labels
from scipion.utils import getExternalJsonTemplates
from pyworkflow.utils import Icon
ENTRY = 'entry'
LABEL = 'label'
CHECKBUTTON = 'Checkbutton'
YES = "Yes"
NO = "No"
FLAG_PARAM = "--"
NOGUI_FLAG = FLAG_PARAM + "nogui"
NOSCHEDULE_FLAG = FLAG_PARAM + "noschedule"
ACCEPT_BUTTON = "Accept"
LEN_LABEL_IN_CHARS = 30
LABEL_ALIGN_PATTERN = '{:>%s}' % LEN_LABEL_IN_CHARS
LEN_BUTTON_IN_CHARS = 12
BUTTON_ALIGN_PATTERN = "{:^%s}" % LEN_BUTTON_IN_CHARS
FIELD_SEP = '~'
VIEW_WIZARD = 'wizardview'
# Project name files
PROJECT_NAME = 'Project name'
DO_NOT_SCHEDULE = "Cancel schedule"
DO_NOT_SHOW_GUI = "Don't show the project"
# Project regex to validate the session id name
PROJECT_PATTERN = "^\w{2}\d{4,6}-\d+$"
PROJECT_REGEX = re.compile(PROJECT_PATTERN)
[docs]class KickoffWindow(ProjectBaseWindow):
""" Windows to manage all projects. """
def __init__(self, template, argsList, showScheduleOption, schedule,
showProjectOption, showProject, showProjectName):
try:
title = '%s (%s on %s)' % ('Workflow template customizer',
pwutils.getLocalUserName(),
pwutils.getLocalHostName())
except Exception:
title = Message.LABEL_PROJECTS
ProjectBaseWindow.__init__(self, title, minsize=(800, 350))
self.template = template
self.argsList = argsList
self.showScheduleOption = showScheduleOption
self.schedule = schedule
self.showProjectOption = showProjectOption
self.showProject = showProject
self.showProjectName = showProjectName
self.viewFuncs = {VIEW_WIZARD: KickoffView}
self.action = Message.LABEL_BUTTON_CANCEL
self.switchView(VIEW_WIZARD)
[docs] def getTemplate(self):
return self.template
[docs] def getAction(self):
return self.action
[docs] def switchView(self, newView, **kwargs):
# Destroy the previous view if exists:
if self.viewWidget:
self.viewWidget.grid_forget()
self.viewWidget.destroy()
# Create the new view: Instantiates KickoffView HERE!.
self.viewWidget = self.viewFuncs[newView](self.footer, self,
template=self.template,
argsList=self.argsList,
showScheduleOption=self.showScheduleOption,
schedule=self.schedule,
showProjectOption=self.showProjectOption,
showProject=self.showProject,
showProjectName=self.showProjectName)
# Grid in the second row (1)
self.viewWidget.grid(row=0, column=0, columnspan=10, sticky='news')
self.footer.rowconfigure(0, weight=1)
self.footer.columnconfigure(0, weight=1)
self.view = newView
def _onClosing(self):
self.root.destroy()
if self.showScheduleOption and self.showProjectOption:
sys.exit()
[docs]class KickoffView(tk.Frame):
def __init__(self, parent, window, template:Template=None, argsList=[],
showScheduleOption=True, schedule=True, showProjectOption=True,
showProject=True, showProjectName=True, **kwargs):
tk.Frame.__init__(self, parent, bg='white', **kwargs)
self.window = window
self.root = window.root
self.vars = {}
self.checkvars = []
self.template = template
self.argsList = argsList
self.showScheduleOption = showScheduleOption
self.schedule = schedule
self.showProjectOption = showProjectOption
self.showProject = showProject
self.showProjectName = showProjectName
bigSize = pw.Config.SCIPION_FONT_SIZE + 2
smallSize = pw.Config.SCIPION_FONT_SIZE - 2
fontName = pw.Config.SCIPION_FONT_NAME
self.bigFont = tkFont.Font(size=bigSize, family=fontName)
self.bigFontBold = tkFont.Font(size=bigSize, family=fontName,
weight='bold')
self.projDateFont = tkFont.Font(size=smallSize, family=fontName)
self.projDelFont = tkFont.Font(size=smallSize, family=fontName,
weight='bold')
# Body section
bodyFrame = tk.Frame(self, bg='white')
bodyFrame.columnconfigure(0, minsize=120)
bodyFrame.columnconfigure(1, minsize=120, weight=1)
bodyFrame.grid(row=0, column=0, sticky='news')
self._fillContent(bodyFrame)
# Add the create project button
btnFrame = tk.Frame(self, bg='white')
btn = HotButton(btnFrame, text=ACCEPT_BUTTON,
font=self.bigFontBold,
command=self._onReadDataFromTemplateForm)
btn.grid(row=0, column=1, sticky='ne', padx=10, pady=10)
# Add the Import project button
btn = Button(btnFrame, Message.LABEL_BUTTON_CANCEL,
font=self.bigFontBold,
command=self._closeCallback)
btn.grid(row=0, column=0, sticky='ne', pady=10)
btnFrame.columnconfigure(0, weight=1)
btnFrame.grid(row=1, column=0, sticky='news')
self.columnconfigure(0, weight=1)
def _closeCallback(self):
self.root.destroy()
if self.showScheduleOption and self.showProjectOption:
sys.exit()
def _fillContent(self, frame):
# Add project name
self.template.genProjectName()
self._addPair(PROJECT_NAME, PROJECT_NAME, 1, frame,
value=self.template.projectName,
visible=self.showProjectName)
self._addPair(DO_NOT_SCHEDULE, DO_NOT_SCHEDULE, 2, frame,
varType=VarTypes.BOOLEAN.value, value=not self.schedule,
visible=self.showScheduleOption)
self._addPair(DO_NOT_SHOW_GUI, DO_NOT_SHOW_GUI, 3, frame,
varType=VarTypes.BOOLEAN.value, value=not self.showProject,
pady=(5, 30), visible=self.showProjectOption)
# Add template params
self._addTemplateFieldsToForm(frame)
def _addPair(self, text, title, r, lf, varType=None,
value=None, pady=2, visible=True):
if visible:
label = tk.Label(lf, text=text, bg='white', font=self.bigFont)
label.grid(row=r, column=0, padx=(10, 5), pady=pady, sticky='nes')
var = tk.StringVar()
if value is not None:
var.set(value)
self.vars[title] = var
if visible:
if varType is None:
widget = tk.Label(lf, font=self.bigFont, textvariable=var)
elif varType == VarTypes.BOOLEAN.value:
var.set(YES if value else NO)
widget = tk.Checkbutton(lf, text="", font=self.bigFont, variable=var,
onvalue=YES, offvalue=NO, bg=pw.Config.SCIPION_BG_COLOR)
else:
widget = tk.Entry(lf, width=30, font=self.bigFont,
textvariable=var)
widget.grid(row=r, column=1, sticky='news', padx=(5, 10), pady=pady)
if varType in (VarTypes.FOLDER.value, VarTypes.PATH.value):
def searchPath(onlyFolders):
result= askPath(path=var.get(), onlyFolders=onlyFolders, master=self.window)
var.set(result)
btn = tk.Label(lf, text="", font=self.bigFont, image=self.window.getImage(Icon.FOLDER), bg=pw.Config.SCIPION_BG_COLOR)
btn.bind('<Button-1>', lambda e: searchPath(varType == VarTypes.FOLDER.value))
btn.grid(row=r,column=2)
def _addTemplateFieldsToForm(self, labelFrame):
row = 5
for field in self.template.params.values():
alias = field.getAlias()
text = field.getTitle() if alias is None else "%s (%s)" % (field.getTitle(), alias)
self._addPair(text, field.getTitle(), row, labelFrame,
varType=field.getType(), value=field.getValue())
row += 1
def _getVar(self, varKey):
return self.vars[varKey]
def _getValue(self, varKey):
return self.vars[varKey].get()
def _setValue(self, varKey, value):
return self.vars[varKey].set(value)
# noinspection PyUnusedLocal
def _onReadDataFromTemplateForm(self, e=None):
errors = []
# Check the entered data
for field in self.template.params.values():
newValue = self._getValue(field.getTitle())
field.setValue(newValue)
validationMsg = field.validate()
if validationMsg:
errors.append("%s value does not validate: %s. Value: %s, Type: %s."
% (field.getTitle(), validationMsg, field.getValue(),
field.getType()))
# Do more checks only if there are no previous errors
if errors:
errors.insert(0, "*Errors*:")
self.window.showError("\n - ".join(errors))
else:
self.template.projectName = self._getValue(PROJECT_NAME)
# Set parent with the data
self.window.template = self.template
if self._getValue(DO_NOT_SCHEDULE) == YES:
self.argsList.append(NOSCHEDULE_FLAG)
if self._getValue(DO_NOT_SHOW_GUI) == YES:
self.argsList.append(NOGUI_FLAG)
self.window.action = ACCEPT_BUTTON
self.window.root.quit()
self.window.root.withdraw()
return
[docs]def getTemplates(templateName=None):
""" Get a template or templates either from arguments
or from the templates directory.
If more than one template is found or passed, a dialog is raised
to choose one.
"""
tempList = TemplateList()
tempId = None
if templateName:
if os.path.isfile(templateName) and os.path.exists(templateName):
t = LocalTemplate("custom_template", templateName)
tempList.addTemplate(t)
else:
tempId = templateName
# Try to find all templates from the template folder and the plugins
if len(tempList.templates) == 0:
tempList.addScipionTemplates(tempId)
if not (tempId is not None and len(tempList.templates) == 1):
tempList.addPluginTemplates(tempId)
if not len(tempList.templates):
raise Exception("No valid file found (*.json.template).\n"
"Please, add (at least one) at %s "
"or pass it as argument(s).\n"
"\n -> Usage: scipion template [PATH.json.template]\n"
"\n see 'scipion help'\n" % getExternalJsonTemplates())
return tempList.sortListByPluginName().templates
[docs]def chooseTemplate(templates, parentWindow=None):
chosenTemplate = None
if len(templates) == 1:
chosenTemplate = templates[0]
else:
provider = pwgui.tree.ListTreeProviderTemplate(templates)
dlg = dialog.ListDialog(parentWindow, "Workflow templates", provider,
"Select one of the templates.",
selectOnDoubleClick=True)
if dlg.result == dialog.RESULT_YES:
chosenTemplate = dlg.values[0]
if chosenTemplate is not None:
print("Template to use: %s" % chosenTemplate)
# Replace environment variables
chosenTemplate.replaceEnvVariables()
return chosenTemplate
[docs]def resolveTemplate(template, argsList, showScheduleOption=True, schedule=True,
showProjectOption=True, showProject=True, showProjectName=True):
""" Resolve a template assigning CML params to the template.
if not enough, a window will pop pup to ask for missing ones only"""
if not assignAllParams(argsList, template):
wizWindow = KickoffWindow(template=template,
argsList=argsList,
showScheduleOption=showScheduleOption,
schedule=schedule,
showProjectOption=showProjectOption,
showProject=showProject,
showProjectName=showProjectName)
wizWindow.show()
return wizWindow.action == ACCEPT_BUTTON
else:
# All parameters have been assigned and template is fully populated
# Add schedule and showProject flags back: removed at flag2value
if not schedule:
argsList.append(NOSCHEDULE_FLAG)
if not showProject:
argsList.append(NOGUI_FLAG)
return True
[docs]def assignAllParams(argsList, template):
"""
Assign CML params to the template, if missing params after assignment
return False
"""
paramsSetted = 0
template.parseContent()
if argsList:
for attr in argsList:
# skipp --params
if attr.startswith(FLAG_PARAM):
continue
aliasAttr, valAttr = attr.split('=')
try:
paramsSetted += template.setParamValue(aliasAttr, valAttr)
except Exception as e:
print(pwutils.redStr(e))
sys.exit(os.EX_DATAERR)
return len(template.params) == paramsSetted
[docs]def createTemplateFile(template):
try:
workflow = template.createTemplateFile()
except Exception as e:
workflow = None
errorStr = "Couldn't create the template.\n" + str(e)
print(errorStr)
traceback.print_exc()
return workflow
[docs]def launchTemplate(argsList, template):
""" Launches a resolved template"""
workflow = createTemplateFile(template)
if workflow is not None:
# Create the project
if not template.projectName:
template.genProjectName()
createProjectFromWorkflow(workflow, template.projectName, argsList)
[docs]def importTemplate(template, window):
"""
Import a resolved template
"""
workflow = createTemplateFile(template)
if workflow is not None:
try:
window.getViewWidget().info('Importing workflow %s' % workflow)
window.project.loadProtocols(workflow)
window.getViewWidget().updateRunsGraph(True)
window.getViewWidget().cleanInfo()
except Exception as ex:
window.showError(str(ex), exception=ex)
[docs]def createProjectFromWorkflow(workflow, projectName, argsList):
scipion = SCIPION_EP
scriptsPath = pw.join('project', 'scripts')
# Clean the project name as pyworkflow will do
projectName = Project.cleanProjectName(projectName)
# Create the project
print("Creating project %s" % projectName)
createProjectScript = os.path.join(scriptsPath, 'create.py')
os.system("python -m %s python %s %s %s" % (scipion, createProjectScript, projectName, workflow))
# Wait 2 seconds to avoid activity
time.sleep(2)
if scheduleProject(argsList):
# Schedule the project
scheduleProjectScript = os.path.join(scriptsPath, 'schedule.py')
print("Scheduling project %s" % projectName)
subprocess.Popen(["python", "-m", scipion, "python", scheduleProjectScript, projectName])
# Wait 5 seconds to avoid activity
time.sleep(5)
if launchGUI(argsList):
print("Showing project %s" % projectName)
# Launch scipion
subprocess.Popen(["python", "-m", scipion, MODE_PROJECT, projectName])
[docs]def flag2Value(argsList, flag):
# Remove the flag from sys.argsv
value = getFlagArg(argsList, flag)
if value:
argsList.remove(flag)
return value
[docs]def launchGUI(argsList):
"""Checks if project GUI has to be launched. Only if --noGUI param is
found in the argument List it will return False"""
return not getFlagArg(argsList, NOGUI_FLAG)
[docs]def scheduleProject(argsList):
return not getFlagArg(argsList, NOSCHEDULE_FLAG)
[docs]def getFlagArg(argsList, flag):
"""Checks if a flag exists (True) or not (False)"""
for arg in argsList:
if flag == arg.lower():
return True
# Flag not found
return False
[docs]def main():
""" Resolves command line arguments for scipion template"""
# Remove "template"
argsList = sys.argv[1:]
# Now, there 2 cases:
# 1.- it comes with a template name (full path) or id (name)
# 2.- is empty
templateName = argsList[0] if argsList else None
templates = getTemplates(templateName)
# Remove the name from the args, in case it is passed
argsList = argsList[1:]
chosenTemplate = chooseTemplate(templates)
if chosenTemplate is not None and resolveTemplate(chosenTemplate, argsList,
schedule=not flag2Value(argsList, NOSCHEDULE_FLAG),
showProject=not flag2Value(argsList, NOGUI_FLAG)):
launchTemplate(argsList, chosenTemplate)
if __name__ == "__main__":
main()