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
1 change: 1 addition & 0 deletions backend/funix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
new_funix_type = hint.new_funix_type
set_app_secret = secret.set_app_secret
generate_redirect_link = widget.generate_redirect_link
generate_redirect_button = widget.generate_redirect_button
mime_encode = mime_encoded
# ---- Util ----

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.3")
plac.call(main, version="Funix 0.6.4")


if __name__ == "__main__":
Expand Down
19 changes: 18 additions & 1 deletion backend/funix/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
from urllib.parse import urlparse
from uuid import uuid4

from flask import Flask, Response, abort, request
from flask import Flask, Response, abort, request, send_file
from flask_sock import Sock
from sqlalchemy import create_engine, text
from sqlalchemy.pool import SingletonThreadPool

from funix.config import banned_function_name_and_path
from funix.config.switch import GlobalSwitchOption
from funix.decorator.file import get_file_info
from funix.decorator.lists import get_uuid_with_name
Expand Down Expand Up @@ -367,3 +368,19 @@ def enable_funix_host_checker(regex: str):
def funix_host_check():
if len(re.findall(regex_string, request.host)) == 0:
abort(403)


def serve_file(route_or_path: str, path: str = None):
if route_or_path and path:
if route_or_path in banned_function_name_and_path:
raise ValueError(f"{route_or_path} is not allowed, banned names: {banned_function_name_and_path}")
@app.route(route_or_path)
def __serve_file():
return send_file(path)
else:
safe_route = os.path.basename(route_or_path)
if safe_route in banned_function_name_and_path:
raise ValueError(f"{safe_route} is not allowed, banned names: {banned_function_name_and_path}")
@app.route(safe_route)
def __serve_file():
return send_file(route_or_path)
2 changes: 1 addition & 1 deletion backend/funix/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
A list, contains the upload widget names.
"""

banned_function_name_and_path = ["list", "file", "static", "config", "param", "call"]
banned_function_name_and_path = ["list", "file", "static", "config", "param", "call", "update"]
"""
The banned function name and path.

