Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 56 additions & 13 deletions StandaloneGUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ class StandaloneGUI(wx.Frame):
Args:
wx (wx.Frame): GUI to configure the exportation of the zip file.
"""

last_opened_directory = "." # Class variable to store the last opened directory

def __init__(self, *args, **kw):
"""Constructor.
"""
Expand All @@ -113,17 +116,19 @@ def InitUI(self):
icon.CopyFromBitmap(wx.Bitmap(os.path.join(ICON_PATH_16_16, "properties.png"), wx.BITMAP_TYPE_ANY))
self.SetIcon(icon)

self.SetSize((-1, 220))
self.SetSize((-1, 250))
panel = wx.Panel(self)

vbox = wx.BoxSizer(wx.VERTICAL)

### Zip name field
hbox1 = wx.BoxSizer(wx.HORIZONTAL)
self.st1 = wx.StaticText(panel, label=_('Filename:'))
self.st1.SetToolTip(_("Select a name for the standalone package"))

hbox1.Add(self.st1, flag=wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=8)
self._tc = wx.TextCtrl(panel, -1, "devsimpy-nogui-pkg.zip", style=wx.TE_RICH2|wx.BORDER_NONE, validator=ZipNameValidator())
self._tc.SetToolTip(_("Must end with .zip"))

hbox1.Add(self._tc, proportion=1)
vbox.Add(hbox1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, border=10)
Expand All @@ -135,17 +140,39 @@ def InitUI(self):

### Zip directory field
hbox2 = wx.BoxSizer(wx.HORIZONTAL)
self._dbb = filebrowse.DirBrowseButton(panel, -1, size=(450, -1))
self._dbb = filebrowse.DirBrowseButton(panel,
-1,
size=(450, -1),
labelText = "Select Directory:",
toolTip=_("Select a target directory to create the standalone package"))
# Set the initial directory
self._dbb.startDirectory = StandaloneGUI.last_opened_directory

hbox2.Add(self._dbb)
vbox.Add(hbox2, flag=wx.LEFT | wx.TOP, border=10)
vbox.Add((-1, 10))

### minimal (optimal) or full export
hbox3 = wx.BoxSizer(wx.HORIZONTAL)

label_format = wx.StaticText(panel, -1, _("Format:"))
label_format.SetToolTip("""The minimal format includes in the archive only the necessary library files\n (may lead to errors depending on your model's architecture).\n The full format includes all library dependencies (less optimal but more secure).""")
self.format = wx.Choice(panel, -1, choices=["Minimal", "Full"])
self.format.SetSelection(0)

hbox3.Add(label_format, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL)
hbox3.Add(self.format, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=10)
vbox.Add(hbox3, flag=wx.LEFT, border=10)
vbox.Add((-1, 10))

### Options
hbox4 = wx.BoxSizer(wx.HORIZONTAL)
self._cb1 = wx.CheckBox(panel, label=_('Add Simulation Kernel'))
self._cb1.SetToolTip(_("Add all files needed to simulate the model."))
self._cb2 = wx.CheckBox(panel, label=_('Add Docker File'))
self._cb3 = wx.CheckBox(panel, label=_('No Time Limit'))
self._cb2.SetToolTip(_("Add Docker file to execute the simulation in a nogui model in a Docker Container."))
self._cb3 = wx.CheckBox(panel, label=_('Add NTL Flag'))
self._cb3.SetToolTip(_("Add the flag -ntl to the simulation in order to define an infinit simulation loop."))

hbox4.Add(self._cb1)
hbox4.Add(self._cb2, flag=wx.LEFT, border=10)
Expand All @@ -156,13 +183,15 @@ def InitUI(self):

hbox5 = wx.BoxSizer(wx.HORIZONTAL)
self._cb4 = wx.CheckBox(panel, label=_('Real Time'))
self._cb4.Enable(False)
self.kernel = wx.Choice(panel, -1, choices=["PyDEVS", "PyPDEVS"])
self.kernel.SetSelection(0)
self.kernel.Enable(False)
label = wx.StaticText(panel, -1, _("Kernel:"))

self.kernel_label = wx.StaticText(panel, -1, _("Kernel:"))
self.kernel_label.Enable(False)

box = wx.BoxSizer(wx.HORIZONTAL)
box.Add(label, 0, wx.ALIGN_CENTER_VERTICAL)
box.Add(self.kernel_label, 0, wx.ALIGN_CENTER_VERTICAL)
box.Add(self.kernel, 0, wx.ALIGN_CENTER_VERTICAL, border=5)

hbox5.Add(box)
Expand All @@ -174,24 +203,36 @@ def InitUI(self):
### Buttons
hbox5 = wx.BoxSizer(wx.HORIZONTAL)
btn1 = wx.Button(panel, wx.ID_OK, _("Ok"), size=(70, 30))
btn2 = wx.Button(panel, wx.ID_CLOSE, _("Close"), size=(70, 30))
hbox5.Add(btn2)
# btn2 = wx.Button(panel, wx.ID_CLOSE, _("Close"), size=(70, 30))
# hbox5.Add(btn2)
hbox5.Add(btn1, flag=wx.LEFT|wx.BOTTOM)
vbox.Add(hbox5, flag=wx.ALIGN_RIGHT|wx.RIGHT, border=10)

panel.SetSizer(vbox)

### Binds
self.Bind(wx.EVT_BUTTON, self.OnOk, id=btn1.GetId())
self.Bind(wx.EVT_BUTTON, self.OnClose, id=btn2.GetId())
self.Bind(wx.EVT_CHECKBOX,self.onChecked, id=self._cb1.GetId())
# self.Bind(wx.EVT_BUTTON, self.OnClose, id=btn2.GetId())
self.Bind(wx.EVT_CHECKBOX,self.OnChecked, id=self._cb1.GetId())
self.Bind(wx.EVT_CHOICE, self.OnChoice, id=self.kernel.GetId())
self.Bind(wx.EVT_DIRPICKER_CHANGED, self.OnDirChanged, id=self._dbb.GetId())


def SetYAML(self,yaml:str)->None:
""" Set the yaml file
"""
self.yaml = yaml

def onChecked(self, event):
def OnDirChanged(self, event):
# Get the selected directory
selected_dir = event.GetPath()
StandaloneGUI.last_opened_directory = selected_dir

def OnChoice(self, event):
selected_option = self.kernel.GetStringSelection()
self._cb4.Enable(selected_option == 'PyPDEVS')

def OnChecked(self, event):
"""Add Simulation Kernel Checkbox has been clicked.

Args:
Expand All @@ -201,6 +242,7 @@ def onChecked(self, event):
# cb.GetLabel(),' is clicked',cb.GetValue()

self.kernel.Enable(cb.IsChecked())
self.kernel_label.Enable(cb.IsChecked())

@BuzyCursorNotification
def OnOk(self, event):
Expand All @@ -212,6 +254,7 @@ def OnOk(self, event):
### call validator for zip name textctrl
if self._tc.GetValidator().Validate(self._tc):

format = self.format.GetString(self.format.GetSelection())
zip_name = self._tc.GetLineText(0)
zip_dir = self._dbb.GetValue()
sim_cb = self._cb1.GetValue()
Expand All @@ -221,7 +264,7 @@ def OnOk(self, event):
kernel = self.kernel.GetString(self.kernel.GetSelection())

### call the StandaloneNoGui class to build the package depending on the settings from the frame.
standalone = StandaloneNoGUI(self.yaml,zip_name,outdir=zip_dir,add_sim_kernel=sim_cb,add_dockerfile=docker_cb,sim_time=ntl_cb,rt=rt, kernel=kernel)
standalone = StandaloneNoGUI(self.yaml,zip_name,format=format,outdir=zip_dir,add_sim_kernel=sim_cb,add_dockerfile=docker_cb,sim_time=ntl_cb,rt=rt, kernel=kernel)

### Try to build de zip package of the standalone version of DEVSimPy-nogui
if standalone.BuildZipPackage():
Expand Down
100 changes: 69 additions & 31 deletions StandaloneNoGUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
#
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##

import os
import os, sys
import subprocess
import zipfile
import zipfile
import configparser
Expand Down Expand Up @@ -73,7 +74,22 @@ def get_domain_path()->str:
return os.path.abspath(built_in['DOMAIN_PATH'])
else:
return "Domain/"


def execute_script(yaml):
# Execute the script using subprocess.Popen
process = subprocess.Popen(["python", "devsimpy-nogui.py", f"{os.path.abspath(yaml)}","10"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
_, stderr = process.communicate()

# Check if the execution was successful
if process.returncode == 0:
# Get the used packages from sys.modules
used_packages = {mod.split('.')[0] for mod in sys.modules.keys() if '.' in mod}
return True, used_packages
else:
# Print error message and return empty set
print(f"Error executing script: {stderr.decode()}")
return False, set()

class StandaloneNoGUI:

### list of files to zip
Expand All @@ -84,12 +100,13 @@ class StandaloneNoGUI:
## list of dir to zip
DIRNAMES = ["DomainInterface/","Mixins/","Patterns/"]

def __init__(self, yaml:str="", outfn:str="devsimpy-nogui-pkg.zip", outdir:str=os.getcwd(), add_sim_kernel:bool=True, add_dockerfile:bool=False, sim_time:str="ntl", rt:bool=False, kernel:str='PyDEVS'):
def __init__(self, yaml:str="", outfn:str="devsimpy-nogui-pkg.zip", format:str="Minimal", outdir:str=os.getcwd(), add_sim_kernel:bool=True, add_dockerfile:bool=False, sim_time:str="ntl", rt:bool=False, kernel:str='PyDEVS'):
""" Generates the zip file with all files needed to execute the devsimpy-nogui script.

Args:
yaml (str): yaml file to zip (optional)
outfn (str): zip file to export all files
format (str): Minimal export only necessary dependancies while Full export all dependancies (less optimal but more secure)
outdir (str): directory where zip file is generated
add_sim_kernel (bool): zip the simlation kernel
add_dockerfile (bool): zip the DockerFile file
Expand All @@ -101,6 +118,7 @@ def __init__(self, yaml:str="", outfn:str="devsimpy-nogui-pkg.zip", outdir:str=o
### local copy
self.yaml = yaml
self.outfn = outfn
self.format = format
self.outdir = outdir
self.add_sim_kernel = add_sim_kernel
self.add_dockerfile = add_dockerfile
Expand Down Expand Up @@ -128,7 +146,7 @@ def GetDockerSpec(self):
"""
"""
return f"""
FROM python:3.8-slim-buster
FROM python:3.10-slim-buster

WORKDIR /app

Expand Down Expand Up @@ -192,32 +210,32 @@ def BuildZipPackage(self) -> None:
###
###################################################################

### add the Domain libairies according to the DOAMIN_PATH var
yaml = YAMLHandler(path)

### to not insert two times the same file
added_files = set()
### lib_path is the directory of the library involved in the yaml model
for path in yaml.extractPythonPaths():
lib_path = os.path.dirname(path)
if lib_path.endswith(('.amd','.cmd')):
lib_path = os.path.dirname(lib_path)

### format the path of the library to include in the archive
lib_name = os.path.basename(os.path.dirname(lib_path))
for file in retrieve_file_paths(lib_path):
if file.endswith(('.py', '.amd', '.cmd')) and '__pycache__' not in file:
relative_path = 'Domain'+file.split(lib_name)[1]
if relative_path not in added_files:
archive.write(file, arcname=relative_path)
added_files.add(relative_path)

### To include all Domain dir
# for file in retrieve_file_paths(self.domain_path):
# if file.endswith(('.py', '.amd', '.cmd')) and \
# '__pycache__' not in file and \
# os.path.basename(os.path.dirname(file)) in lib_to_inculde:
# archive.write(file, arcname='Domain'+os.path.join(file.split('Domain')[1], os.path.basename(file)))
if self.format == "Minimal":
### add the Domain libairies according to the DOAMIN_PATH var
yaml = YAMLHandler(path)

### to not insert two times the same file
added_files = set()
### lib_path is the directory of the library involved in the yaml model
for path in yaml.extractPythonPaths():
lib_path = os.path.dirname(path)
if lib_path.endswith(('.amd','.cmd')):
lib_path = os.path.dirname(lib_path)

### format the path of the library to include in the archive
lib_name = os.path.basename(os.path.dirname(lib_path))
for file in retrieve_file_paths(lib_path):
if file.endswith(('.py', '.amd', '.cmd')) and '__pycache__' not in file:
relative_path = 'Domain'+file.split(lib_name)[1]
if relative_path not in added_files:
archive.write(file, arcname=relative_path)
added_files.add(relative_path)
else:
## To include all Domain dir
for file in retrieve_file_paths(self.domain_path):
if file.endswith(('.py', '.amd', '.cmd')) and \
'__pycache__' not in file:
archive.write(file, arcname='Domain'+os.path.join(file.split('Domain')[1], os.path.basename(file)))

###################################################################
###
Expand Down Expand Up @@ -260,7 +278,27 @@ def BuildZipPackage(self) -> None:

### write config file
archive.writestr("config.json", self.GetConfigSpec())



###################################################################
###
### Requierements files
###
###################################################################

### TODO: include the self.format condition to choose if you want a minimal or full dependencies

# Example: Execute a script and get the used packages
success, packages = execute_script(self.yaml)

if success:
print("Used packages:")
for package in packages:
print(package)
else:
print("Script execution failed.")


### add requirements-nogui.txt file
### TODO: packages used in models include in the yaml file are not defined in the requirements.txt!
archive.write('requirements-nogui.txt', 'requirements.txt')
Expand Down