fohrloop / dash-uploader

The alternative upload component for python Dash applications.
MIT License
141 stars 29 forks source link

Feat/callback state #130

Open mapix opened 9 months ago

mapix commented 9 months ago

Ref: https://github.com/fohrloop/dash-uploader/issues/104

mapix commented 9 months ago

@fohrloop The reason I didn't add more tests here is because the current tests are a bit difficult to run on my local machine. If feasible, I will concentrate on adding more tests after more issues are resolved.

mapix commented 9 months ago

The current modification has been running well in my scene for quite a long time, for example:

@du.callback(
    output=[
        PREVIEW_IMAGE_ID.get_output("children"),
        UPLOAD_IMAGE_LOADING_ID.get_output("children"),
        LOGIN_MODAL_ID.get_output("is_open"),
    ],
    state=[
        SESSION_STORE_ID.get_state("data"),
        TOKEN_STORE_ID.get_state("data"),
    ],
    id=UPLOAD_IMAGE_ID.get_identifier(),
)
def callback_on_completion(status: du.UploadStatus, session_id, token):
    if session_id != status.upload_id:
        return no_update, status_alert("Session mismatch.", color="danger"), no_update

    if (not token) or (not is_valid_token(token)):
        return no_update, status_alert("Login required.", color="danger"), True

    session = Session.load(session_id)
    if not session:
        return no_update, status_alert("Session not found.", color="danger"), no_update

    if status.is_completed:
        return (
            render_images(session),
            status_alert(
                f"Successfully uploaded {status.n_uploaded}/{status.n_total}.",
                color="green",
            ),
            no_update,
        )
    return (
        render_images(session),
        status_alert(f"Uploading {status.n_uploaded}...", color="green"),
        no_update,
    )
fohrloop commented 9 months ago

Hi @mapix ,

Glad to see this one getting a fix. Tests would be nice though; what could be done to make testing easier on your local machine?

I don't think I have too much time to contribute on this project, but could we get some user to comment this change? Or someone from your team?

@salvocamiolo / @salvo-camiolo ?

Niko

mapix commented 9 months ago

@fohrloop

The reason for the difficulty with tests is that I always have a critical Case failure on my Mac, but I cannot determine for a while whether it is due to differences in the logic of handling unlink files between Windows and Mac. This error is not related to the current changes. I wonder if other contributors have encountered this issue as well.

Regarding more contributors, @Sisyphus235 from my team will collaborate more on this project.

================================================================================================== test session starts ==================================================================================================
platform darwin -- Python 3.10.12, pytest-7.4.3, pluggy-1.3.0 -- /Users/wfluo/miniconda3/envs/dash/bin/python
cachedir: .pytest_cache
rootdir: /Users/wfluo/workspaces/dash-uploader
configfile: pytest.ini
testpaths: tests/
plugins: anyio-3.7.1, dash-2.13.0
collected 8 items                                                                                                                                                                                                       

tests/test_chromedriver.py::test_chromedriver_version_okay PASSED                                                                                                                                                 [ 12%]
tests/test_usage.py::test_render01_render_component PASSED                                                                                                                                                        [ 25%]
tests/test_usage.py::test_upload01_upload_a_file PASSED                                                                                                                                                           [ 37%]
tests/test_disabled.py::test_disabled01_check_disabled_property_update PASSED                                                                                                                                     [ 50%]
tests/test_disabled.py::test_disabled02_check_disabled_effect PASSED                                                                                                                                              [ 62%]
tests/test_uploading_same_file_twice_with_errors.py::test_uploadtwice01_upload_a_file_twice_and_reserve_it PASSED                                                                                                 [ 75%]
tests/test_uploading_same_file_twice_with_errors.py::test_uploadtwice02_upload_a_file_twice_with_error FAILED                                                                                                     [ 87%]
tests/test_uploadstatus.py::test_uploadstatus_creation_and_serialization PASSED                                                                                                                                   [100%]

======================================================================================================= FAILURES ========================================================================================================
___________________________________________________________________________________ test_uploadtwice02_upload_a_file_twice_with_error ___________________________________________________________________________________

