From c6b5d006bb8fdd58793e3084d9ea120b64483e10 Mon Sep 17 00:00:00 2001 From: yazawazi <47273265+Yazawazi@users.noreply.github.com> Date: Fri, 12 Jul 2024 05:54:50 +0800 Subject: [PATCH] fix: isolate data with Flask instances All but session have been quarantined (`pre_fill` has been implemented in a special way) and `session` will be isolated in the future. --- backend/funix/__init__.py | 9 ++--- backend/funix/app/websocket.py | 1 - backend/funix/decorator/__init__.py | 55 ++++++++++++++++++---------- backend/funix/decorator/call.py | 7 ++-- backend/funix/decorator/param.py | 7 ++-- backend/funix/decorator/secret.py | 56 ++++++++++++++++++++--------- backend/funix/jupyter/__init__.py | 4 +++ backend/funix/util/module.py | 3 +- 8 files changed, 92 insertions(+), 50 deletions(-) diff --git a/backend/funix/__init__.py b/backend/funix/__init__.py index 041ceeb..383a2a7 100644 --- a/backend/funix/__init__.py +++ b/backend/funix/__init__.py @@ -56,11 +56,6 @@ # ---- Util ---- # ---- Exports ---- -if GlobalSwitchOption.in_notebook: - import logging - - logging.getLogger("werkzeug").setLevel(logging.ERROR) - __use_git = False """ Funix uses git to clone repo, now this feature is optional. @@ -203,7 +198,7 @@ def import_from_config( raise Exception("GitPython is not installed, please install it first!") if app_secret and isinstance(app_secret, str): - set_app_secret(app_secret) + set_app_secret(app.name, app_secret) if dir_mode: base_dir = file_or_module_name @@ -418,7 +413,7 @@ def run( parsed_ip = ip_address(host) parsed_port = get_unused_port_from(port, parsed_ip) - funix_secrets = secret.export_secrets() + funix_secrets = secret.export_secrets(app.name) if funix_secrets: local = get_compressed_ip_address_as_str(parsed_ip) print("Secrets:") diff --git a/backend/funix/app/websocket.py b/backend/funix/app/websocket.py index 27c6992..1ff7e79 100644 --- a/backend/funix/app/websocket.py +++ b/backend/funix/app/websocket.py @@ -1,7 +1,6 @@ """ Websocket class to redirect """ -import sys from io import StringIO from json import dumps diff --git a/backend/funix/decorator/__init__.py b/backend/funix/decorator/__init__.py index 3f5acf8..b73c12a 100644 --- a/backend/funix/decorator/__init__.py +++ b/backend/funix/decorator/__init__.py @@ -82,7 +82,6 @@ except: pass - __pandas_use = False """ Whether Funix can handle pandas or pandera-related logic @@ -101,9 +100,9 @@ except: pass -__decorated_functions_names_list: list[str] = [] +__decorated_functions_names_list: dict[str, list[str]] = {} """ -A list, each element is the name of the decorated function. +A dict, each element is the names of the decorated function. """ __wrapper_enabled: bool = False @@ -121,7 +120,7 @@ Default dir mode info. """ -handled_object: list[int] = [] +handled_object: dict[str, list[int]] = {app.name: []} """ Handled object ids. """ @@ -182,17 +181,18 @@ def enable_wrapper() -> None: enable_file_service(app) -def object_is_handled(object_id: int) -> bool: +def object_is_handled(app_: Flask, object_id: int) -> bool: """ Check if the object is handled. Parameters: + app_ (Flask): The app. object_id (int): The object id. Returns: bool: True if handled, False otherwise. """ - return object_id in handled_object + return app_.name in handled_object and object_id in handled_object[app_.name] def funix( @@ -291,6 +291,12 @@ def funix( app_ = app sock_ = sock + if app_.name not in __decorated_functions_names_list: + __decorated_functions_names_list[app_.name] = [] + + if app_.name not in handled_object: + handled_object[app_.name] = [] + def decorator(function: Callable) -> callable: """ Decorator for functions to convert them to web apps @@ -304,7 +310,7 @@ def decorator(function: Callable) -> callable: Raises: Check code for details """ - handled_object.append(id(function)) + handled_object[app_.name].append(id(function)) if disable: return function if __wrapper_enabled: @@ -393,30 +399,32 @@ def decorator(function: Callable) -> callable: endpoint = get_endpoint(path, unique_function_name, function_name) if unique_function_name: - if unique_function_name in __decorated_functions_names_list: + if unique_function_name in __decorated_functions_names_list[app_.name]: raise ValueError( f"Function with name {function_name} already exists, you better check other files, they may " f"have the same function name" ) else: - if function_title in __decorated_functions_names_list: + if function_title in __decorated_functions_names_list[app_.name]: raise ValueError( f"Function with name {function_title} already exists" ) - if app_secret := get_app_secret(): - set_function_secret(app_secret, function_id, function_title) + if app_secret := get_app_secret(app_.name): + set_function_secret(app_.name, app_secret, function_id, function_title) elif secret: if isinstance(secret, bool): - set_function_secret(token_hex(16), function_id, function_title) + set_function_secret( + app_.name, token_hex(16), function_id, function_title + ) else: - set_function_secret(secret, function_id, function_title) + set_function_secret(app_.name, secret, function_id, function_title) - secret_key = get_secret_by_id(function_id) is not None + secret_key = get_secret_by_id(app_.name, function_id) is not None if unique_function_name: - __decorated_functions_names_list.append(unique_function_name) + __decorated_functions_names_list[app_.name].append(unique_function_name) else: - __decorated_functions_names_list.append(function_title) + __decorated_functions_names_list[app_.name].append(function_title) need_websocket = isgeneratorfunction(function) @@ -618,7 +626,7 @@ def decorated_function_param_getter(): flask.Response: The function's parameters """ return get_param_for_funix( - pre_fill, decorated_function, session_description + app_.name, pre_fill, decorated_function, session_description ) decorated_function_param_getter_name = f"{function_name}_param_getter" @@ -650,7 +658,7 @@ def verify_secret(): Returns: flask.Response: The verification result """ - return check_secret(function_id) + return check_secret(app_.name, function_id) decorated_function_verify_secret_name = f"{function_name}_verify_secret" @@ -671,6 +679,7 @@ def verify_secret(): @wraps(function) def wrapper(ws=None): result = funix_call( + app_.name, limiters, need_websocket, __pandas_use, @@ -711,7 +720,11 @@ def funix_class(disable: bool = False): def __funix_class(cls): - handled_object.append(id(cls)) + if not GlobalSwitchOption.in_notebook: + if app.name in handled_object: + handled_object[app.name].append(id(cls)) + else: + handled_object[app.name] = [id(cls)] if inspect.isclass(cls): if not hasattr(cls, "__init__"): raise Exception("Class must have __init__ method!") @@ -723,6 +736,10 @@ def __funix_class(cls): f.visit(ast.parse(class_source_code)) if GlobalSwitchOption.in_notebook: + if f.class_app.name in handled_object: + handled_object[f.class_app.name].append(id(cls)) + else: + handled_object[f.class_app.name] = [id(cls)] jupyter(f.class_app) return cls else: diff --git a/backend/funix/decorator/call.py b/backend/funix/decorator/call.py index 51e8135..f8f4605 100644 --- a/backend/funix/decorator/call.py +++ b/backend/funix/decorator/call.py @@ -64,6 +64,7 @@ def kumo_callback(): def funix_call( + app_name: str, limiters: list[Limiter] | None, need_websocket: bool, use_pandas: bool, @@ -125,12 +126,12 @@ def original_result_to_pre_fill_metadata( for index_or_key in pre_fill_metadata: if index_or_key is PreFillEmpty: set_global_variable( - function_call_address + "_result", + app_name + function_call_address + "_result", function_call_result, ) else: set_global_variable( - function_call_address + f"_{index_or_key}", + app_name + function_call_address + f"_{index_or_key}", function_call_result[index_or_key], ) @@ -243,7 +244,7 @@ def output_to_web_function(**wrapped_function_kwargs): if secret_key: if "__funix_secret" in function_kwargs: if ( - not get_secret_by_id(function_id) + not get_secret_by_id(app_name, function_id) == function_kwargs["__funix_secret"] ): incorrect_secret_error = { diff --git a/backend/funix/decorator/param.py b/backend/funix/decorator/param.py index e5fd0b9..040a5c4 100644 --- a/backend/funix/decorator/param.py +++ b/backend/funix/decorator/param.py @@ -308,6 +308,7 @@ def create_parse_type_metadata(function_id: str): def get_param_for_funix( + app_name: str, pre_fill: dict | None, decorated_function: dict, session_description: str, @@ -317,11 +318,13 @@ def get_param_for_funix( for argument_key, from_function_info in pre_fill.items(): if isinstance(from_function_info, tuple): last_result = get_global_variable( - str(id(from_function_info[0])) + f"_{from_function_info[1]}" + app_name + + str(id(from_function_info[0])) + + f"_{from_function_info[1]}" ) else: last_result = get_global_variable( - str(id(from_function_info)) + "_result" + app_name + str(id(from_function_info)) + "_result" ) if last_result is not None: new_decorated_function["params"][argument_key]["default"] = last_result diff --git a/backend/funix/decorator/secret.py b/backend/funix/decorator/secret.py index ed9c3d7..350d41f 100644 --- a/backend/funix/decorator/secret.py +++ b/backend/funix/decorator/secret.py @@ -2,84 +2,106 @@ from flask import Response, request -__decorated_id_to_function_dict: dict[str, str] = {} +__decorated_id_to_function_dict: dict[str, dict[str, str]] = {} """ A dict, key is function id, value is function name. """ -__decorated_secret_functions_dict: dict[str, str] = {} +__decorated_secret_functions_dict: dict[str, dict[str, str]] = {} """ A dict, key is function id, value is secret. For checking if the secret is correct. """ -__app_secret: str | None = None +__app_secret: dict[str, str | None] = {} """ App secret, for all functions. """ -def get_secret_by_id(function_id: str) -> str | None: +def get_secret_by_id(app_name: str, function_id: str) -> str | None: """ Get the secret of a function by id. Parameters: + app_name (str): The app name. function_id (str): The function id. Returns: str | None: The secret. """ global __decorated_secret_functions_dict - return __decorated_secret_functions_dict.get(function_id, None) + if app_name not in __decorated_secret_functions_dict: + return None + return __decorated_secret_functions_dict[app_name].get(function_id, None) -def set_function_secret(secret: str, function_id: str, function_name: str) -> None: +def set_function_secret( + app_name: str, secret: str, function_id: str, function_name: str +) -> None: """ Set the secret of a function. Parameters: + app_name (str): The app name. secret (str): The secret. function_id (str): The function id. function_name (str): The function name (or with path). """ global __decorated_secret_functions_dict, __decorated_id_to_function_dict - __decorated_secret_functions_dict[function_id] = secret - __decorated_id_to_function_dict[function_id] = function_name + if app_name not in __decorated_secret_functions_dict: + __decorated_secret_functions_dict[app_name] = { + function_id: secret, + } + __decorated_id_to_function_dict[app_name] = { + function_id: function_name, + } + else: + __decorated_secret_functions_dict[app_name][function_id] = secret + __decorated_id_to_function_dict[app_name][function_id] = function_name -def set_app_secret(secret: str) -> None: +def set_app_secret(app_name: str, secret: str) -> None: """ Set the app secret, it will be used for all functions. Parameters: + app_name (str): The app name. secret (str): The secret. """ global __app_secret - __app_secret = secret + __app_secret[app_name] = secret -def get_app_secret() -> str | None: +def get_app_secret(app_name: str) -> str | None: """ Get the app secret. + Parameters: + app_name (str): The app name. + Returns: str | None: The app secret. """ global __app_secret - return __app_secret + return __app_secret.get(app_name, None) -def export_secrets(): +def export_secrets(app_name: str): """ Export all secrets from the decorated functions. + + Parameters: + app_name (str): The app name. """ __new_dict: dict[str, str] = {} - for function_id, secret in __decorated_secret_functions_dict.items(): - __new_dict[__decorated_id_to_function_dict[function_id]] = secret + if app_name in __decorated_secret_functions_dict: + for function_id, secret in __decorated_secret_functions_dict[app_name].items(): + __new_dict[__decorated_id_to_function_dict[app_name][function_id]] = secret return __new_dict -def check_secret(function_id: str): +def check_secret(app_name: str, function_id: str): data = request.get_json() failed_data = Response( @@ -99,7 +121,7 @@ def check_secret(function_id: str): return failed_data user_secret = request.get_json()["secret"] - if user_secret == __decorated_secret_functions_dict[function_id]: + if user_secret == __decorated_secret_functions_dict[app_name][function_id]: return Response( dumps( { diff --git a/backend/funix/jupyter/__init__.py b/backend/funix/jupyter/__init__.py index f8405b1..eea1df6 100644 --- a/backend/funix/jupyter/__init__.py +++ b/backend/funix/jupyter/__init__.py @@ -11,6 +11,10 @@ def jupyter(app: Flask) -> None: + if GlobalSwitchOption.in_notebook: + import logging + + logging.getLogger("werkzeug").setLevel(logging.ERROR) enable_file_service(app) enable_list(app) from IPython.display import IFrame, display diff --git a/backend/funix/util/module.py b/backend/funix/util/module.py index 720e9a7..cc1dbda 100644 --- a/backend/funix/util/module.py +++ b/backend/funix/util/module.py @@ -11,6 +11,7 @@ from uuid import uuid4 from funix import decorator, hint +from funix.app import app def getsourcefile_safe(obj: Any) -> str | None: @@ -105,7 +106,7 @@ def handle_module( if getsourcefile_safe(module_member) != module.__file__: continue in_funix = ( - decorator.object_is_handled(id(module_member)) + decorator.object_is_handled(app, id(module_member)) or id(module_member) in hint.custom_cls_ids ) if in_funix: