Skip to content

Commit

Permalink
Add GUI
Browse files Browse the repository at this point in the history
  • Loading branch information
pfebrer committed May 23, 2024
1 parent b3cd918 commit ef689a4
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 163 deletions.
39 changes: 17 additions & 22 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name = "nodify"
description = "Supercharge your functional application with a powerful node system."
readme = "README.md"
license = {file = "LICENSE"}
version = "0.0.6"
version = "0.0.7"

dependencies = []

Expand Down Expand Up @@ -58,6 +58,22 @@ docs = [
"matplotlib",
]

server = [
"simplejson",
]

gui = [
"simplejson", # Because built-in json parses nan and JS does not understand it
"black",
"isort",
"flask",
"flask-socketio >= 5.0",
"eventlet"
]

[project.scripts]
nodify-gui = "nodify.gui.cli:nodify_gui_cli"


[tool.pytest.ini_options]
testpaths = [
Expand Down Expand Up @@ -132,24 +148,3 @@ extend-exclude = """
| .*/\\..*
)
"""


[tool.cibuildwheel]
build-verbosity = 3
test-extras = "test"

skip = [
"pp*",
"*musllinux*",
]

[tool.cibuildwheel.linux]

[tool.cibuildwheel.windows]

before-build = "pip install delvewheel"
repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}"

test-command = "pytest --pyargs nodify -m 'not slow'"

[tool.cibuildwheel.macos]
2 changes: 2 additions & 0 deletions src/nodify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
from .node import Node, ConstantNode
from .utils import nodify_module
from .workflow import Workflow

from .gui import open_frontend, launch_gui
129 changes: 129 additions & 0 deletions src/nodify/gui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import importlib
import webbrowser
from pathlib import Path
from typing import List, Literal, Optional, Type, Union


def open_frontend(
backend: Literal["socket", "pyodide"],
port: Optional[int] = None,
host: Optional[str] = None,
session_cls: Optional[Union[str, Type]] = None,
pyodide_packages: List[str] = [],
pyodide_pypi_packages: List[str] = [],
):
"""Opens the GUI frontend in the default browser.
Note that this function doesn't launch a server backend! For that use ``gui`` instead.
Parameters
----------
backend:
The backend to tell the GUI to use.
port:
If the backend is "socket", port to use to communicate with the backend.
host:
If the backend is "socket", host to use to communicate with the backend.
session_cls:
If the backend is "pyodide", the session class to instantiate to create the session.
If it is a string, it must be the full path to the session class, including the module.
pyodide_packages:
If the backend is "pyodide", the packages to load from the pyodide distribution.
pyodide_pypi_packages:
If the backend is "pyodide", the packages to install from PyPI in the pyodide environment.
"""
this_path = Path(__file__)
html_path = this_path.parent / "build" / "index.html"

if backend == "socket":
url = "file://" + str(html_path) + f"#/?backend=socket"
if port is not None:
url += f"&port={port}"
if host is not None:
url += f"&host={host}"

elif backend == "pyodide":
url = (
"file://"
+ str(html_path)
+ f"#/?backend=pyodide&packages={','.join(pyodide_packages)}&micropipPackages={','.join(pyodide_pypi_packages)}"
)
if session_cls is not None:
if hasattr(session_cls, "_nodify_default_session"):
session_cls = session_cls._nodify_default_session

if isinstance(session_cls, type):
session_cls = session_cls.__module__ + "." + session_cls.__name__
url += f"&session_cls={session_cls}"
else:
raise ValueError(f"Invalid backend {backend}")

return webbrowser.open(url)


def launch_gui(
backend: Literal["socket", "pyodide"] = "socket",
port: Optional[int] = None,
host: Optional[str] = None,
session_cls: Optional[Union[str, Type]] = None,
pyodide_packages: List[str] = [],
pyodide_pypi_packages: List[str] = [],
):
"""Launches the GUI, including both the frontend and the backend.
Parameters
----------
backend:
The backend to use. If "socket", the server will be launched.
port:
If the backend is "socket", port to use to communicate between frontend and backend.
host:
If the backend is "socket", host to use to communicate between frontend and backend.
session_cls:
The session class to instantiate to create the session. If not provided, the default session
will be used.
pyodide_packages:
If the backend is "pyodide", the packages to load from the pyodide distribution.
pyodide_pypi_packages:
If the backend is "pyodide", the packages to install from PyPI in the pyodide environment.
"""

if backend == "socket":
from ..server.flask_socketio.app import SocketioApp

if hasattr(session_cls, "_nodify_default_session"):
session_cls = session_cls._nodify_default_session

if isinstance(session_cls, str):

session_path = session_cls.split(".")
if len(session_path) == 1:
session_path = importlib.import_module(
session_path[0]
)._nodify_default_session
session_path = session_path.split(".")

module_name = ".".join(session_path[:-1])
cls_name = session_path[-1]

