diff --git a/dash_uploader/callbacks.py b/dash_uploader/callbacks.py index 24934ab..5130984 100644 --- a/dash_uploader/callbacks.py +++ b/dash_uploader/callbacks.py @@ -5,8 +5,23 @@ import dash_uploader.settings as settings -def create_dash_callback(callback, settings): # pylint: disable=redefined-outer-name - """Wrap the dash callback with the du.settings. +def query_app_and_root(component_id): + """Query the app and the root folder by the given component id. + This is a private method, and should not be exposed to users. + """ + app_idx = settings.user_configs_query.get(component_id, None) + if app_idx is None: + app_idx = settings.user_configs_default + app_item = settings.user_configs[app_idx] + if not app_item["is_dash"]: + raise TypeError("The du.configure_upload must be called with a dash.Dash instance before the @du.callback can be used! Please, configure the dash-uploader.") + app = app_item["app"] + upload_folder_root = app_item["upload_folder_root"] + return app, upload_folder_root + + +def create_dash_callback(callback, app_root_folder): # pylint: disable=redefined-outer-name + """Wrap the dash callback with the upload_folder_root. This function could be used as a wrapper. It will add the configurations of dash-uploader to the callback. This is a private method, and should not be exposed to users. @@ -19,9 +34,9 @@ def wrapper(iscompleted, filenames, upload_id): out = [] if filenames is not None: if upload_id: - root_folder = Path(settings.UPLOAD_FOLDER_ROOT) / upload_id + root_folder = Path(app_root_folder) / upload_id else: - root_folder = Path(settings.UPLOAD_FOLDER_ROOT) + root_folder = Path(app_root_folder) for filename in filenames: file = root_folder / filename @@ -56,9 +71,8 @@ def callback( ) def get_a_list(filenames): return html.Ul([html.Li(filenames)]) - - """ + app, upload_folder_root = query_app_and_root(id) def add_callback(function): """ @@ -77,15 +91,10 @@ def add_callback(function): """ dash_callback = create_dash_callback( function, - settings, + upload_folder_root, ) - if not hasattr(settings, "app"): - raise Exception( - "The du.configure_upload must be called before the @du.callback can be used! Please, configure the dash-uploader." - ) - - dash_callback = settings.app.callback( + dash_callback = app.callback( output, [Input(id, "isCompleted")], [State(id, "fileNames"), State(id, "upload_id")], diff --git a/dash_uploader/configure_upload.py b/dash_uploader/configure_upload.py index beb9586..e4761b3 100644 --- a/dash_uploader/configure_upload.py +++ b/dash_uploader/configure_upload.py @@ -1,15 +1,86 @@ import logging +import dash +import flask + import dash_uploader.settings as settings -from dash_uploader.upload import update_upload_api from dash_uploader.httprequesthandler import HttpRequestHandler logger = logging.getLogger("dash_uploader") +def update_upload_api(requests_pathname_prefix, upload_api): + """Path join for the API path name. + This is a private method, and should not be exposed to users. + """ + if requests_pathname_prefix == "/": + return upload_api + return "/".join( + [ + requests_pathname_prefix.rstrip("/"), + upload_api.lstrip("/"), + ] + ) + + +def check_app(app): + """Check the validity of the provided app. + The app requires to be a dash.Dash instance or a flask.Flask + instance. It should not be repeated in the user configurations. + This is a private method, and should not be exposed to users. + """ + is_dash = isinstance(app, dash.Dash) + if not is_dash and not isinstance(app, flask.Flask): + raise TypeError( + 'The argument "app" requires to be a dash.Dash instance or a flask.Flask instance.' + ) + return is_dash + + +def check_upload_component_ids(upload_component_ids): + """Check the validity of the component ids. + This function is used for checking the validity of provided component + ids. A valid id should be a non-empty str and not repeated in the + configurations. + This is a private method, and should not be exposed to users. + """ + # When None, check the default configs. + if upload_component_ids is None: + if settings.user_configs_default is not None: + raise ValueError( + "The default app has been configured before. A repeated configuration is not allowed." + ) + return None + # When not None, check the ids first. + valid_ids = None + if isinstance(upload_component_ids, str) and upload_component_ids != "": + valid_ids = (upload_component_ids,) + if isinstance(upload_component_ids, (list, tuple)): + if all( + map(lambda uid: isinstance(uid, str) and uid != "", upload_component_ids) + ): + valid_ids = tuple(upload_component_ids) + if valid_ids is None: + raise TypeError( + 'The argument "upload_component_ids" should be None, str or [str].' + ) + # Then, check the repetition of the provided ids. + for uid in valid_ids: + if uid in settings.user_configs_query: + raise ValueError( + 'The component id "{0}" has been configured before. A repeated configuration is not allowed.' + ) + return valid_ids + + def configure_upload( - app, folder, use_upload_id=True, upload_api=None, http_request_handler=None + app, + folder, + use_upload_id=True, + upload_api=None, + http_request_handler=None, + upload_component_ids=None, ): r""" Configure the upload APIs for dash app. @@ -17,8 +88,9 @@ def configure_upload( Parameters --------- - app: dash.Dash - The application instance + app: dash.Dash or flask.Flask + The application instance. It is required to be a dash.Dash + for using du.callback. folder: str The folder where to upload files. Can be relative ("uploads") or @@ -44,35 +116,60 @@ def configure_upload( If you provide a class, use a subclass of HttpRequestHandler. See the documentation of dash_uploader.HttpRequestHandler for more details. + upload_component_ids: None or str or [str] + A list of du.Upload component ids. If set None, this configuration would be + regarded as default configurations. If not, the registered app would be + configured for the provided components. """ - settings.UPLOAD_FOLDER_ROOT = folder - settings.app = app + # Check the validity of arguments. + is_dash = check_app(app) + upload_component_ids = check_upload_component_ids(upload_component_ids) + # Configure the API. Extra configs are needed if using a proxy for dash app. if upload_api is None: upload_api = settings.upload_api + if is_dash and upload_api is not None: + routes_pathname_prefix = app.config.get("routes_pathname_prefix", "/") + requests_pathname_prefix = app.config.get("requests_pathname_prefix", "/") + service = update_upload_api(requests_pathname_prefix, upload_api) + full_upload_api = update_upload_api(routes_pathname_prefix, upload_api) else: - # Set the upload api since du.Upload components - # that are created after du.configure_upload - # need to be able to read the api endpoint. - settings.upload_api = upload_api - - # Needed if using a proxy - settings.requests_pathname_prefix = app.config.get("requests_pathname_prefix", "/") - settings.routes_pathname_prefix = app.config.get("routes_pathname_prefix", "/") - - upload_api = update_upload_api(settings.routes_pathname_prefix, upload_api) + service = upload_api + full_upload_api = upload_api + # Set the request handler. if http_request_handler is None: http_request_handler = HttpRequestHandler + server = app.server if is_dash else app decorate_server( - app.server, + server, folder, - upload_api, + full_upload_api, http_request_handler=http_request_handler, use_upload_id=use_upload_id, ) + # If no bugs are triggered, would update the user configs. + # Set the upload api since du.Upload components + # that are created after du.configure_upload + # need to be able to read the api endpoint. + app_idx = len(settings.user_configs) + settings.user_configs.append({ + "app": app, + "service": service, + "upload_api": upload_api, + "upload_folder_root": folder, + "is_dash": is_dash, + "upload_component_ids": upload_component_ids, + }) + # Set the query. + if upload_component_ids is not None: + for uid in upload_component_ids: + settings.user_configs_query[uid] = app_idx + else: + settings.user_configs_default = app_idx + def decorate_server( server, @@ -106,5 +203,6 @@ def decorate_server( server, upload_folder=temp_base, use_upload_id=use_upload_id ) - server.add_url_rule(upload_api, None, handler.get, methods=["GET"]) - server.add_url_rule(upload_api, None, handler.post, methods=["POST"]) + end_point = upload_api.lstrip("/").replace("/", ".") + server.add_url_rule(upload_api, end_point + ".get", handler.get, methods=["GET"]) + server.add_url_rule(upload_api, end_point + ".post", handler.post, methods=["POST"]) diff --git a/dash_uploader/settings.py b/dash_uploader/settings.py index d0409e4..c160858 100644 --- a/dash_uploader/settings.py +++ b/dash_uploader/settings.py @@ -2,19 +2,37 @@ # The du.configure_upload can change this upload_api = "/API/resumable" -# Needed if using a proxy; when dash.Dash is used -# with a `requests_pathname_prefix`. -# The front-end will prefix this string to the requests -# that are made to the proxy server -requests_pathname_prefix = '/' +# User configurations: +# The configuration list is used for storing user-defined configurations. +# Each item is set by an independent du.configure_upload. The list is +# formatted as +# user_configs = { +# { +# 'app': dash.Dash() or flask.Flask(), +# 'service': str, +# 'upload_api': str, +# 'routes_pathname_prefix': str, +# 'requests_pathname_prefix': str, +# 'upload_folder_root': str, +# 'is_dash': bool +# 'upload_component_ids': [str] +# }, +# ... +# } +# It is not recommended to change this dict manually. It should be +# automatically set by du.configure_upload. +user_configs = list() -# From dash source code: -# Note that `requests_pathname_prefix` is the prefix for the AJAX calls that -# originate from the client (the web browser) and `routes_pathname_prefix` is -# the prefix for the API routes on the backend (this flask server). -# `url_base_pathname` will set `requests_pathname_prefix` and -# `routes_pathname_prefix` to the same value. -# If you need these to be different values then you should set -# `requests_pathname_prefix` and `routes_pathname_prefix`, -# not `url_base_pathname`. -routes_pathname_prefix = '/' +# Backward query dict: +# This dictionary is used for fast querying the items in user_configs. It +# is formatted as +# user_configs_query = { +# 'upload_id_1': list_index_1, +# 'upload_id_2': list_index_2, +# ... +# } +# user_configs_query is a str (The default name of the configs.) +# It is not recommended to change this dict manually. It should be +# automatically set by du.configure_upload. +user_configs_query = {} +user_configs_default = None diff --git a/dash_uploader/upload.py b/dash_uploader/upload.py index 5c3cbc6..5c26399 100644 --- a/dash_uploader/upload.py +++ b/dash_uploader/upload.py @@ -16,16 +16,15 @@ } -def update_upload_api(requests_pathname_prefix, upload_api): - '''Path join for the API path name. +def query_service_addr(component_id): + """Query the service address by the given component id. This is a private method, and should not be exposed to users. - ''' - if requests_pathname_prefix == '/': - return upload_api - return '/'.join([ - requests_pathname_prefix.rstrip('/'), - upload_api.lstrip('/'), - ]) + """ + app_idx = settings.user_configs_query.get(component_id, None) + if app_idx is None: + app_idx = settings.user_configs_default + service_addr = settings.user_configs[app_idx]["service"] + return service_addr def combine(overiding_dict, base_dict): @@ -125,8 +124,7 @@ def Upload( if upload_id is None: upload_id = uuid.uuid1() - service = update_upload_api(settings.requests_pathname_prefix, - settings.upload_api) + service = query_service_addr(id) arguments = dict( id=id,