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
2 changes: 1 addition & 1 deletion backend/funix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import funix.decorator.theme as theme
import funix.decorator.widget as widget
import funix.hint as hint
from funix.app import app, enable_funix_host_checker
from funix.app import app, sock, enable_funix_host_checker
from funix.config.switch import GlobalSwitchOption
from funix.frontend import run_open_frontend, start
from funix.jupyter import jupyter
Expand Down
2 changes: 1 addition & 1 deletion backend/funix/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def cli_main():

This function is called when you run `python -m funix` or `funix` from the command line.
"""
plac.call(main, version="Funix 0.6.1")
plac.call(main, version="Funix 0.6.2")


if __name__ == "__main__":
Expand Down
112 changes: 111 additions & 1 deletion backend/funix/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import re
from datetime import datetime, timezone
from io import BytesIO
from pathlib import Path
from urllib.parse import urlparse
from uuid import uuid4
Expand All @@ -22,6 +23,8 @@
from funix.frontend import start
from funix.hint import LogLevel

from json import loads

app = Flask(__name__)
app.secret_key = GlobalSwitchOption.get_session_key()
app.config.update(
Expand All @@ -32,6 +35,113 @@
app.json.sort_keys = False
sock = Sock(app)

matplotlib_figure_manager = {}


class MatplotlibWebsocket:
supports_binary = True

def __init__(self, manager=None, ws=None):
self.manager = manager
self.ws = ws
if self.manager is not None:
self.manager.add_web_socket(self)
self.supports_binary = True

def on_close(self):
self.manager.remove_web_socket(self)
self.ws.close()

def set_manager(self, manager):
self.manager = manager
self.manager.add_web_socket(self)

def set_ws(self, ws):
self.ws = ws

def on_message(self, message: dict):
if message["type"] == "supports_binary":
self.supports_binary = message["value"]
else:
self.manager.handle_json(message)

def send_json(self, content: dict):
self.ws.send(json.dumps(content))

def send_binary(self, content: bytes):
if self.supports_binary:
self.ws.send(content)
else:
data_uri = "data:image/png;base64," + content.decode("utf-8")
self.ws.send(data_uri)


matplotlib_figure_managers = {}


def add_sock_route(flask_sock: Sock):
@flask_sock.route("/ws-plot/<path:figure_id>")
def __ws_plot(ws, figure_id: str):
"""
WebSocket route for plot.

Routes:
/ws-plot: The WebSocket route for plot.

Parameters:
ws (Any): The WebSocket.

Returns:
None
"""
if figure_id not in matplotlib_figure_managers:
manager_class = MatplotlibWebsocket(
matplotlib_figure_manager[int(figure_id)], ws
)
matplotlib_figure_managers[int(figure_id)] = manager_class
else:
manager_class = matplotlib_figure_managers[int(figure_id)]
manager_class.set_ws(ws)
while True:
data = ws.receive()
if data is not None:
dict_data = loads(data)
manager_class.on_message(dict_data)


def add_app_route(flask_app: Flask):
@flask_app.route("/plot-download/<path:figure_id>/<path:format>")
def __ws_plot(figure_id: str, format: str):
"""
Download the plot.

Routes:
/plot-download: The route to download the plot.

Parameters:
figure_id (str): The figure id.
format (str): The format of the plot.

Returns:
None
"""
buffer = BytesIO()
manager = matplotlib_figure_manager[int(figure_id)]
manager.canvas.figure.savefig(buffer, format=format)
buffer.seek(0)
buffer_name = f"plot.{format}"
return Response(
buffer,
mimetype="application/octet-stream",
headers={
"Content-Disposition": f"attachment; filename={buffer_name}",
"Content-Length": str(len(buffer.getvalue())),
},
)


add_sock_route(sock)


def funix_auto_cors(response: Response) -> Response:
if "HTTP_ORIGIN" not in request.environ:
Expand Down Expand Up @@ -63,7 +173,7 @@ def get_new_app_and_sock_for_jupyter() -> tuple[Flask, Sock]:
SESSION_TYPE="filesystem",
)
new_sock = Sock(new_app)

add_sock_route(new_sock)
new_app.after_request(funix_auto_cors)
start(new_app)

Expand Down
23 changes: 16 additions & 7 deletions backend/funix/decorator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from inspect import getsource, isgeneratorfunction, signature
from secrets import token_hex
from types import ModuleType
from typing import Callable, Optional, ParamSpec, TypeVar, Union
from typing import Callable, Optional, ParamSpec, TypeVar, Union, Literal
from uuid import uuid4

from docstring_parser import parse
Expand Down Expand Up @@ -55,6 +55,7 @@
from funix.hint import (
AcceptableWidgetsList,
ArgumentConfigType,
AutoRunType,
ConditionalVisibleType,
DestinationType,
DirectionType,
Expand Down Expand Up @@ -232,9 +233,9 @@ def funix(
rate_limit: RateLimiter = None,
reactive: ReactiveType = None,
print_to_web: bool = False,
autorun: bool = False,
autorun: AutoRunType = False,
disable: bool = False,
figure_to_image: bool = False,
matplotlib_format: Literal["png", "svg", "agg"] = "svg",
keep_last: bool = False,
app_and_sock: tuple[Flask, Sock] | None = None,
jupyter_class: bool = False,
Expand All @@ -249,7 +250,7 @@ def funix(
The heart of Funix, all the beginning of the magic happens here
(or at least most of it lol)

See document for more details, the docstring here is just a brief summary
See the document for more details, the docstring here is just a brief summary

Parameters:
path(str): path to the function, if None, the function name will be used (if title available, use title)
Expand Down Expand Up @@ -284,7 +285,9 @@ def funix(
print_to_web(bool): handle all stdout to web
autorun(bool): allow users to use continuity runs on the front end
disable(bool): disable this function
figure_to_image(bool): convert matplotlib figure to image
matplotlib_format(MatplotFormatType): Matplotlib format
available formats: "png", "svg", "agg"
for "agg", the image will be rendered in the interact widget, but has issues with the bbox
keep_last(bool): keep the last input and output in the frontend
app_and_sock(tuple[Flask, Sock]): the Flask app and the Sock instance, if None, use the global app and sock.
In jupyter, funix automatically generates the new app and sock.
Expand Down Expand Up @@ -508,6 +511,11 @@ def _function_reactive_update():
app_.post(f"/update/{function_id}")(_function_reactive_update)
app_.post(f"/update/{endpoint}")(_function_reactive_update)

real_autorun = autorun

if isinstance(autorun, bool):
real_autorun = "disable" if not autorun else "always"

decorated_functions_list_append(
app_.name,
{
Expand All @@ -518,7 +526,7 @@ def _function_reactive_update():
"id": function_id,
"websocket": need_websocket,
"reactive": has_reactive_params,
"autorun": autorun,
"autorun": real_autorun,
"keepLast": keep_last,
"width": width if width else ["50%", "50%"],
"class": is_class_method,
Expand Down Expand Up @@ -550,7 +558,7 @@ def _function_reactive_update():
param_widget_example_callable = {}

cast_to_list_flag, return_type_parsed = parse_function_annotation(
function_signature, figure_to_image
function_signature, matplotlib_format != "agg"
)

safe_input_layout = [] if not input_layout else input_layout
Expand Down Expand Up @@ -789,6 +797,7 @@ def wrapper(ws=None):
json_schema_props,
print_to_web,
secret_key,
matplotlib_format,
ws,
)
if result is not None:
Expand Down
2 changes: 2 additions & 0 deletions backend/funix/decorator/call.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def funix_call(
json_schema_props: dict,
print_to_web: bool,
secret_key: bool,
matplotlib_format: str,
ws=None,
):
for limiter in global_rate_limiters + limiters:
Expand Down Expand Up @@ -173,6 +174,7 @@ def pre_anal_result(frame: Any, function_call_result: Any):
function_call_result,
return_type_parsed,
cast_to_list_flag,
matplotlib_format,
)
except:
return {
Expand Down
Loading