dash_duo = <dash.testing.composite.DashComposite object at 0x10775ca30>, testfile10kb_csv = PosixPath('/Users/wfluo/workspaces/dash-uploader/tests/testfile_for_reupload.csv')
testfile10kb_2_csv = PosixPath('/Users/wfluo/workspaces/dash-uploader/tests/testfile2_for_reupload.csv')

    def test_uploadtwice02_upload_a_file_twice_with_error(
        dash_duo, testfile10kb_csv, testfile10kb_2_csv
    ):
        # Same as test_uploadtwice01_upload_a_file_twice_and_reserve_it
        # but force an error

        # This app does not have retries on the "remove_file"
        # function, and therefore the error alert will appear
        # to the user instantly. (to make tests faster)
        app = import_app("tests.apps.testapp_noretry_remove_file")
        dash_duo.start_server(app)

        def upload_file(file_to_upload):
            # User sees the component
            upload = dash_duo.find_element("#dash-uploader")

            # Upload the file.
            # Clicking the upload component would open a file dialog and
            # this would require the tests to use OS specific GUI tools
            # to select the file. This could be added in the future but it's
            # probably very this would be broken
            upload_input = upload.find_element(
                By.XPATH, "//input[@name='dash-uploader-upload']"
            )
            upload_input.send_keys(str(file_to_upload))

        upload_file(testfile10kb_csv)

        # Wait for "Completed: testfile_for_reupload.csv" text, with 10 second timeout
        WebDriverWait(dash_duo._driver, 10).until(
            EC.text_to_be_present_in_element(
                (By.XPATH, "//div[@id='dash-uploader']/*/label"), testfile10kb_csv.name
            )
        )

        # Get the div with the output values
        callback_output = dash_duo.find_element("#callback-output")
        # Get the name of the uploaded file
        uploaded_file = callback_output.find_element(By.XPATH, "//ul").text
        uploaded_file = Path(uploaded_file)

        assert uploaded_file.name == testfile10kb_csv.name
        assert uploaded_file.exists()
        assert uploaded_file.stat().st_size == testfile10kb_csv.stat().st_size

        # Upload another file to change the labels.
        upload_file(testfile10kb_2_csv)
        # Wait for "Completed: testfile2_for_reupload.csv" text, with 10 second timeout
        WebDriverWait(dash_duo._driver, 10).until(
            EC.text_to_be_present_in_element(
                (By.XPATH, "//div[@id='dash-uploader']/*/label"), testfile10kb_2_csv.name
            )
        )
        # Wait that the callback for the 'testfile2_for_reupload.csv' has been fired.
        WebDriverWait(dash_duo._driver, 10).until(
            EC.text_to_be_present_in_element(
                (By.XPATH, '//*[@id="callback-output"]/ul/li'), testfile10kb_2_csv.name
            )
        )

        # Reserve file & make it impossible to upload testfile10kb_csv
        # Hold the file for 4 seconds
        file_reserve_thread = threading.Thread(
            target=reserve_file_for_while, args=(uploaded_file, HOLD_TIME_FOR_FILE)
        )
        file_reserve_thread.start()

        # Reupload file again with same filename
        upload_file(testfile10kb_csv)

        # Expect to see an error alert text for the user
        # in the following seconds
>       WebDriverWait(dash_duo._driver, 4).until(EC.alert_is_present())

tests/test_uploading_same_file_twice_with_errors.py:214: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <selenium.webdriver.support.wait.WebDriverWait (session="b1af585958442773e1def274d4b51b67")>, method = <function alert_is_present.<locals>._predicate at 0x107780160>, message = ''

    def until(self, method, message=''):
        """Calls the method provided with the driver as an argument until the \
        return value does not evaluate to ``False``.

        :param method: callable(WebDriver)
        :param message: optional message for :exc:`TimeoutException`
        :returns: the result of the last call to `method`
        :raises: :exc:`selenium.common.exceptions.TimeoutException` if timeout occurs
        """
        screen = None
        stacktrace = None

        end_time = time.monotonic() + self._timeout
        while True:
            try:
                value = method(self._driver)
                if value:
                    return value
            except self._ignored_exceptions as exc:
                screen = getattr(exc, 'screen', None)
                stacktrace = getattr(exc, 'stacktrace', None)
            time.sleep(self._poll)
            if time.monotonic() > end_time:
                break
>       raise TimeoutException(message, screen, stacktrace)
E       selenium.common.exceptions.TimeoutException: Message:

../../miniconda3/envs/dash/lib/python3.10/site-packages/selenium/webdriver/support/wait.py:87: TimeoutException
------------------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------------------
Dash is running on http://127.0.0.1:58055/

 * Serving Flask app 'tests.apps.testapp_noretry_remove_file'
 * Debug mode: off
