diff --git a/gui/wxpython/gmodeler/g.gui.gmodeler.html b/gui/wxpython/gmodeler/g.gui.gmodeler.html index a6067c19aa8..631c06de9d8 100644 --- a/gui/wxpython/gmodeler/g.gui.gmodeler.html +++ b/gui/wxpython/gmodeler/g.gui.gmodeler.html @@ -39,6 +39,7 @@

DESCRIPTION

  • save model properties to a file (GRASS Model File|*.gxm)
  • export model to Python script
  • export model to Python script in the form of a PyWPS process
  • +
  • export model to an actinia process
  • export model to image file
  • @@ -57,7 +58,7 @@

    Main dialog

    (2) Load model from file, (3) Save current model to file, (4) Export model to image, -(5) Export model to Python script, +(5) Export model to a (Python/PyWPS/actinia) script, (6) Add command (GRASS module) to model, (7) Add data to model, (8) Manually define relation between @@ -82,9 +83,9 @@

    Main dialog

    There is also a lower menu bar in the Graphical modeler dialog where one can manage model items, visualize commands, add or manage model variables, -define default values and descriptions. The Python editor dialog window -allows seeing workflows written in Python code, either as a basic Python -script, or as a PyWPS script. The rightmost tab of the bottom menu is +define default values and descriptions. The Script editor dialog window +allows seeing and exporting workflows as basic Python scripts, as PyWPS +scripts, or as actinia processes. The rightmost tab of the bottom menu is automatically triggered when the model is activated and shows all the steps of running GRASS modeler modules; in the case some errors occur in the calculation process, they are are written at that place. @@ -220,7 +221,7 @@

    Managing model parameters

    -The final model, the list of all model items, and the Python code window with +The final model, the list of all model items, and the Script editor window with Save and Run option are shown in the figures below.

    @@ -237,7 +238,7 @@

    Managing model parameters


    -Figure: Items with Python editor window. +Figure: Items with Script editor window.

    @@ -345,8 +346,8 @@

    Handling intermediate data

    Figure: Usage and definition of intermediate data in model. -

    Using the Python editor

    -By using the Python editor in the Graphical Modeler the user can add Python code and then +

    Using the Script editor

    +By using the Script editor in the Graphical Modeler, the user can add Python code and then run it with Run button or just save it as a Python script *.py. The result is shown in the Figure below: @@ -356,11 +357,11 @@

    Using the Python editor


    -Figure: Python editor in the wxGUI Graphical Modeler. +Figure: Script editor in the wxGUI Graphical Modeler. -In the Script type combobox, the user can also choose a PyWPS export -instead of a basic Python script. A PyWPS process based on the model will be +The second option in the Script type combobox exports a PyWPS script +instead of a basic Python one. A PyWPS process based on the model will be generated then; for the PyWPS script, the Run button is disabled as users are expected to include this script in their web processing service and not to run it on itself. @@ -369,7 +370,20 @@

    Using the Python editor


    -Figure: Python editor in the wxGUI Graphical Modeler - set to PyWPS. +Figure: Script editor in the wxGUI Graphical Modeler - set to PyWPS. + + +The third option in the Script type combobox exports an actinia process +chain (non-parameterized model) or an actinia template (parameterized model). +An actinia JSON based on the model will be generated then; as for the PyWPS +script, the Run button is disabled as users are expected to include this +JSON in their web processing service and not to run it on itself. + +
    + + +
    +Figure: Script editor in the wxGUI Graphical Modeler - set to actinia.

    diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_actinia_code.png b/gui/wxpython/gmodeler/g_gui_gmodeler_actinia_code.png new file mode 100644 index 00000000000..099fcf968c4 Binary files /dev/null and b/gui/wxpython/gmodeler/g_gui_gmodeler_actinia_code.png differ diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_items.png b/gui/wxpython/gmodeler/g_gui_gmodeler_items.png index 976bb125cb9..146a45e89a1 100644 Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_items.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_items.png differ diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_python.png b/gui/wxpython/gmodeler/g_gui_gmodeler_python.png index 6eb219bc475..9c07efd7606 100644 Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_python.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_python.png differ diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_python_code.png b/gui/wxpython/gmodeler/g_gui_gmodeler_python_code.png index 77c28876d9c..1cf220fdc53 100644 Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_python_code.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_python_code.png differ diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_python_code_result.png b/gui/wxpython/gmodeler/g_gui_gmodeler_python_code_result.png index 777ba420027..75db847b647 100644 Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_python_code_result.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_python_code_result.png differ diff --git a/gui/wxpython/gmodeler/g_gui_gmodeler_pywps_code.png b/gui/wxpython/gmodeler/g_gui_gmodeler_pywps_code.png index 0c5acf5f402..e62da627f20 100644 Binary files a/gui/wxpython/gmodeler/g_gui_gmodeler_pywps_code.png and b/gui/wxpython/gmodeler/g_gui_gmodeler_pywps_code.png differ diff --git a/gui/wxpython/gmodeler/model.py b/gui/wxpython/gmodeler/model.py index b33c5b6d4e4..00f670d20cb 100644 --- a/gui/wxpython/gmodeler/model.py +++ b/gui/wxpython/gmodeler/model.py @@ -17,6 +17,7 @@ - model::ModelComment - model::ProcessModelFile - model::WriteModelFile + - model::WriteActiniaFile - model::WritePyWPSFile - model::WritePythonFile - model::ModelParamDialog @@ -27,7 +28,7 @@ (>=v2). Read the file COPYING that comes with GRASS for details. @author Martin Landa -@PyWPS, Python parameterization Ondrej Pesek +@actinia, PyWPS, Python parameterization Ondrej Pesek """ import os @@ -2636,6 +2637,7 @@ def __init__(self, fd, model): self.fd = None self.model = None self.indent = None + self.grassAPI = None # call method_write...() @@ -2701,12 +2703,17 @@ def _writePythonComment(self, item): for line in item.GetLabel().splitlines(): self.fd.write("#" + line + "\n") + def _getParamName(self, parameter_name, item): + return "{module_nickname}_{param_name}".format( + module_nickname=self._getModuleNickname(item), + param_name=parameter_name, + ) + @staticmethod - def _getParamName(parameter_name, item): - return "{module_name}{module_id}_{param_name}".format( + def _getModuleNickname(item): + return "{module_name}{module_id}".format( module_name=re.sub("[^a-zA-Z]+", "", item.GetLabel()), module_id=item.GetId(), - param_name=parameter_name, ) def _getItemFlags(self, item, opts, variables): @@ -2743,6 +2750,155 @@ def _getItemFlags(self, item, opts, variables): return item_true_flags, item_parameterized_flags, item_params +class WriteActiniaFile(WriteScriptFile): + """Class for exporting model to an actinia script.""" + + def __init__(self, fd, model, grassAPI=None): + """Class for exporting model to actinia script.""" + self.fd = fd + self.model = model + self.indent = 2 + + self._writeActinia() + + def _writeActinia(self): + """Write actinia model to file.""" + properties = self.model.GetProperties() + + description = properties["description"] + + self.fd.write( + f"""{{ +{' ' * self.indent * 1}"id": "model", +{' ' * self.indent * 1}"description": "{'""'.join(description.splitlines())}", +{' ' * self.indent * 1}"version": "1", +""" + ) + + parameterized = False + module_list_str = "" + for item in self.model.GetItems(ModelAction): + parameterizedParams = item.GetParameterizedParams() + if len(parameterizedParams["params"]) > 0: + parameterized = True + + module_list_str += self._getPythonAction(item, parameterizedParams) + module_list_str += f"{' ' * self.indent * 3}}},\n" + + if parameterized is True: + self.fd.write(f'{" " * self.indent * 1}"template": {{\n') + self.fd.write( + f"""{' ' * self.indent * 2}"list": [ + """ + ) + else: + self.fd.write( + f"""{' ' * self.indent}"list": [ + """ + ) + + # module_list_str[:-2] to get rid of the trailing comma and newline + self.fd.write(module_list_str[:-2] + "\n") + + if parameterized is True: + self.fd.write(f"{' ' * self.indent * 2}]\n{' ' * self.indent * 1}}}\n}}") + else: + self.fd.write(f"{' ' * self.indent * 1}]\n}}") + + def _getPythonAction(self, item, variables={}, intermediates=None): + """Write model action to Python file""" + task = GUI(show=None).ParseCommand(cmd=item.GetLog(string=False)) + strcmd = f"{' ' * self.indent * 3}{{\n" + + return ( + strcmd + self._getPythonActionCmd(item, task, len(strcmd), variables) + "\n" + ) + + def _getPythonActionCmd(self, item, task, cmdIndent, variables={}): + opts = task.get_options() + + ret = "" + parameterizedParams = [v["name"] for v in variables["params"]] + + flags, itemParameterizedFlags, params = self._getItemFlags( + item, opts, variables + ) + inputs = [] + outputs = [] + + if len(itemParameterizedFlags) > 0: + dlg = wx.MessageDialog( + self.model.canvas, + message=_( + f"Module {task.get_name()} in your model contains " + f"parameterized flags. actinia does not support " + f"parameterized flags. The following flags are therefore " + f"not being written in the generated json: " + f"{itemParameterizedFlags}" + ), + caption=_("Warning"), + style=wx.OK_DEFAULT | wx.ICON_WARNING, + ) + dlg.ShowModal() + dlg.Destroy() + + for p in opts["params"]: + name = p.get("name", None) + value = p.get("value", None) + + if (name and value) or (name in parameterizedParams): + + if name in parameterizedParams: + parameterizedParam = self._getParamName(name, item) + default_val = p.get("value", "") + + if len(default_val) > 0: + parameterizedParam += f"|default({default_val})" + + value = f"{{{{ {parameterizedParam} }}}}" + + param_string = f'{{"param": "{name}", "value": "{value}"}}' + age = p.get("age", "old") + if age == "new": + outputs.append(param_string) + else: + inputs.append(param_string) + + ret += f'{" " * self.indent * 4}"module": "{task.get_name()}",\n' + ret += f'{" " * self.indent * 4}"id": "{self._getModuleNickname(item)}",\n' + + # write flags + if flags: + ret += f'{" " * self.indent * 4}"flags": "{flags}",\n' + + # write inputs and outputs + if len(inputs) > 0: + ret += self.write_params("inputs", inputs) + else: + ret += "}" + + if len(outputs) > 0: + ret += self.write_params("outputs", outputs) + + # ret[:-2] to get rid of the trailing comma and newline + # (to make the json valid) + return ret[:-2] + + def write_params(self, param_type, params): + """Write the full list of parameters of one type. + + :param param_type: type of parameters (inputs or outputs) + :params: list of the parameters + """ + ret = f'{" " * self.indent * 4}"{param_type}": [\n' + for opt in params[:-1]: + ret += f"{' ' * self.indent * 5}{opt},\n" + ret += f"{' ' * self.indent * 5}{params[-1]}\n" + ret += f"{' ' * self.indent * 4}],\n" + + return ret + + class WritePyWPSFile(WriteScriptFile): """Class for exporting model to PyWPS script.""" @@ -2786,7 +2942,7 @@ def __init__(self): """ # noqa: E501 ) - for item in self.model.GetItems(): + for item in self.model.GetItems(ModelAction): self._write_input_outputs(item, self.model.GetIntermediateData()[:3]) self.fd.write( diff --git a/gui/wxpython/gmodeler/panels.py b/gui/wxpython/gmodeler/panels.py index 76a5237448e..7a2762ab4ce 100644 --- a/gui/wxpython/gmodeler/panels.py +++ b/gui/wxpython/gmodeler/panels.py @@ -75,6 +75,7 @@ WriteModelFile, ModelDataSeries, ModelDataSingle, + WriteActiniaFile, WritePythonFile, WritePyWPSFile, ) @@ -185,7 +186,7 @@ def __init__( page=self.variablePanel, text=_("Variables"), name="variables" ) self.notebook.AddPage( - page=self.pythonPanel, text=_("Python editor"), name="python" + page=self.pythonPanel, text=_("Script editor"), name="python" ) self.notebook.AddPage( page=self.goutput, text=_("Command output"), name="output" @@ -1036,8 +1037,25 @@ def OnExportImage(self, event): dlg.Destroy() def OnExportPython(self, event=None, text=None): - """Export model to Python script""" + """Export model to Python script.""" + self.pythonPanel.SetWriteObject("Python") + self.ExportScript() + + def OnExportPyWPS(self, event=None, text=None): + """Export model to PyWPS script.""" + self.pythonPanel.SetWriteObject("PyWPS") + self.ExportScript() + + def OnExportActinia(self, event=None, text=None): + """Export model to actinia script.""" + self.pythonPanel.SetWriteObject("actinia") + self.ExportScript() + + def ExportScript(self): + """Export model to script.""" + orig_script_type = self.pythonPanel.body.script_type filename = self.pythonPanel.SaveAs(force=True) + self.pythonPanel.SetWriteObject(orig_script_type) self.SetStatusText(_("Model exported to <%s>") % filename) def OnPreferences(self, event): @@ -1606,6 +1624,7 @@ def __init__(self, parent, id=wx.ID_ANY, **kwargs): choices=[ _("Python"), _("PyWPS"), + _("actinia"), ], ) self.script_type_box.SetSelection(0) # Python @@ -1644,6 +1663,30 @@ def _layout(self): sizer.SetSizeHints(self) self.SetSizer(sizer) + def GetScriptExt(self): + """Get extension for script exporting. + :return: script extension + """ + if self.write_object == WriteActiniaFile: + ext = "json" + else: + # Python, PyWPS + ext = "py" + + return ext + + def SetWriteObject(self, script_type): + """Set correct self.write_object dependng on the script type. + :param script_type: script type name as a string + """ + if script_type == "PyWPS": + self.write_object = WritePyWPSFile + elif script_type == "actinia": + self.write_object = WriteActiniaFile + else: + # script_type == "Python", fallback + self.write_object = WritePythonFile + def RefreshScript(self): """Refresh the script. @@ -1693,12 +1736,18 @@ def SaveAs(self, force=False): :return: filename """ filename = "" + file_ext = self.GetScriptExt() + if file_ext == "py": + fn_wildcard = _("Python script (*.py)|*.py") + elif file_ext == "json": + fn_wildcard = _("JSON file (*.json)|*.json") + dlg = wx.FileDialog( parent=self, message=_("Choose file to save"), defaultFile=os.path.basename(self.parent.GetModelFile(ext=False)), defaultDir=os.getcwd(), - wildcard=_("Python script (*.py)|*.py"), + wildcard=fn_wildcard, style=wx.FD_SAVE, ) @@ -1709,8 +1758,8 @@ def SaveAs(self, force=False): return "" # check for extension - if filename[-3:] != ".py": - filename += ".py" + if filename[-len(file_ext) - 1 :] != f".{file_ext}": + filename += f".{file_ext}" if os.path.exists(filename): dlg = wx.MessageDialog( @@ -1780,10 +1829,8 @@ def OnDone(self, event): def OnChangeScriptType(self, event): new_script_type = self.script_type_box.GetStringSelection() - if new_script_type == "Python": - self.write_object = WritePythonFile - elif new_script_type == "PyWPS": - self.write_object = WritePyWPSFile + + self.SetWriteObject(new_script_type) if self.RefreshScript(): self.body.script_type = new_script_type @@ -1795,11 +1842,9 @@ def OnChangeScriptType(self, event): self.script_type_box.SetStringSelection(self.body.script_type) if self.body.script_type == "Python": - self.write_object = WritePythonFile self.btnRun.Enable() self.btnRun.SetToolTip(_("Run script")) - elif self.body.script_type == "PyWPS": - self.write_object = WritePyWPSFile + elif self.body.script_type in ("PyWPS", "actinia"): self.btnRun.Disable() self.btnRun.SetToolTip( _("Run script - enabled only for basic Python scripts") diff --git a/gui/wxpython/xml/menudata_modeler.xml b/gui/wxpython/xml/menudata_modeler.xml index cf6d1b3e444..9102b524541 100644 --- a/gui/wxpython/xml/menudata_modeler.xml +++ b/gui/wxpython/xml/menudata_modeler.xml @@ -43,6 +43,16 @@ OnExportPython Ctrl+Alt+P + + + Export model to PyWPS script + OnExportPyWPS + + + + Export model to actinia script + OnExportActinia +