session = getattr(importlib.import_module(module_name), cls_name)()
elif isinstance(session_cls, type):
session = session_cls()
else:
session = None

print(session_cls, session)

app = SocketioApp(session=session, async_mode="threading")
app.launch(frontend=True, server=True, server_host=host, server_port=port)
return app
elif backend == "pyodide":
return open_frontend(
"pyodide",
session_cls=session_cls,
pyodide_packages=pyodide_packages,
pyodide_pypi_packages=pyodide_pypi_packages,
)
else:
raise ValueError(f"Invalid backend {backend}")
94 changes: 94 additions & 0 deletions src/nodify/gui/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import argparse
import importlib

from nodify.gui import launch_gui
from nodify.utils import nodify_module


def build_cli(title, description, defaults, modules=[]):
"""Builds the command line interface for launching nodify's GUI"""

def cli():
parser = argparse.ArgumentParser(prog=title, description=description)

parser.add_argument(
"modules",
type=str,
nargs="?",
help="Modules to nodify before launching the GUI",
)
parser.add_argument(
"--backend",
type=str,
default=defaults.get("backend", "socket"),
help="The backend to use. If 'socket', the server will be launched.",
)
parser.add_argument(
"--port",
type=int,
default=defaults.get("port", None),
help="If the backend is 'socket', port to use to communicate between frontend and backend.",
)
parser.add_argument(
"--host",
type=str,
default=defaults.get("host", None),
help="If the backend is 'socket', host to use to communicate between frontend and backend.",
)
parser.add_argument(
"--pyodide-packages",
type=str,
nargs="*",
default=defaults.get("pyodide_packages", []),
help="If the backend is 'pyodide', the packages to load from the pyodide distribution.",
)
parser.add_argument(
"--pyodide-pypi-packages",
type=str,
nargs="*",
default=defaults.get("pyodide_pypi_packages", []),
help="If the backend is 'pyodide', the packages to install from PyPI in the pyodide environment.",
)
parser.add_argument(
"--session-cls",
type=str,
default=defaults.get("session_cls", None),
help="The session class to instantiate to create the session. If not provided, the default session will be used.",
)

args = parser.parse_args()

if not hasattr(args, "modules"):
modules_to_nodify = [*modules, defaults.get("modules", [])]
else:
modules_to_nodify = [*modules, args.modules]

# Nodify all requested modules
for module_string in modules_to_nodify:
module = importlib.import_module(module_string)
nodify_module(module)

launch_gui(
backend=args.backend,
port=args.port,
host=args.host,
session_cls=args.session_cls,
pyodide_packages=args.pyodide_packages,
pyodide_pypi_packages=args.pyodide_pypi_packages,
)

return cli


nodify_gui_cli = build_cli(
"nodify-gui",
"Command line utility to launch nodify's graphical interface.",
{
"backend": "socket",
"port": None,
"host": None,
"session_cls": None,
"pyodide_packages": [],
"pyodide_pypi_packages": [],
},
)
23 changes: 12 additions & 11 deletions src/nodify/server/flask_socketio/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from ..sync import Connection
from .emiters import emit, emit_error, emit_session
from .sync import SocketioConnection
from .user_management import if_user_can, listen_to_users, with_user_management
from .user_management import if_user_can # , listen_to_users , with_user_management

# Register the environment variables that the user can tweak
register_env_variable(
Expand Down Expand Up @@ -73,7 +73,8 @@ def __init__(

# No user management yet
if False:
with_user_management(self.app)
...
# with_user_management(self.app)

socketio = SocketIO(
self.app,
Expand All @@ -92,7 +93,8 @@ def __init__(
on = socketio.on

if False:
listen_to_users(on, emit_session)
...
# listen_to_users(on, emit_session)

connection = SocketioLastUpdateEmiter(socketio)

Expand All @@ -108,7 +110,6 @@ def __init__(
# -------------------------------------------
@socketio.on_error()
def send_error(err):
print("SOME ERROR", err)
if self.session is not None:
self.session.logger.exception(err)

Expand Down Expand Up @@ -214,12 +215,11 @@ def run_server(
def get_server_address(self) -> str:
return f"http://{self.host}:{self.port}"

@staticmethod
def open_frontend():
def open_frontend(self):
"""Opens the graphical interface"""
webbrowser.open(
str(Path(__file__).parent.parent.parent / "build" / "index.html")
)
from nodify.gui import open_frontend

open_frontend("socket", self.port, self.host)

def launch(
self,
Expand Down Expand Up @@ -249,8 +249,6 @@ def launch(
interactive: bool, optional
whether an interactive console should be started.
"""
if frontend:
self.open_frontend()

if not server:
return
Expand Down Expand Up @@ -281,6 +279,9 @@ def launch(
for t in self.threads:
t.start()

if frontend:
self.open_frontend()

if interactive:
try:
while 1:
Expand Down
Loading

0 comments on commit ef689a4

Please sign in to comment.