<UploadStatus: latest_file = C:\tmp\Uploads/680cec64-8cf4-11ee-984b-bef0c687ed75/testfile_for_reupload.csv, uploaded_files = [C:\tmp\Uploads/680cec64-8cf4-11ee-984b-bef0c687ed75/testfile_for_reupload.csv], is_completed = True, n_uploaded = 1, n_total = 1, uploaded_size_mb = 0.009766578674316406, total_size_mb = 0.009766578674316406, progress = 1.0, upload_id = 680cec64-8cf4-11ee-984b-bef0c687ed75>
<UploadStatus: latest_file = C:\tmp\Uploads/680cec64-8cf4-11ee-984b-bef0c687ed75/testfile2_for_reupload.csv, uploaded_files = [C:\tmp\Uploads/680cec64-8cf4-11ee-984b-bef0c687ed75/testfile2_for_reupload.csv], is_completed = True, n_uploaded = 1, n_total = 1, uploaded_size_mb = 0.009766578674316406, total_size_mb = 0.009766578674316406, progress = 1.0, upload_id = 680cec64-8cf4-11ee-984b-bef0c687ed75>
<UploadStatus: latest_file = C:\tmp\Uploads/680cec64-8cf4-11ee-984b-bef0c687ed75/testfile_for_reupload.csv, uploaded_files = [C:\tmp\Uploads/680cec64-8cf4-11ee-984b-bef0c687ed75/testfile_for_reupload.csv], is_completed = True, n_uploaded = 1, n_total = 1, uploaded_size_mb = 0.009766578674316406, total_size_mb = 0.009766578674316406, progress = 1.0, upload_id = 680cec64-8cf4-11ee-984b-bef0c687ed75>
--------------------------------------------------------------------------------------------------- Captured log call ---------------------------------------------------------------------------------------------------
15:13:03 | INFO | dash.dash:2040 | Dash is running on http://127.0.0.1:58055/

15:13:03 | INFO | werkzeug:224 | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:58055
15:13:03 | INFO | werkzeug:224 | Press CTRL+C to quit
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET / HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET / HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/deps/polyfill@7.v2_13_0m1697123470.12.1.min.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/deps/react-dom@16.v2_13_0m1697123470.14.0.min.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/deps/prop-types@15.v2_13_0m1697123470.8.1.min.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash_uploader/_build/dash_uploader.v0_7_0-a2m1698038156.min.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/deps/react@16.v2_13_0m1697123470.14.0.min.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1697123469.min.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/dcc/dash_core_components.v2_12_0m1697123469.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/dcc/dash_core_components-shared.v2_12_0m1697123469.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/html/dash_html_components.v2_0_14m1697123470.min.js HTTP/1.1" 200 -
15:13:03 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:03] "GET /_dash-component-suites/dash/dash_table/bundle.v5_2_7m1697123469.js HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "GET /_dash-layout HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "GET /_dash-dependencies HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "GET /_favicon.ico?v=2.13.0 HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "POST /API/dash-uploader HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "POST /_dash-update-component HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "POST /_dash-update-component HTTP/1.1" 204 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "POST /API/dash-uploader HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "POST /_dash-update-component HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "POST /_dash-update-component HTTP/1.1" 204 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "POST /API/dash-uploader HTTP/1.1" 200 -
15:13:04 | INFO | werkzeug:224 | 127.0.0.1 - - [27/Nov/2023 15:13:04] "POST /_dash-update-component HTTP/1.1" 200 -
=================================================================================================== warnings summary ====================================================================================================
tests/test_chromedriver.py::test_chromedriver_version_okay
  /Users/wfluo/miniconda3/envs/dash/lib/python3.10/site-packages/selenium/webdriver/remote/remote_connection.py:390: DeprecationWarning: HTTPResponse.getheader() is deprecated and will be removed in urllib3 v2.1.0. Instead use HTTPResponse.headers.get(name, default).
    if response.getheader('Content-Type'):

tests/test_chromedriver.py::test_chromedriver_version_okay
  /Users/wfluo/miniconda3/envs/dash/lib/python3.10/site-packages/selenium/webdriver/remote/remote_connection.py:391: DeprecationWarning: HTTPResponse.getheader() is deprecated and will be removed in urllib3 v2.1.0. Instead use HTTPResponse.headers.get(name, default).
    content_type = response.getheader('Content-Type').split(';')

tests/test_usage.py: 25 warnings
tests/test_disabled.py: 175 warnings
tests/test_uploading_same_file_twice_with_errors.py: 56 warnings
  /Users/wfluo/miniconda3/envs/dash/lib/python3.10/site-packages/selenium/webdriver/remote/remote_connection.py:390: DeprecationWarning:

  HTTPResponse.getheader() is deprecated and will be removed in urllib3 v2.1.0. Instead use HTTPResponse.headers.get(name, default).

tests/test_usage.py: 25 warnings
tests/test_disabled.py: 175 warnings
tests/test_uploading_same_file_twice_with_errors.py: 56 warnings
  /Users/wfluo/miniconda3/envs/dash/lib/python3.10/site-packages/selenium/webdriver/remote/remote_connection.py:391: DeprecationWarning:

  HTTPResponse.getheader() is deprecated and will be removed in urllib3 v2.1.0. Instead use HTTPResponse.headers.get(name, default).

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
====================================================================================== 1 failed, 7 passed, 514 warnings in 47.66s =======================================================================================