Expand Down
14 changes: 13 additions & 1 deletion backend/funix/decorator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
set_function_secret,
)
from funix.decorator.theme import get_parsed_theme_fot_funix
from funix.decorator.widget import parse_argument_config, widget_parse
from funix.decorator.widget import get_uuid_by_callable, parse_argument_config, widget_parse
from funix.hint import (
AcceptableWidgetsList,
ArgumentConfigType,
Expand All @@ -64,6 +64,7 @@
InputLayout,
LabelsType,
Markdown,
NextTo,
OutputLayout,
PreFillType,
ReactiveType,
Expand Down Expand Up @@ -246,6 +247,7 @@ def funix(
order: Optional[int] = None,
just_run: bool = False,
class_line: Optional[int] = None,
next_to: Optional[Callable] = None,
):
"""
Decorator for functions to convert them to web apps
Expand Down Expand Up @@ -301,6 +303,7 @@ def funix(
order(int): the order of the function
just_run(bool): just run the function, no input panel for the function
class_line(int): the line number of the class
next_to(Callable): the function to be called next

Returns:
function: the decorated function
Expand Down Expand Up @@ -489,6 +492,13 @@ def decorator(function: Callable[P, R]) -> Callable[P, R]:
need_websocket = True
setattr(function_signature, "_return_annotation", Markdown)

if next_to:
print(f"WARNING: the {function_name} function has next to, the output type will be ignored.")
setattr(function_signature, "_return_annotation", NextTo)

if need_websocket and next_to:
raise ValueError("You cannot use next to with websocket mode.")

has_reactive_params = False

reactive_config: dict[str, tuple[Callable, dict[str, str]]] = {}
Expand Down Expand Up @@ -751,6 +761,7 @@ def decorated_function_param_getter():
param_widget_whitelist_callable,
param_widget_example_callable,
class_method_qualname if is_class_method else function.__qualname__,
next_to
)

decorated_function_param_getter_name = f"{function_name}_param_getter"
Expand Down Expand Up @@ -817,6 +828,7 @@ def wrapper(ws=None):
secret_key,
matplotlib_format,
ws,
next_to,
)
if result is not None:
return result
Expand Down
24 changes: 23 additions & 1 deletion backend/funix/decorator/call.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import sys
from copy import deepcopy
from functools import wraps
from inspect import isgeneratorfunction
from inspect import isgeneratorfunction, signature
from json import dumps, loads
from traceback import format_exc
from typing import Any, Callable
from urllib.request import urlopen
from uuid import uuid4

from flask import request, session
from funix.decorator.encoder import FunixJsonEncoder
from git import exc
from requests import post

from funix.app.websocket import StdoutToWebsocket
Expand All @@ -19,6 +21,7 @@
from funix.decorator.pre_fill import get_pre_fill_metadata
from funix.decorator.response import response_item_to_class
from funix.decorator.secret import get_secret_by_id
from funix.decorator.widget import get_path_by_callable
from funix.hint import PreFillEmpty, WrapperException
from funix.session import set_global_variable

Expand Down Expand Up @@ -104,6 +107,7 @@ def funix_call(
secret_key: bool,
matplotlib_format: str,
ws=None,
next_to: Callable | None = None,
):
for limiter in global_rate_limiters + limiters:
limit_result = limiter.rate_limit()
Expand Down Expand Up @@ -189,6 +193,24 @@ def wrapped_function(**wrapped_function_kwargs):
function_call_result = call_function_get_frame(
function, **wrapped_function_kwargs
)
if next_to:
try:
if isinstance(function_call_result[1], tuple):
arguments = signature(next_to).bind(*function_call_result[1])
arguments.apply_defaults()
arguments = arguments.arguments
elif isinstance(function_call_result[1], dict):
arguments = signature(next_to).bind(**function_call_result[1])
arguments.apply_defaults()
arguments = arguments.arguments
else:
print("Warning: You must return a tuple or a dict.")
arguments = function_call_result[1]
except:
print("Warning: Cannot bind arguments, it means the type of the arguments is not correct.")
arguments = function_call_result[1]

return dumps({"args": arguments, "path": get_path_by_callable(next_to) }, cls=FunixJsonEncoder)
return pre_anal_result(function_call_result[0], function_call_result[1])
except WrapperException as e:
return {
Expand Down
21 changes: 21 additions & 0 deletions backend/funix/decorator/encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from json import JSONEncoder
from datetime import datetime


class FunixJsonEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
if hasattr(o, "__class__"):
clz = o.__class__
if hasattr(clz, "__base__"):
base = clz.__base__
# Check pydantic
try:
from pydantic import BaseModel

if issubclass(base, BaseModel):
return o.model_dump(mode="json")
except ImportError:
return super().default(o)
return super().default(o)
26 changes: 6 additions & 20 deletions backend/funix/decorator/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from json import dumps, JSONEncoder
from types import MappingProxyType
from typing import Any, Callable
from datetime import datetime


from flask import Response

Expand All @@ -16,12 +16,14 @@
get_type_dict,
get_type_widget_prop,
)
from funix.decorator.widget import get_uuid_by_callable
from funix.session import get_global_variable
from funix.decorator.layout import (
pydantic_layout_dict,
pydantic_name_dict,
pydantic_widget_dict,
)
from funix.decorator.encoder import FunixJsonEncoder

dataframe_parse_metadata: dict[str, dict[str, list[str]]] = {}
"""
Expand All @@ -34,25 +36,6 @@
"""


class FunixJsonEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
if hasattr(o, "__class__"):
clz = o.__class__
if hasattr(clz, "__base__"):
base = clz.__base__
# Check pydantic
try:
from pydantic import BaseModel

if issubclass(base, BaseModel):
return o.model_dump(mode="json")
except ImportError:
return super().default(o)
return super().default(o)


def apply_decorated_params(
item_props: dict, decorated_params: dict, param_name: str, function_name: str = None
) -> None:
Expand Down Expand Up @@ -701,6 +684,7 @@ def get_param_for_funix(
param_widget_whitelist_callable: dict,
param_widget_example_callable: dict,
qualname: str,
next_to: Callable | None,
):
new_decorated_function = deepcopy(decorated_function)
if pre_fill is not None:
Expand Down Expand Up @@ -876,6 +860,8 @@ def get_param_for_funix(
des = get_global_variable(session_description)
new_decorated_function["description"] = des
new_decorated_function["schema"]["description"] = des
if next_to:
new_decorated_function["nextToUuid"] = get_uuid_by_callable(next_to)
return Response(
dumps(new_decorated_function, cls=FunixJsonEncoder), mimetype="application/json"
)
Expand Down
68 changes: 67 additions & 1 deletion backend/funix/decorator/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Any, Callable, Union

from funix.app import app
from funix.decorator.encoder import FunixJsonEncoder
from funix.decorator.lists import (
get_class_method_funix,
get_function_detail_by_uuid,
Expand Down Expand Up @@ -345,7 +346,7 @@ def generate_redirect_link_core(
arguments = signature(_function).bind(*args, **kwargs)
arguments.apply_defaults()
dict_args = arguments.arguments
json_plain = json.dumps(dict_args)
json_plain = json.dumps(dict_args, cls=FunixJsonEncoder)
web_safe_args = base64.urlsafe_b64encode(json_plain.encode()).decode()
return f"/{result['path']}?args={web_safe_args}", result

Expand All @@ -368,3 +369,68 @@ def generate_redirect_link(
"""
url, result = generate_redirect_link_core(function, *args, **kwargs)
return f"<a href='{url}'>{result['name']}</a>"

def generate_redirect_button(
function: Callable,
auto_direct: bool = True,
use_raw: bool = False,
raw_args: dict = {},
button_text: str = "Redirect",
*args,
**kwargs,
) -> dict:
"""
Generate a redirect button.
"""
function_qualname = function.__qualname__
_function = function
if "." in function_qualname:
class_function = get_class_method_funix(
app_name=app.name, method_qualname=function_qualname
)
if class_function:
_function = class_function
else:
raise ValueError(f"Function {function_qualname} not found.")
jump_uuid = get_function_uuid_with_id(app_name=app.name, _id=id(_function))
if jump_uuid == "":
raise ValueError(f"Function {function_qualname} not found.")
result = get_function_detail_by_uuid(app_name=app.name, uuid=jump_uuid)
if use_raw:
json_plain = json.dumps(raw_args, cls=FunixJsonEncoder)
else:
arguments = signature(_function).bind(*args, **kwargs)
arguments.apply_defaults()
dict_args = arguments.arguments
json_plain = json.dumps(dict_args, cls=FunixJsonEncoder)
return {"path": result["path"], "args": json_plain, "text": button_text, "auto_direct": auto_direct}

def get_function_by_callable(function: Callable) -> Callable | None:
function_qualname = function.__qualname__
_function = function
if "." in function_qualname:
class_function = get_class_method_funix(
app_name=app.name, method_qualname=function_qualname
)
if class_function:
_function = class_function
else:
return None
return _function


def get_uuid_by_callable(function: Callable) -> str | None:
_function = get_function_by_callable(function)
if _function is None:
return None
jump_uuid = get_function_uuid_with_id(app_name=app.name, _id=id(_function))
if jump_uuid == "":
return None
return jump_uuid


def get_path_by_callable(function: Callable) -> str | None:
_function = get_function_by_callable(function)
if _function is None:
return None
return get_function_detail_by_uuid(app_name=app.name, uuid=get_function_uuid_with_id(app_name=app.name, _id=id(_function)))["path"]
Loading