From 0ea63d39f912867072b7b4cdc6f7b20ca8c36501 Mon Sep 17 00:00:00 2001 From: Nicolas Drebenstedt Date: Tue, 4 Feb 2025 11:05:58 +0100 Subject: [PATCH 1/4] update reflex --- .github/workflows/testing.yml | 2 +- README.md | 2 +- mex/drop/api.py | 2 +- mex/drop/exceptions.py | 11 ++ mex/drop/file_history/state.py | 1 + mex/drop/login/state.py | 1 + mex/drop/main.py | 11 +- mex/drop/state.py | 2 + mex/drop/upload/state.py | 3 + mex/mex.py | 24 +--- pdm.lock | 226 +++++++++++++++++---------------- pyproject.toml | 13 +- 12 files changed, 158 insertions(+), 140 deletions(-) create mode 100644 mex/drop/exceptions.py diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 31a3d0e..6005000 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -64,7 +64,7 @@ jobs: env: MEX_DROP_API_KEY_DATABASE: ${{ secrets.MEX_DROP_API_KEY_DATABASE }} run: | - pdm run drop run & + pdm run drop & sleep 30 && curl --connect-timeout 1 --max-time 20 --retry 10 --retry-delay 1 http://localhost:8000/_system/check && make pytest diff --git a/README.md b/README.md index e32e38d..42ffed0 100644 --- a/README.md +++ b/README.md @@ -105,4 +105,4 @@ components of the MEx project are open-sourced under the same license as well. ### Drop -- `pdm run drop run` starts the drop service +- `pdm run drop` starts the drop service diff --git a/mex/drop/api.py b/mex/drop/api.py index ed4b897..a5bf760 100644 --- a/mex/drop/api.py +++ b/mex/drop/api.py @@ -120,7 +120,7 @@ async def drop_data( ) raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Unsupported content typeor format.", + detail="Unsupported content type or format.", ) diff --git a/mex/drop/exceptions.py b/mex/drop/exceptions.py new file mode 100644 index 0000000..8a9bf75 --- /dev/null +++ b/mex/drop/exceptions.py @@ -0,0 +1,11 @@ +import reflex as rx +from reflex.event import EventSpec + + +def custom_backend_handler(exception: Exception) -> EventSpec: + """Custom backend exception handler.""" + if str(exception) == "401: Missing authentication header X-API-Key.": + return rx.toast.error("Please enter your API key.") + if str(exception) == "401: The provided API Key is not recognized.": + return rx.toast.error("Invalid API key.") + return rx.toast.error(f"Backend Error: {exception}") diff --git a/mex/drop/file_history/state.py b/mex/drop/file_history/state.py index 255bc2e..bddaf48 100644 --- a/mex/drop/file_history/state.py +++ b/mex/drop/file_history/state.py @@ -14,6 +14,7 @@ class ListState(State): file_list: list[dict] = [] + @rx.event def get_uploaded_files(self) -> EventSpec | None: """Get the list of files uploaded by the user to X System.""" cast(State, State).check_login() diff --git a/mex/drop/login/state.py b/mex/drop/login/state.py index 6b17cda..0997985 100644 --- a/mex/drop/login/state.py +++ b/mex/drop/login/state.py @@ -11,6 +11,7 @@ class LoginState(State): api_key: str x_system: str + @rx.event def login_user(self) -> EventSpec: """Log in the user.""" authorized_x_systems = get_current_authorized_x_systems(api_key=self.api_key) diff --git a/mex/drop/main.py b/mex/drop/main.py index 12cbcb7..cc8a0be 100644 --- a/mex/drop/main.py +++ b/mex/drop/main.py @@ -1,8 +1,10 @@ +import typer import uvicorn from fastapi import ( FastAPI, ) from pydantic import BaseModel +from reflex.reflex import run from uvicorn.config import LOGGING_CONFIG as DEFAULT_UVICORN_LOGGING_CONFIG from mex.common.cli import entrypoint @@ -39,8 +41,8 @@ def check_system_status() -> SystemStatus: @entrypoint(DropSettings) -def main() -> None: # pragma: no cover - """Start the drop server process.""" +def backend() -> None: # pragma: no cover + """Start the drop fastAPI backend.""" settings = DropSettings.get() uvicorn.run( "mex.drop.main:app", @@ -51,3 +53,8 @@ def main() -> None: # pragma: no cover log_config=UVICORN_LOGGING_CONFIG, headers=[("server", "mex-drop")], ) + + +def main() -> None: # pragma: no cover + """Start the editor service.""" + typer.run(run) diff --git a/mex/drop/state.py b/mex/drop/state.py index acffabd..d3ec0a4 100644 --- a/mex/drop/state.py +++ b/mex/drop/state.py @@ -14,11 +14,13 @@ class State(rx.State): user: User | None = None + @rx.event def logout(self) -> EventSpec: """Log out the user.""" self.reset() return rx.redirect("/") + @rx.event def check_login(self) -> EventSpec | None: """Check if the user is logged in.""" if self.user is None: diff --git a/mex/drop/upload/state.py b/mex/drop/upload/state.py index 9fe02b3..3121000 100644 --- a/mex/drop/upload/state.py +++ b/mex/drop/upload/state.py @@ -20,6 +20,7 @@ class AppState(State): temp_files: list[TempFile] = [] + @rx.event async def handle_upload(self, files: list[rx.UploadFile]) -> EventSpec | None: """Handle the upload of file(s) and save them to the temporary file list. @@ -47,6 +48,7 @@ async def handle_upload(self, files: list[rx.UploadFile]) -> EventSpec | None: self.temp_files.append(TempFile(title=str(file.filename), content=content)) return None + @rx.event async def submit_data(self) -> EventSpec: """Submit temporarily uploaded file(s) and save in corresponding directory. @@ -70,6 +72,7 @@ async def submit_data(self) -> EventSpec: self.temp_files.clear() return rx.toast.success("File upload successful!") + @rx.event def cancel_upload(self, filename: str) -> EventSpec: """Delete file from temporary file list. diff --git a/mex/mex.py b/mex/mex.py index f199a6c..2ed0da6 100644 --- a/mex/mex.py +++ b/mex/mex.py @@ -2,9 +2,9 @@ import reflex as rx from reflex.components.radix import themes -from reflex.event import EventSpec from mex.drop.api import check_system_status, router +from mex.drop.exceptions import custom_backend_handler from mex.drop.file_history.main import file_history_index from mex.drop.file_history.state import ListState from mex.drop.login.main import login_index @@ -14,7 +14,10 @@ app = rx.App( html_lang="en", - theme=themes.theme(accent_color="blue"), + theme=themes.theme( + accent_color="blue", + has_background=False, + ), ) app.add_page( upload_index, @@ -43,20 +46,5 @@ app.api.contact = {"name": "MEx Team", "email": "mex@rki.de"} app.api.description = "Upload and download data for the MEx service." app.api.include_router(router) -app.register_lifespan_task( - DropSettings.get, -) - - -def custom_backend_handler( - exception: Exception, -) -> EventSpec: - """Custom backend exception handler.""" - if str(exception) == "401: Missing authentication header X-API-Key.": - return rx.toast.error("Please enter your API key.") - if str(exception) == "401: The provided API Key is not recognized.": - return rx.toast.error("Invalid API key.") - return rx.toast.error("Backend Error: " + str(exception)) - - +app.register_lifespan_task(DropSettings.get) app.backend_exception_handler = custom_backend_handler diff --git a/pdm.lock b/pdm.lock index a56f375..0e4a40a 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:206a95f6795a6f7dd585b69c41755a5b1842a6f6a6a66cf75fbf006f93c3027b" +content_hash = "sha256:ab96c19751114e62fc21d35a9592350d34ca72e0a144d82dfbc442558aff333b" [[metadata.targets]] requires_python = "==3.11.*" @@ -115,7 +115,7 @@ files = [ [[package]] name = "babel" -version = "2.16.0" +version = "2.17.0" requires_python = ">=3.8" summary = "Internationalization utilities" groups = ["dev"] @@ -124,8 +124,8 @@ dependencies = [ "pytz>=2015.7; python_version < \"3.9\"", ] files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [[package]] @@ -146,7 +146,7 @@ version = "1.2.0" requires_python = ">=3.8" summary = "Backport of CPython tarfile module" groups = ["default"] -marker = "python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version == \"3.11\"" files = [ {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, @@ -199,14 +199,14 @@ files = [ [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." groups = ["default", "dev"] marker = "python_version == \"3.11\"" files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] @@ -215,7 +215,7 @@ version = "1.17.1" requires_python = ">=3.8" summary = "Foreign Function Interface for Python calling C code." groups = ["default"] -marker = "platform_python_implementation != \"PyPy\" and sys_platform == \"linux\" and python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and python_version == \"3.11\"" dependencies = [ "pycparser", ] @@ -282,7 +282,7 @@ version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." groups = ["default", "dev"] -marker = "sys_platform == \"win32\" and python_version == \"3.11\" or os_name == \"nt\" and python_version == \"3.11\" or platform_system == \"Windows\" and python_version == \"3.11\"" +marker = "sys_platform == \"win32\" and python_version == \"3.11\" or platform_system == \"Windows\" and python_version == \"3.11\" or os_name == \"nt\" and python_version == \"3.11\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -294,7 +294,7 @@ version = "44.0.0" requires_python = "!=3.9.0,!=3.9.1,>=3.7" summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." groups = ["default"] -marker = "sys_platform == \"linux\" and python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and python_version == \"3.11\"" dependencies = [ "cffi>=1.12; platform_python_implementation != \"PyPy\"", ] @@ -412,7 +412,7 @@ files = [ [[package]] name = "fastapi" -version = "0.115.7" +version = "0.115.8" requires_python = ">=3.8" summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" groups = ["default"] @@ -423,8 +423,8 @@ dependencies = [ "typing-extensions>=4.8.0", ] files = [ - {file = "fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e"}, - {file = "fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015"}, + {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, + {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, ] [[package]] @@ -463,7 +463,7 @@ files = [ [[package]] name = "fastapi" -version = "0.115.7" +version = "0.115.8" extras = ["standard"] requires_python = ">=3.8" summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" @@ -472,15 +472,15 @@ marker = "python_version == \"3.11\"" dependencies = [ "email-validator>=2.0.0", "fastapi-cli[standard]>=0.0.5", - "fastapi==0.115.7", + "fastapi==0.115.8", "httpx>=0.23.0", "jinja2>=3.1.5", "python-multipart>=0.0.18", "uvicorn[standard]>=0.12.0", ] files = [ - {file = "fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e"}, - {file = "fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015"}, + {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, + {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, ] [[package]] @@ -586,6 +586,21 @@ files = [ {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] +[[package]] +name = "id" +version = "1.5.0" +requires_python = ">=3.8" +summary = "A tool for generating OIDC identities" +groups = ["default"] +marker = "python_version == \"3.11\"" +dependencies = [ + "requests", +] +files = [ + {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"}, + {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"}, +] + [[package]] name = "idna" version = "3.10" @@ -616,7 +631,7 @@ version = "8.6.1" requires_python = ">=3.9" summary = "Read metadata from Python packages" groups = ["default"] -marker = "python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version == \"3.11\"" dependencies = [ "typing-extensions>=3.6.4; python_version < \"3.8\"", "zipp>=3.20", @@ -672,7 +687,7 @@ files = [ [[package]] name = "ipython" -version = "8.31.0" +version = "8.32.0" requires_python = ">=3.10" summary = "IPython: Productive Interactive Computing" groups = ["dev"] @@ -691,8 +706,8 @@ dependencies = [ "typing-extensions>=4.6; python_version < \"3.12\"", ] files = [ - {file = "ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6"}, - {file = "ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b"}, + {file = "ipython-8.32.0-py3-none-any.whl", hash = "sha256:cae85b0c61eff1fc48b0a8002de5958b6528fa9c8defb1894da63f42613708aa"}, + {file = "ipython-8.32.0.tar.gz", hash = "sha256:be2c91895b0b9ea7ba49d33b23e2040c352b33eb6a519cca7ce6e0c743444251"}, ] [[package]] @@ -701,7 +716,7 @@ version = "3.4.0" requires_python = ">=3.8" summary = "Utility functions for Python class constructs" groups = ["default"] -marker = "python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version == \"3.11\"" dependencies = [ "more-itertools", ] @@ -716,7 +731,7 @@ version = "6.0.1" requires_python = ">=3.8" summary = "Useful decorators and context managers" groups = ["default"] -marker = "python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version == \"3.11\"" dependencies = [ "backports-tarfile; python_version < \"3.12\"", ] @@ -731,7 +746,7 @@ version = "4.1.0" requires_python = ">=3.8" summary = "Functools like those found in stdlib" groups = ["default"] -marker = "python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version == \"3.11\"" dependencies = [ "more-itertools", ] @@ -761,7 +776,7 @@ version = "0.8.0" requires_python = ">=3.7" summary = "Low-level, pure Python DBus protocol wrapper." groups = ["default"] -marker = "sys_platform == \"linux\" and python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and python_version == \"3.11\"" files = [ {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, @@ -788,7 +803,7 @@ version = "25.6.0" requires_python = ">=3.9" summary = "Store and access your passwords safely." groups = ["default"] -marker = "python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version == \"3.11\"" dependencies = [ "SecretStorage>=3.2; sys_platform == \"linux\"", "importlib-metadata>=4.11.4; python_version < \"3.12\"", @@ -927,11 +942,11 @@ files = [ [[package]] name = "mex-common" -version = "0.47.0" +version = "0.49.3" requires_python = ">=3.11,<3.13" git = "https://github.com/robert-koch-institut/mex-common.git" -ref = "0.47.0" -revision = "d05390fdf1723d301c78c9fd873c226664c5983b" +ref = "0.49.3" +revision = "408511b12507ab374dda937600875359c131b903" summary = "Common library for MEx python projects." groups = ["default"] marker = "python_version == \"3.11\"" @@ -940,10 +955,10 @@ dependencies = [ "click<9,>=8", "langdetect<2,>=1", "ldap3<3,>=2", - "mex-model @ git+https://github.com/robert-koch-institut/mex-model.git@3.4.0", + "mex-model @ git+https://github.com/robert-koch-institut/mex-model.git@3.5.1", "numpy<3,>=2", "pandas<3,>=2", - "pyarrow<19,>=18", + "pyarrow<20,>=19", "pydantic-settings<3,>=2", "pydantic<2.10,>=2", "pytz<2024.2,>=2024", @@ -952,11 +967,11 @@ dependencies = [ [[package]] name = "mex-model" -version = "3.4.0" +version = "3.5.1" requires_python = ">=3.11,<3.13" git = "https://github.com/robert-koch-institut/mex-model.git" -ref = "3.4.0" -revision = "08d161d40c659a69c52af76b654db41f49616a24" +ref = "3.5.1" +revision = "6216b062053c4de6c25875d7e72b0f683350f7c3" summary = "JSON schema files defining the MEx metadata model." groups = ["default"] marker = "python_version == \"3.11\"" @@ -967,7 +982,7 @@ version = "10.6.0" requires_python = ">=3.9" summary = "More routines for operating on iterables, beyond itertools" groups = ["default"] -marker = "python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version == \"3.11\"" files = [ {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, @@ -1145,18 +1160,6 @@ files = [ {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] -[[package]] -name = "pkginfo" -version = "1.10.0" -requires_python = ">=3.6" -summary = "Query metadata from sdists / bdists / installed packages." -groups = ["default"] -marker = "python_version == \"3.11\"" -files = [ - {file = "pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097"}, - {file = "pkginfo-1.10.0.tar.gz", hash = "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297"}, -] - [[package]] name = "platformdirs" version = "4.3.6" @@ -1171,23 +1174,23 @@ files = [ [[package]] name = "playwright" -version = "1.49.1" +version = "1.50.0" requires_python = ">=3.9" summary = "A high-level API to automate web browsers" groups = ["dev"] marker = "python_version == \"3.11\"" dependencies = [ - "greenlet==3.1.1", - "pyee==12.0.0", + "greenlet<4.0.0,>=3.1.1", + "pyee<13,>=12", ] files = [ - {file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"}, - {file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"}, - {file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"}, - {file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"}, - {file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"}, - {file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"}, - {file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"}, + {file = "playwright-1.50.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:f36d754a6c5bd9bf7f14e8f57a2aea6fd08f39ca4c8476481b9c83e299531148"}, + {file = "playwright-1.50.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40f274384591dfd27f2b014596250b2250c843ed1f7f4ef5d2960ecb91b4961e"}, + {file = "playwright-1.50.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9922ef9bcd316995f01e220acffd2d37a463b4ad10fd73e388add03841dfa230"}, + {file = "playwright-1.50.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:8fc628c492d12b13d1f347137b2ac6c04f98197ff0985ef0403a9a9ee0d39131"}, + {file = "playwright-1.50.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcff35f72db2689a79007aee78f1b0621a22e6e3d6c1f58aaa9ac805bf4497c"}, + {file = "playwright-1.50.0-py3-none-win32.whl", hash = "sha256:3b906f4d351260016a8c5cc1e003bb341651ae682f62213b50168ed581c7558a"}, + {file = "playwright-1.50.0-py3-none-win_amd64.whl", hash = "sha256:1859423da82de631704d5e3d88602d755462b0906824c1debe140979397d2e8d"}, ] [[package]] @@ -1259,20 +1262,20 @@ files = [ [[package]] name = "pyarrow" -version = "18.1.0" +version = "19.0.0" requires_python = ">=3.9" summary = "Python library for Apache Arrow" groups = ["default"] marker = "python_version == \"3.11\"" files = [ - {file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:eaeabf638408de2772ce3d7793b2668d4bb93807deed1725413b70e3156a7854"}, - {file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:3b2e2239339c538f3464308fd345113f886ad031ef8266c6f004d49769bb074c"}, - {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39a2e0ed32a0970e4e46c262753417a60c43a3246972cfc2d3eb85aedd01b21"}, - {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31e9417ba9c42627574bdbfeada7217ad8a4cbbe45b9d6bdd4b62abbca4c6f6"}, - {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01c034b576ce0eef554f7c3d8c341714954be9b3f5d5bc7117006b85fcf302fe"}, - {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f266a2c0fc31995a06ebd30bcfdb7f615d7278035ec5b1cd71c48d56daaf30b0"}, - {file = "pyarrow-18.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4f13eee18433f99adefaeb7e01d83b59f73360c231d4782d9ddfaf1c3fbde0a"}, - {file = "pyarrow-18.1.0.tar.gz", hash = "sha256:9386d3ca9c145b5539a1cfc75df07757dff870168c959b473a0bccbc3abc8c73"}, + {file = "pyarrow-19.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8e3a839bf36ec03b4315dc924d36dcde5444a50066f1c10f8290293c0427b46a"}, + {file = "pyarrow-19.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ce42275097512d9e4e4a39aade58ef2b3798a93aa3026566b7892177c266f735"}, + {file = "pyarrow-19.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9348a0137568c45601b031a8d118275069435f151cbb77e6a08a27e8125f59d4"}, + {file = "pyarrow-19.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0144a712d990d60f7f42b7a31f0acaccf4c1e43e957f7b1ad58150d6f639c1"}, + {file = "pyarrow-19.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2a1a109dfda558eb011e5f6385837daffd920d54ca00669f7a11132d0b1e6042"}, + {file = "pyarrow-19.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:be686bf625aa7b9bada18defb3a3ea3981c1099697239788ff111d87f04cd263"}, + {file = "pyarrow-19.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:239ca66d9a05844bdf5af128861af525e14df3c9591bcc05bac25918e650d3a2"}, + {file = "pyarrow-19.0.0.tar.gz", hash = "sha256:8d47c691765cf497aaeed4954d226568563f1b3b74ff61139f2d77876717084b"}, ] [[package]] @@ -1293,7 +1296,7 @@ version = "2.22" requires_python = ">=3.8" summary = "C parser in Python" groups = ["default"] -marker = "platform_python_implementation != \"PyPy\" and sys_platform == \"linux\" and python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and python_version == \"3.11\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1361,7 +1364,7 @@ files = [ [[package]] name = "pyee" -version = "12.0.0" +version = "12.1.1" requires_python = ">=3.8" summary = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" groups = ["dev"] @@ -1370,8 +1373,8 @@ dependencies = [ "typing-extensions", ] files = [ - {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"}, - {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"}, + {file = "pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef"}, + {file = "pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3"}, ] [[package]] @@ -1436,7 +1439,7 @@ files = [ [[package]] name = "pytest-playwright" -version = "0.6.2" +version = "0.7.0" requires_python = ">=3.9" summary = "A pytest wrapper with fixtures for Playwright to automate web browsers" groups = ["dev"] @@ -1448,8 +1451,8 @@ dependencies = [ "python-slugify<9.0.0,>=6.0.0", ] files = [ - {file = "pytest_playwright-0.6.2-py3-none-any.whl", hash = "sha256:0eff73bebe497b0158befed91e2f5fe94cfa17181f8b3acf575beed84e7e9043"}, - {file = "pytest_playwright-0.6.2.tar.gz", hash = "sha256:ff4054b19aa05df096ac6f74f0572591566aaf0f6d97f6cb9674db8a4d4ed06c"}, + {file = "pytest_playwright-0.7.0-py3-none-any.whl", hash = "sha256:2516d0871fa606634bfe32afbcc0342d68da2dbff97fe3459849e9c428486da2"}, + {file = "pytest_playwright-0.7.0.tar.gz", hash = "sha256:b3f2ea514bbead96d26376fac182f68dcd6571e7cb41680a89ff1673c05d60b6"}, ] [[package]] @@ -1569,7 +1572,7 @@ version = "0.2.3" requires_python = ">=3.6" summary = "A (partial) reimplementation of pywin32 using ctypes/cffi" groups = ["default"] -marker = "sys_platform == \"win32\" and python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"win32\" and python_version == \"3.11\"" files = [ {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, @@ -1629,7 +1632,7 @@ files = [ [[package]] name = "reflex" -version = "0.6.3.post1" +version = "0.6.8" requires_python = "<4.0,>=3.9" summary = "Web apps in pure Python." groups = ["default"] @@ -1653,22 +1656,23 @@ dependencies = [ "python-socketio<6.0,>=5.7.0", "redis<6.0,>=4.3.5", "reflex-chakra>=0.6.0", - "reflex-hosting-cli<2.0,>=0.1.2", + "reflex-hosting-cli<2.0,>=0.1.29", "rich<14.0,>=13.0.0", "setuptools>=75.0", "sqlmodel<0.1,>=0.0.14", "starlette-admin<1.0,>=0.11.0", "tomlkit<1.0,>=0.12.4", - "twine<6.0,>=4.0.0", + "twine<7.0,>=4.0.0", "typer<1.0,>=0.4.2", + "typing-extensions>=4.6.0", "uvicorn>=0.20.0", "wheel<1.0,>=0.42.0", "wrapt<2.0,>=1.11.0; python_version < \"3.11\"", "wrapt<2.0,>=1.14.0; python_version >= \"3.11\"", ] files = [ - {file = "reflex-0.6.3.post1-py3-none-any.whl", hash = "sha256:47c2324285327b0d7bb855613a59ef8b8aa5050640f6f2462f92ca734c59f47c"}, - {file = "reflex-0.6.3.post1.tar.gz", hash = "sha256:7645cdcdd00b15dabd1642ce39527b0bfb39813956450ab0b0a78dadc929dcfa"}, + {file = "reflex-0.6.8-py3-none-any.whl", hash = "sha256:958f4e81eff1eccd553200dc80b93980b5888dd712bbf51f436d4d245037acec"}, + {file = "reflex-0.6.8.tar.gz", hash = "sha256:cc6892e235902b6c86bc64ee6c0459d7c73f9ffedbc37f91d8b48de76a74a688"}, ] [[package]] @@ -1688,7 +1692,7 @@ files = [ [[package]] name = "reflex-hosting-cli" -version = "0.1.33" +version = "0.1.34" requires_python = "<4.0,>=3.9" summary = "Reflex Hosting CLI" groups = ["default"] @@ -1704,8 +1708,8 @@ dependencies = [ "typer<1,>=0.15.0", ] files = [ - {file = "reflex_hosting_cli-0.1.33-py3-none-any.whl", hash = "sha256:3fe72fc448a231c61de4ac646f42c936c70e91330f616a23aec658f905d53bc4"}, - {file = "reflex_hosting_cli-0.1.33.tar.gz", hash = "sha256:81c4a896b106eea99f1cab53ea23a6e19802592ce0468cc38d93d440bc95263a"}, + {file = "reflex_hosting_cli-0.1.34-py3-none-any.whl", hash = "sha256:eabc4dc7bf68e022a9388614c1a35b5ab36b01021df063d0c3356eda0e245264"}, + {file = "reflex_hosting_cli-0.1.34.tar.gz", hash = "sha256:07be37fda6dcede0a5d4bc1fd1786d9a3df5ad4e49dc1b6ba335418563cfecec"}, ] [[package]] @@ -1789,30 +1793,30 @@ files = [ [[package]] name = "ruff" -version = "0.9.3" +version = "0.9.4" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["dev"] marker = "python_version == \"3.11\"" files = [ - {file = "ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624"}, - {file = "ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c"}, - {file = "ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b"}, - {file = "ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c"}, - {file = "ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4"}, - {file = "ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b"}, - {file = "ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a"}, + {file = "ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706"}, + {file = "ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf"}, + {file = "ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0"}, + {file = "ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402"}, + {file = "ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e"}, + {file = "ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41"}, + {file = "ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7"}, ] [[package]] @@ -1821,7 +1825,7 @@ version = "3.3.3" requires_python = ">=3.6" summary = "Python bindings to FreeDesktop.org Secret Service API" groups = ["default"] -marker = "sys_platform == \"linux\" and python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and python_version == \"3.11\"" dependencies = [ "cryptography>=2.0", "jeepney>=0.6", @@ -2147,16 +2151,16 @@ files = [ [[package]] name = "twine" -version = "5.1.1" +version = "6.1.0" requires_python = ">=3.8" summary = "Collection of utilities for publishing packages on PyPI" groups = ["default"] marker = "python_version == \"3.11\"" dependencies = [ - "importlib-metadata>=3.6", - "keyring>=15.1", - "pkginfo<1.11", - "pkginfo>=1.8.1", + "id", + "importlib-metadata>=3.6; python_version < \"3.10\"", + "keyring>=15.1; platform_machine != \"ppc64le\" and platform_machine != \"s390x\"", + "packaging>=24.0", "readme-renderer>=35.0", "requests-toolbelt!=0.9.0,>=0.8.0", "requests>=2.20", @@ -2165,8 +2169,8 @@ dependencies = [ "urllib3>=1.26.0", ] files = [ - {file = "twine-5.1.1-py3-none-any.whl", hash = "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997"}, - {file = "twine-5.1.1.tar.gz", hash = "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db"}, + {file = "twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384"}, + {file = "twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd"}, ] [[package]] @@ -2400,7 +2404,7 @@ version = "3.21.0" requires_python = ">=3.9" summary = "Backport of pathlib-compatible object wrapper for zip files" groups = ["default"] -marker = "python_version == \"3.11\"" +marker = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version == \"3.11\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, diff --git a/pyproject.toml b/pyproject.toml index 39103d7..4002124 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,17 +10,18 @@ requires-python = ">=3.11,<3.13" dependencies = [ "aiofile>=3,<4", "fastapi[standard]>=0.110,<1", - "mex-common @ git+https://github.com/robert-koch-institut/mex-common.git@0.47.0", + "mex-common @ git+https://github.com/robert-koch-institut/mex-common.git@0.49.3", "pydantic>=2,<3", - "starlette>=0.37,<1", - "reflex>=0.5,<0.6.4", + "reflex>=0.6,<1", + "starlette>=0.45,<1", + "typer<1,>=0.15", ] optional-dependencies.dev = [ "ipdb>=0.13,<1", "mypy>=1,<2", "nest-asyncio>=1,<2", "openpyxl>=3,<4", - "pytest-playwright>=0.5,<1", + "pytest-playwright>=0.7,<1", "pytest-random-order>=1,<2", "pytest>=8,<9", "ruff>=0.9,<1", @@ -28,8 +29,8 @@ optional-dependencies.dev = [ ] [project.scripts] -drop = "reflex.reflex:cli" -backend-only = "mex.drop.main:main" +drop = "mex.drop.main:main" +backend-only = "mex.drop.main:backend" [tool.cruft] template = "https://github.com/robert-koch-institut/mex-template" From e232cbf2b785b6df57022f1776c3cdbbf22f0504 Mon Sep 17 00:00:00 2001 From: Nicolas Drebenstedt Date: Tue, 4 Feb 2025 14:29:40 +0100 Subject: [PATCH 2/4] align mex-drop with the editor codebase --- mex/drop/api/__init__.py | 0 mex/drop/{api.py => api/main.py} | 0 mex/drop/file_history/main.py | 37 +++--- mex/drop/file_history/state.py | 11 +- mex/drop/layout.py | 127 +++++++++++++++++++++ mex/drop/login/main.py | 133 ++++++++++------------ mex/drop/login/state.py | 14 +-- mex/drop/main.py | 16 +-- mex/drop/models.py | 18 +++ mex/drop/navigation.py | 53 --------- mex/drop/state.py | 28 +++-- mex/drop/upload/main.py | 143 ++++++++++++++---------- mex/drop/upload/state.py | 2 +- mex/mex.py | 37 +++--- tests/api/__init__.py | 0 tests/{test_api.py => api/test_main.py} | 0 tests/file_history/test_main.py | 2 +- tests/file_history/test_state.py | 10 +- tests/login/test_main.py | 2 +- tests/login/test_state.py | 8 +- tests/upload/test_main.py | 2 +- 21 files changed, 369 insertions(+), 274 deletions(-) create mode 100644 mex/drop/api/__init__.py rename mex/drop/{api.py => api/main.py} (100%) create mode 100644 mex/drop/layout.py create mode 100644 mex/drop/models.py delete mode 100644 mex/drop/navigation.py create mode 100644 tests/api/__init__.py rename tests/{test_api.py => api/test_main.py} (100%) diff --git a/mex/drop/api/__init__.py b/mex/drop/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mex/drop/api.py b/mex/drop/api/main.py similarity index 100% rename from mex/drop/api.py rename to mex/drop/api/main.py diff --git a/mex/drop/file_history/main.py b/mex/drop/file_history/main.py index 8ceee63..34f8454 100644 --- a/mex/drop/file_history/main.py +++ b/mex/drop/file_history/main.py @@ -1,7 +1,7 @@ import reflex as rx from mex.drop.file_history.state import ListState -from mex.drop.navigation import nav_bar +from mex.drop.layout import page def render_file_row(file: dict) -> rx.Component: @@ -39,31 +39,20 @@ def uploaded_files_display() -> rx.Component: ) -def file_history_index() -> rx.Component: - """Return the index for the file history page.""" - return rx.box( - nav_bar(), - rx.center( - rx.card( - rx.vstack( - rx.text("File Upload History", weight="bold", align="center"), - rx.divider(size="4"), - rx.scroll_area( - rx.flex( - uploaded_files_display(), - ), - type="always", - scrollbars="vertical", - height=350, +def index() -> rx.Component: + """Return the index for the file history component.""" + return page( + rx.card( + rx.vstack( + rx.scroll_area( + rx.flex( + uploaded_files_display(), ), + type="hover", + scrollbars="vertical", ), - width="70%", - padding="2em", - margin="4em", - custom_attrs={"data-testid": "index-card"}, ), + custom_attrs={"data-testid": "index-card"}, + width="100%", ), - background_color="var(--gray-2)", - height="100%", - padding="2em", ) diff --git a/mex/drop/file_history/state.py b/mex/drop/file_history/state.py index bddaf48..b6fd7be 100644 --- a/mex/drop/file_history/state.py +++ b/mex/drop/file_history/state.py @@ -1,6 +1,5 @@ import pathlib from datetime import UTC, datetime -from typing import cast import reflex as rx from reflex.event import EventSpec @@ -15,12 +14,12 @@ class ListState(State): file_list: list[dict] = [] @rx.event - def get_uploaded_files(self) -> EventSpec | None: - """Get the list of files uploaded by the user to X System.""" - cast(State, State).check_login() - if not self.user: - return rx.toast.error("No User logged in.", close_button=True) + def refresh(self) -> EventSpec | None: + """Refresh the list of files uploaded by the user to X-System.""" settings = DropSettings.get() + if not self.user: # pragma: no cover + msg = "Should have redirected to login." + raise RuntimeError(msg) x_system_data_dir = pathlib.Path( settings.drop_directory, str(self.user.x_system) ) diff --git a/mex/drop/layout.py b/mex/drop/layout.py new file mode 100644 index 0000000..b02b811 --- /dev/null +++ b/mex/drop/layout.py @@ -0,0 +1,127 @@ +from typing import cast + +import reflex as rx + +from mex.drop.models import NavItem, User +from mex.drop.state import State + + +def user_button() -> rx.Component: + """Return a user button with an icon that indicates their access rights.""" + return rx.button( + rx.icon(tag="user_round_cog"), + variant="ghost", + style={"marginTop": "0"}, + ) + + +def user_menu() -> rx.Component: + """Return a user menu with a trigger, the current X-System and a logout button.""" + return rx.menu.root( + rx.menu.trigger(user_button()), + rx.menu.content( + rx.menu.item(cast(User, State.user).x_system, disabled=True), + rx.menu.separator(), + rx.menu.item( + "Logout", + on_select=State.logout, + ), + ), + custom_attrs={"data-testid": "user-menu"}, + ) + + +def nav_link(item: NavItem) -> rx.Component: + """Return a link component for the given navigation item.""" + return rx.link( + rx.text(item.title, size="4", weight="medium"), + href=item.path, + underline=item.underline, + class_name="nav-item", + ) + + +def app_logo() -> rx.Component: + """Return the app logo with icon and label.""" + return rx.hstack( + rx.icon( + "droplets", + size=28, + ), + rx.heading( + "MEx Drop", + weight="medium", + style={"userSelect": "none"}, + ), + custom_attrs={"data-testid": "app-logo"}, + ) + + +def nav_bar() -> rx.Component: + """Return a navigation bar component.""" + return rx.vstack( + rx.box( + style={ + "height": "var(--space-6)", + "width": "100%", + "backdropFilter": " var(--backdrop-filter-panel)", + "backgroundColor": "var(--card-background-color)", + }, + ), + rx.card( + rx.hstack( + app_logo(), + rx.divider(orientation="vertical", size="2"), + rx.hstack( + rx.foreach(State.nav_items, nav_link), + justify="start", + spacing="4", + ), + rx.divider(orientation="vertical", size="2"), + user_menu(), + rx.spacer(), + rx.color_mode.button(), + justify="between", + align_items="center", + ), + size="2", + custom_attrs={"data-testid": "nav-bar"}, + style={ + "width": "100%", + "marginTop": "calc(-1 * var(--base-card-border-width))", + }, + ), + spacing="0", + style={ + "maxWidth": "calc(1480px * var(--scaling))", + "minWidth": "calc(800px * var(--scaling))", + "position": "fixed", + "top": "0", + "width": "100%", + "zIndex": "1000", + }, + ) + + +def page(*children: rx.Component) -> rx.Component: + """Return a page fragment with navigation bar and given children.""" + return rx.cond( + State.user, + rx.center( + nav_bar(), + rx.hstack( + *children, + style={ + "maxWidth": "calc(1480px * var(--scaling))", + "minWidth": "calc(800px * var(--scaling))", + "padding": "calc(var(--space-6) * 4) var(--space-6) var(--space-6)", + "width": "100%", + }, + custom_attrs={"data-testid": "page-body"}, + ), + ), + rx.center( + rx.spinner(size="3"), + style={"marginTop": "40vh"}, + ), + ) diff --git a/mex/drop/login/main.py b/mex/drop/login/main.py index dadbf09..e44ef32 100644 --- a/mex/drop/login/main.py +++ b/mex/drop/login/main.py @@ -1,88 +1,75 @@ -from typing import cast - import reflex as rx +from mex.drop.layout import app_logo from mex.drop.login.state import LoginState -from mex.drop.navigation import mex_drop_logo -def login_form() -> rx.Component: - """Return login form components.""" - return rx.card( - rx.vstack( - rx.vstack( - rx.hstack( - rx.text( - "X System", - size="3", - weight="medium", - ), - justify="between", - width="100%", - ), - rx.input( - placeholder="Enter X System name", - on_change=cast(LoginState, LoginState).set_x_system(), - required=True, - type="text", - size="3", - width="100%", - ), - spacing="2", - width="100%", - ), - rx.vstack( - rx.text( - "API Key", - size="3", - weight="medium", - text_align="left", - width="100%", - ), - rx.input( - placeholder="Enter API key", - on_change=cast(LoginState, LoginState).set_api_key(), - required=True, - type="password", - size="3", - width="100%", - ), - justify="start", - spacing="2", - width="100%", - ), - rx.button( - "Log in", - on_click=cast(LoginState, LoginState).login_user(), - size="3", - width="100%", - ), - spacing="6", - width="100%", +def login_x_system() -> rx.Component: + """Return a form field for the X-System.""" + return rx.vstack( + rx.text("X-System"), + rx.input( + autofocus=True, + on_change=LoginState.set_x_system, + placeholder="X-System", + size="3", + tab_index=1, + style={"width": "80%"}, + ), + style={"width": "100%"}, + ) + + +def login_api_key() -> rx.Component: + """Return a form field for the API key.""" + return rx.vstack( + rx.text("API key"), + rx.input( + on_change=LoginState.set_api_key, + placeholder="API key", + size="3", + tab_index=2, + type="password", + style={"width": "80%"}, ), - size="4", - max_width="28em", - width="100%", + style={"width": "100%"}, ) -def login_index() -> rx.Component: +def login_button() -> rx.Component: + """Return a submit button for the login form.""" + return rx.button( + "Log in", + on_click=LoginState.login, + size="3", + tab_index=3, + style={ + "padding": "0 var(--space-6)", + "marginTop": "var(--space-4)", + }, + ) + + +def index() -> rx.Component: """Return the index for the login component.""" - return rx.box( - rx.center( - rx.card( + return rx.center( + rx.card( + rx.vstack( + app_logo(), + rx.divider(size="4"), rx.vstack( - mex_drop_logo(), - rx.divider(size="4"), - login_form(), - spacing="4", + login_x_system(), + login_api_key(), + login_button(), + style={"width": "100%"}, ), - top="20vh", - width="400px", - variant="classic", - custom_attrs={"data-testid": "login-card"}, + spacing="4", ), + style={ + "width": "calc(400px * var(--scaling))", + "top": "20vh", + }, + variant="classic", + custom_attrs={"data-testid": "login-card"}, ), - background_color="var(--gray-2)", - min_height="100vh", ) diff --git a/mex/drop/login/state.py b/mex/drop/login/state.py index 0997985..36a5d21 100644 --- a/mex/drop/login/state.py +++ b/mex/drop/login/state.py @@ -12,13 +12,13 @@ class LoginState(State): x_system: str @rx.event - def login_user(self) -> EventSpec: + def login(self) -> EventSpec: """Log in the user.""" authorized_x_systems = get_current_authorized_x_systems(api_key=self.api_key) - if not is_authorized(str(self.x_system), authorized_x_systems): - return rx.toast.error( - "API Key not authorized to drop data for this x_system.", - close_button=True, + if is_authorized(str(self.x_system), authorized_x_systems): + self.user = User( + api_key=self.api_key, + x_system=self.x_system, ) - self.user = User(api_key=self.api_key, x_system=self.x_system) - return rx.redirect("/upload") + return rx.redirect("/") + return rx.window_alert("Invalid credentials.") diff --git a/mex/drop/main.py b/mex/drop/main.py index cc8a0be..a5c4dc1 100644 --- a/mex/drop/main.py +++ b/mex/drop/main.py @@ -3,13 +3,12 @@ from fastapi import ( FastAPI, ) -from pydantic import BaseModel from reflex.reflex import run from uvicorn.config import LOGGING_CONFIG as DEFAULT_UVICORN_LOGGING_CONFIG from mex.common.cli import entrypoint from mex.common.logging import logger -from mex.drop.api import router +from mex.drop.api.main import check_system_status, router from mex.drop.settings import DropSettings UVICORN_LOGGING_CONFIG = DEFAULT_UVICORN_LOGGING_CONFIG.copy() @@ -26,18 +25,7 @@ description="Upload and download data for the MEx service.", ) app.include_router(router) - - -class SystemStatus(BaseModel): - """Response model for system status check.""" - - status: str - - -@app.get("/_system/check", tags=["system"]) -def check_system_status() -> SystemStatus: - """Check that the drop server is healthy and responsive.""" - return SystemStatus(status="ok") +app.add_api_route("/_system/check", check_system_status, tags=["system"]) @entrypoint(DropSettings) diff --git a/mex/drop/models.py b/mex/drop/models.py new file mode 100644 index 0000000..23eb800 --- /dev/null +++ b/mex/drop/models.py @@ -0,0 +1,18 @@ +from typing import Literal + +import reflex as rx + + +class User(rx.Base): + """Info on the currently logged-in user.""" + + x_system: str + api_key: str + + +class NavItem(rx.Base): + """Model for one navigation bar item.""" + + title: str = "" + path: str = "/" + underline: Literal["always", "none"] = "none" diff --git a/mex/drop/navigation.py b/mex/drop/navigation.py deleted file mode 100644 index c41274f..0000000 --- a/mex/drop/navigation.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import cast - -import reflex as rx - -from mex.drop.state import State - - -def mex_drop_logo() -> rx.Component: - """Return the mex-drop logo with icon and label.""" - return rx.hstack( - rx.icon( - "droplets", - size=28, - ), - rx.heading( - "MEx Drop", - weight="medium", - style={"user-select": "none"}, - ), - custom_attrs={"data-testid": "drop-logo"}, - ) - - -def navbar_link(text: str, url: str) -> rx.Component: - """Return a navigation bar item link.""" - return rx.link(rx.text(text, size="4", weight="medium"), href=url) - - -def nav_bar() -> rx.Component: - """Return the navigation bar with logo, page links and logout button.""" - return rx.card( - rx.hstack( - mex_drop_logo(), - rx.divider(size="2", orientation="vertical"), - rx.hstack( - navbar_link("Upload", "/upload"), - navbar_link("File History", "/file-history"), - justify="start", - spacing="5", - ), - rx.spacer(spacing="4"), - rx.button( - "Logout", - on_click=cast(State, State).logout(), - size="3", - bg="royalblue", - ), - justify="between", - align_items="center", - ), - size="2", - width="100%", - ) diff --git a/mex/drop/state.py b/mex/drop/state.py index d3ec0a4..7a1d986 100644 --- a/mex/drop/state.py +++ b/mex/drop/state.py @@ -1,18 +1,23 @@ import reflex as rx from reflex.event import EventSpec - -class User(rx.Base): - """Info on the currently logged-in user.""" - - x_system: str - api_key: str +from mex.drop.models import NavItem, User class State(rx.State): """The app state.""" user: User | None = None + nav_items: list[NavItem] = [ + NavItem( + title="Upload", + path="/", + ), + NavItem( + title="File History", + path="/file-history", + ), + ] @rx.event def logout(self) -> EventSpec: @@ -24,5 +29,14 @@ def logout(self) -> EventSpec: def check_login(self) -> EventSpec | None: """Check if the user is logged in.""" if self.user is None: - return rx.redirect("/") + return rx.redirect("/login") return None + + @rx.event + def load_nav(self) -> None: + """Event hook for updating the navigation on page loads.""" + for nav_item in self.nav_items: + if self.router.page.path == nav_item.path: + nav_item.underline = "always" + else: + nav_item.underline = "none" diff --git a/mex/drop/upload/main.py b/mex/drop/upload/main.py index 3778560..0889459 100644 --- a/mex/drop/upload/main.py +++ b/mex/drop/upload/main.py @@ -1,26 +1,40 @@ +from typing import cast + import reflex as rx from mex.drop.files_io import ALLOWED_CONTENT_TYPES -from mex.drop.navigation import nav_bar +from mex.drop.layout import page from mex.drop.upload.state import AppState, TempFile def uploaded_file_display() -> rx.Component: """Displays list of uploaded files from drop interface.""" - return rx.table.root( - rx.table.header( - rx.table.row( - rx.table.column_header_cell("Selected File", width=260), - rx.table.column_header_cell("Actions", width=120), + return rx.scroll_area( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell( + "Selected File", + style={"width": "80%"}, + ), + rx.table.column_header_cell( + "Action", + ), + ), ), - ), - rx.table.body( - rx.foreach( - AppState.temp_files, - create_file_row, + rx.table.body( + rx.foreach( + AppState.temp_files, + create_file_row, + ), ), ), - width="100%", + type="hover", + scrollbars="both", + style={ + "width": "100%", + "height": "100%", + }, ) @@ -28,18 +42,21 @@ def create_file_row(temp_file: TempFile) -> rx.Component: """Create table row for each uploaded file. Args: - temp_file (TempFile): temporarily uploaded file + temp_file: temporarily uploaded file """ return rx.table.row( - rx.table.row_header_cell(temp_file.title, width=280), + rx.table.row_header_cell(temp_file.title), rx.table.cell( rx.button( rx.icon(tag="trash-2"), - title="remove file", - color_scheme="red", - on_click=lambda: AppState.cancel_upload(temp_file.title), # type: ignore[arg-type,call-arg] + title="Remove file", + color_scheme="tomato", + variant="ghost", + on_click=lambda: cast(AppState, AppState).cancel_upload( + temp_file.title + ), ), - width=100, + style={"width": "100%"}, ), ) @@ -48,14 +65,21 @@ def create_drag_and_drop() -> rx.Component: """Create card for drag and drop area for file selection.""" return rx.card( rx.vstack( - rx.text("File Upload", size="3", weight="bold"), + rx.text( + "File Upload", + size="2", + weight="bold", + style={ + "padding": "calc(12px * var(--scaling)) " + "calc(12px * var(--scaling)) 0;" + }, + ), rx.divider(size="4"), - rx.text("Please select and upload your files here.", size="2"), rx.upload( rx.vstack( rx.icon( "file-down", - size=30, + size=28, ), rx.text( "Drag and drop or click to select files", @@ -69,22 +93,29 @@ def create_drag_and_drop() -> rx.Component: ), rx.button( "Select Files", - bg="royalblue", + variant="surface", ), align="center", ), - id="upload_one", + id="file_upload_area", max_files=100, - border="1px dotted var(--accent-8)", - padding="3em", - on_drop=AppState.handle_upload(rx.upload_files(upload_id="upload_one")), # type: ignore[call-arg] + style={ + "border": "var(--card-border-width) dotted var(--accent-8)", + "borderRadius": "calc(var(--base-card-border-radius) - " + "var(--base-card-border-width))", + "padding": "var(--space-4)", + "margin": "var(--space-8) auto", + }, + on_drop=cast(AppState, AppState).handle_upload( + rx.upload_files(upload_id="file_upload_area") + ), ), - spacing="4", + spacing="3", ), - width="100%", - height="100%", - padding="15px", - custom_attrs={"data-testid": "index-card"}, + style={ + "width": "100%", + "height": "100%", + }, ) @@ -92,44 +123,34 @@ def create_file_handling_card() -> rx.Component: """Create card for file handling and upload.""" return rx.card( rx.vstack( - rx.scroll_area( - rx.flex( - uploaded_file_display(), - ), - type="always", - scrollbars="both", - height=285, - ), + uploaded_file_display(), rx.hstack( rx.spacer(spacing="3"), rx.button( "Submit", - on_click=AppState.submit_data, # type: ignore[call-arg] - bg="royalblue", + on_click=AppState.submit_data, + color_scheme="jade", ), - width="100%", + style={"width": "100%"}, ), + style={"height": "100%"}, ), - width="100%", - height="100%", + style={ + "width": "100%", + "height": "100%", + }, ) -def upload_index() -> rx.Component: - """Return the index for the drop app.""" - return rx.box( - nav_bar(), - rx.center( - rx.container( - rx.hstack( - create_drag_and_drop(), - create_file_handling_card(), - width="100%", - margin="4em", - ) - ) - ), - background_color="var(--gray-2)", - min_height="100vh", - padding="2em", +def index() -> rx.Component: + """Return the index for the upload component.""" + return page( + rx.hstack( + create_drag_and_drop(), + create_file_handling_card(), + style={ + "width": "100%", + "height": "calc(480px * var(--scaling))", + }, + ) ) diff --git a/mex/drop/upload/state.py b/mex/drop/upload/state.py index 3121000..938e341 100644 --- a/mex/drop/upload/state.py +++ b/mex/drop/upload/state.py @@ -77,7 +77,7 @@ def cancel_upload(self, filename: str) -> EventSpec: """Delete file from temporary file list. Args: - filename (str): title of file to be deleted + filename: title of file to be deleted Returns: EventSpec: Reflex event, toast info message diff --git a/mex/mex.py b/mex/mex.py index 2ed0da6..8fdaa6b 100644 --- a/mex/mex.py +++ b/mex/mex.py @@ -1,16 +1,16 @@ -from typing import cast - import reflex as rx from reflex.components.radix import themes +from reflex.utils.console import info as log_info -from mex.drop.api import check_system_status, router +from mex.common.logging import logger +from mex.drop.api.main import check_system_status, router from mex.drop.exceptions import custom_backend_handler -from mex.drop.file_history.main import file_history_index +from mex.drop.file_history.main import index as file_history_index from mex.drop.file_history.state import ListState -from mex.drop.login.main import login_index +from mex.drop.login.main import index as login_index from mex.drop.settings import DropSettings from mex.drop.state import State -from mex.drop.upload.main import upload_index +from mex.drop.upload.main import index as upload_index app = rx.App( html_lang="en", @@ -21,20 +21,20 @@ ) app.add_page( upload_index, - route="/upload", - title="MEx Drop", - on_load=cast(State, State).check_login(), -) -app.add_page( - login_index, route="/", - title="MEx Drop | Login", + title="MEx Drop | Upload", + on_load=[State.check_login, State.load_nav], ) app.add_page( file_history_index, route="/file-history", title="MEx Drop | File History", - on_load=cast(ListState, ListState).get_uploaded_files(), + on_load=[State.check_login, State.load_nav, ListState.refresh], +) +app.add_page( + login_index, + route="/login", + title="MEx Drop | Login", ) app.api.add_api_route( "/_system/check", @@ -46,5 +46,12 @@ app.api.contact = {"name": "MEx Team", "email": "mex@rki.de"} app.api.description = "Upload and download data for the MEx service." app.api.include_router(router) -app.register_lifespan_task(DropSettings.get) app.backend_exception_handler = custom_backend_handler + +app.register_lifespan_task( + lambda: logger.info(DropSettings.get().text()), +) +app.register_lifespan_task( + log_info, + msg="MEx Drop is running, shut it down using CTRL+C", +) diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_api.py b/tests/api/test_main.py similarity index 100% rename from tests/test_api.py rename to tests/api/test_main.py diff --git a/tests/file_history/test_main.py b/tests/file_history/test_main.py index 10e96a2..53ff5a6 100644 --- a/tests/file_history/test_main.py +++ b/tests/file_history/test_main.py @@ -18,7 +18,7 @@ def upload_file(page: Page) -> None: def login(page: Page, get_test_key) -> None: page.get_by_placeholder("API key").fill(get_test_key("test")) - page.get_by_placeholder("X System").fill("test") + page.get_by_placeholder("X-System").fill("test") page.get_by_text("Log in").click() diff --git a/tests/file_history/test_state.py b/tests/file_history/test_state.py index dddf82f..fd08e9a 100644 --- a/tests/file_history/test_state.py +++ b/tests/file_history/test_state.py @@ -18,7 +18,7 @@ def setup_list_state(get_test_key) -> ListState: @patch("mex.drop.file_history.state.pathlib.Path") @patch("mex.drop.file_history.state.DropSettings.get") @patch("mex.drop.file_history.state.rx.toast.error") -def test_get_uploaded_files_missing_directory( +def test_refresh_missing_directory( mock_toast_error, mock_drop_settings_get, mock_path, setup_list_state ): """Test the case where the x-system directory does not exist.""" @@ -31,7 +31,7 @@ def test_get_uploaded_files_missing_directory( mock_x_system_dir = mock_path.return_value mock_x_system_dir.is_dir.return_value = False - state.get_uploaded_files() + state.refresh() mock_toast_error.assert_called_once_with( "The requested x-system was not found on this server.", close_button=True @@ -40,9 +40,7 @@ def test_get_uploaded_files_missing_directory( @patch("mex.drop.file_history.state.pathlib.Path") @patch("mex.drop.file_history.state.DropSettings.get") -def test_get_uploaded_files_success( - mock_drop_settings_get, mock_path, setup_list_state -): +def test_refresh_success(mock_drop_settings_get, mock_path, setup_list_state): """Test successful retrieval of uploaded files.""" state = setup_list_state @@ -61,7 +59,7 @@ def test_get_uploaded_files_success( mock_x_system_dir.glob.return_value = [mock_file] - state.get_uploaded_files() + state.refresh() expected_file_list = [ { diff --git a/tests/login/test_main.py b/tests/login/test_main.py index 7b75cc4..8da3751 100644 --- a/tests/login/test_main.py +++ b/tests/login/test_main.py @@ -6,7 +6,7 @@ def test_login_page(page: Page, get_test_key) -> None: page.goto("http://localhost:3000") page.get_by_placeholder("API Key").fill(get_test_key("test")) - page.get_by_placeholder("X System").fill("test") + page.get_by_placeholder("X-System").fill("test") page.screenshot(path="tests_test_login_test_login_success.jpeg") page.get_by_text("Log in").click() diff --git a/tests/login/test_state.py b/tests/login/test_state.py index c476ce2..4689278 100644 --- a/tests/login/test_state.py +++ b/tests/login/test_state.py @@ -2,13 +2,13 @@ from mex.drop.state import State -def test_login_user_and_logout(get_test_key) -> None: +def test_login_and_logout(get_test_key) -> None: login_state = LoginState( api_key=get_test_key("test_system"), x_system="test_system", parent_state=State(), ) - assert "/" in str(login_state.login_user()) + assert "/" in str(login_state.login()) assert login_state.user assert login_state.user.x_system == "test_system" assert login_state.user.api_key == get_test_key("test_system") @@ -17,9 +17,9 @@ def test_login_user_and_logout(get_test_key) -> None: assert login_state.user is None -def test_login_user_fail(get_test_key) -> None: +def test_login_fail(get_test_key) -> None: login_state = LoginState( api_key=get_test_key("test_system"), x_system="wrong_sys", parent_state=State() ) - login_state.login_user() + login_state.login() assert login_state.user is None diff --git a/tests/upload/test_main.py b/tests/upload/test_main.py index 6562820..6eda1f6 100644 --- a/tests/upload/test_main.py +++ b/tests/upload/test_main.py @@ -10,7 +10,7 @@ def login(page: Page, get_test_key) -> None: page.get_by_placeholder("API key").fill(get_test_key("test")) - page.get_by_placeholder("X System").fill("test") + page.get_by_placeholder("X-System").fill("test") page.get_by_text("Log in").click() From 5f8c9c8a842811c7b890832e6a402fed9c9d9d02 Mon Sep 17 00:00:00 2001 From: Nicolas Drebenstedt Date: Tue, 4 Feb 2025 14:39:26 +0100 Subject: [PATCH 3/4] cl and test path fixes --- CHANGELOG.md | 11 +++++++++++ tests/api/test_main.py | 3 +-- tests/conftest.py | 1 + tests/file_history/test_main.py | 4 +--- tests/upload/test_main.py | 3 +-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d2290..b1448db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,17 +11,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes +- update mex-common to version 0.49.3 +- BREAKING: you must start the local dev mode simply with `pdm run drop` (no 2nd run) +- move custom backend exception handler to its own module +- move custom api code to its own package `mex.drop.api` +- align general layout functions (page, logo, navbar, etc) with mex-editor +- align login component and navbar state handling with mex-editor +- update styling with more idiomatic variable syntax and responsive scaling + ### Deprecated ### Removed ### Fixed +- decorate state handlers with `@rx.event` to satisfy new reflex versions + ### Security ## [0.9.1] - 2025-01-09 ### Added + - mex-drop UI shows file list with name and timestamps ## [0.9.0] - 2024-12-05 diff --git a/tests/api/test_main.py b/tests/api/test_main.py index f577360..d988ba5 100644 --- a/tests/api/test_main.py +++ b/tests/api/test_main.py @@ -13,8 +13,7 @@ from mex.drop.files_io import ALLOWED_CONTENT_TYPES from mex.drop.settings import DropSettings from mex.drop.types import EntityType, XSystem - -TESTDATA_DIR = Path(__file__).parent / "test_files" +from tests.conftest import TESTDATA_DIR @pytest.fixture diff --git a/tests/conftest.py b/tests/conftest.py index b2bd43b..969dd51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from mex.mex import app pytest_plugins = ("mex.common.testing.plugin",) +TESTDATA_DIR = Path(__file__).parent / "test_files" TEST_USER_DATABASE = { diff --git a/tests/file_history/test_main.py b/tests/file_history/test_main.py index 53ff5a6..40abed3 100644 --- a/tests/file_history/test_main.py +++ b/tests/file_history/test_main.py @@ -1,9 +1,7 @@ -import pathlib - import pytest from playwright.sync_api import Page, expect -TESTDATA_DIR = pathlib.Path(__file__).parent.parent / "test_files" +from tests.conftest import TESTDATA_DIR def upload_file(page: Page) -> None: diff --git a/tests/upload/test_main.py b/tests/upload/test_main.py index 6eda1f6..37e51bd 100644 --- a/tests/upload/test_main.py +++ b/tests/upload/test_main.py @@ -4,8 +4,7 @@ from playwright.sync_api import Page, expect from mex.drop.settings import DropSettings - -TESTDATA_DIR = pathlib.Path(__file__).parent.parent / "test_files" +from tests.conftest import TESTDATA_DIR def login(page: Page, get_test_key) -> None: From c951b6d61cd03516b9c523601f4da9cf2097b2a4 Mon Sep 17 00:00:00 2001 From: Nicolas Drebenstedt Date: Tue, 4 Feb 2025 15:21:27 +0100 Subject: [PATCH 4/4] clean up and fix tests --- mex/drop/exceptions.py | 4 +-- mex/drop/layout.py | 6 ++-- mex/drop/login/main.py | 7 +++-- mex/drop/login/state.py | 3 +- mex/drop/upload/main.py | 10 +++--- mex/drop/upload/state.py | 4 +-- pyproject.toml | 2 +- tests/api/test_main.py | 2 +- tests/conftest.py | 7 +++++ tests/file_history/test_main.py | 4 +-- tests/file_history/test_state.py | 8 ++--- tests/login/test_main.py | 8 +++-- tests/upload/test_main.py | 4 +-- tests/upload/test_state.py | 52 +++++++++++++++----------------- 14 files changed, 65 insertions(+), 56 deletions(-) diff --git a/mex/drop/exceptions.py b/mex/drop/exceptions.py index 8a9bf75..3e6e58e 100644 --- a/mex/drop/exceptions.py +++ b/mex/drop/exceptions.py @@ -5,7 +5,7 @@ def custom_backend_handler(exception: Exception) -> EventSpec: """Custom backend exception handler.""" if str(exception) == "401: Missing authentication header X-API-Key.": - return rx.toast.error("Please enter your API key.") + return rx.toast.error("Please enter your API Key.") if str(exception) == "401: The provided API Key is not recognized.": - return rx.toast.error("Invalid API key.") + return rx.toast.error("Invalid API Key.") return rx.toast.error(f"Backend Error: {exception}") diff --git a/mex/drop/layout.py b/mex/drop/layout.py index b02b811..ea4611e 100644 --- a/mex/drop/layout.py +++ b/mex/drop/layout.py @@ -18,7 +18,10 @@ def user_button() -> rx.Component: def user_menu() -> rx.Component: """Return a user menu with a trigger, the current X-System and a logout button.""" return rx.menu.root( - rx.menu.trigger(user_button()), + rx.menu.trigger( + user_button(), + custom_attrs={"data-testid": "user-menu"}, + ), rx.menu.content( rx.menu.item(cast(User, State.user).x_system, disabled=True), rx.menu.separator(), @@ -27,7 +30,6 @@ def user_menu() -> rx.Component: on_select=State.logout, ), ), - custom_attrs={"data-testid": "user-menu"}, ) diff --git a/mex/drop/login/main.py b/mex/drop/login/main.py index e44ef32..b7058f9 100644 --- a/mex/drop/login/main.py +++ b/mex/drop/login/main.py @@ -23,10 +23,10 @@ def login_x_system() -> rx.Component: def login_api_key() -> rx.Component: """Return a form field for the API key.""" return rx.vstack( - rx.text("API key"), + rx.text("API Key"), rx.input( on_change=LoginState.set_api_key, - placeholder="API key", + placeholder="API Key", size="3", tab_index=2, type="password", @@ -39,7 +39,7 @@ def login_api_key() -> rx.Component: def login_button() -> rx.Component: """Return a submit button for the login form.""" return rx.button( - "Log in", + "Login", on_click=LoginState.login, size="3", tab_index=3, @@ -47,6 +47,7 @@ def login_button() -> rx.Component: "padding": "0 var(--space-6)", "marginTop": "var(--space-4)", }, + custom_attrs={"data-testid": "login-button"}, ) diff --git a/mex/drop/login/state.py b/mex/drop/login/state.py index 36a5d21..4b44d26 100644 --- a/mex/drop/login/state.py +++ b/mex/drop/login/state.py @@ -13,12 +13,13 @@ class LoginState(State): @rx.event def login(self) -> EventSpec: - """Log in the user.""" + """Login the user.""" authorized_x_systems = get_current_authorized_x_systems(api_key=self.api_key) if is_authorized(str(self.x_system), authorized_x_systems): self.user = User( api_key=self.api_key, x_system=self.x_system, ) + self.reset() # reset api_key/x_system return rx.redirect("/") return rx.window_alert("Invalid credentials.") diff --git a/mex/drop/upload/main.py b/mex/drop/upload/main.py index 0889459..22a5207 100644 --- a/mex/drop/upload/main.py +++ b/mex/drop/upload/main.py @@ -4,7 +4,7 @@ from mex.drop.files_io import ALLOWED_CONTENT_TYPES from mex.drop.layout import page -from mex.drop.upload.state import AppState, TempFile +from mex.drop.upload.state import TempFile, UploadState def uploaded_file_display() -> rx.Component: @@ -24,7 +24,7 @@ def uploaded_file_display() -> rx.Component: ), rx.table.body( rx.foreach( - AppState.temp_files, + UploadState.temp_files, create_file_row, ), ), @@ -52,7 +52,7 @@ def create_file_row(temp_file: TempFile) -> rx.Component: title="Remove file", color_scheme="tomato", variant="ghost", - on_click=lambda: cast(AppState, AppState).cancel_upload( + on_click=lambda: cast(UploadState, UploadState).cancel_upload( temp_file.title ), ), @@ -106,7 +106,7 @@ def create_drag_and_drop() -> rx.Component: "padding": "var(--space-4)", "margin": "var(--space-8) auto", }, - on_drop=cast(AppState, AppState).handle_upload( + on_drop=cast(UploadState, UploadState).handle_upload( rx.upload_files(upload_id="file_upload_area") ), ), @@ -128,7 +128,7 @@ def create_file_handling_card() -> rx.Component: rx.spacer(spacing="3"), rx.button( "Submit", - on_click=AppState.submit_data, + on_click=UploadState.submit_data, color_scheme="jade", ), style={"width": "100%"}, diff --git a/mex/drop/upload/state.py b/mex/drop/upload/state.py index 938e341..deea061 100644 --- a/mex/drop/upload/state.py +++ b/mex/drop/upload/state.py @@ -15,8 +15,8 @@ class TempFile(rx.Base): content: bytes -class AppState(State): - """The app state.""" +class UploadState(State): + """The state for the upload page.""" temp_files: list[TempFile] = [] diff --git a/pyproject.toml b/pyproject.toml index 4002124..da011ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ distribution = true update-all = { cmd = "pdm update --group :all --update-all --save-compatible" } lock-all = { cmd = "pdm lock --group :all --python='==3.11.*'" } install-lockfile = { cmd = "pdm install --group :all --frozen-lockfile" } -install-playwright = { cmd = "playwright install firefox" } +install-playwright = { cmd = "pdm run playwright install firefox" } install-all = { composite = ["install-lockfile", "install-playwright"] } export-all = { cmd = "pdm export --group :all --no-hashes -f requirements" } apidoc = { cmd = "pdm run sphinx-apidoc -f -o docs/source mex" } diff --git a/tests/api/test_main.py b/tests/api/test_main.py index d988ba5..5395dd8 100644 --- a/tests/api/test_main.py +++ b/tests/api/test_main.py @@ -187,7 +187,7 @@ def test_drop_data( # noqa: PLR0913 settings: DropSettings, ) -> None: mocked_sink = AsyncMock(return_value=None) - monkeypatch.setattr(mex.drop.api, "json_sink", mocked_sink) + monkeypatch.setattr(mex.drop.api.main, "json_sink", mocked_sink) if api_key: client.headers.update({"X-API-Key": api_key}) diff --git a/tests/conftest.py b/tests/conftest.py index 969dd51..4bddd5f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ from fastapi.testclient import TestClient from mex.drop.settings import DropSettings +from mex.drop.state import State, User from mex.drop.types import APIKey from mex.mex import app @@ -82,3 +83,9 @@ def _clean_test_directory() -> Path: return test_dir return _clean_test_directory + + +@pytest.fixture +def app_state(get_test_key) -> State: + """Fixture to set up a global state with a mock user.""" + return State(user=User(x_system="test_system", api_key=get_test_key("test_system"))) diff --git a/tests/file_history/test_main.py b/tests/file_history/test_main.py index 40abed3..4fda1be 100644 --- a/tests/file_history/test_main.py +++ b/tests/file_history/test_main.py @@ -15,9 +15,9 @@ def upload_file(page: Page) -> None: def login(page: Page, get_test_key) -> None: - page.get_by_placeholder("API key").fill(get_test_key("test")) + page.get_by_placeholder("API Key").fill(get_test_key("test")) page.get_by_placeholder("X-System").fill("test") - page.get_by_text("Log in").click() + page.get_by_test_id("login-button").click() @pytest.mark.integration diff --git a/tests/file_history/test_state.py b/tests/file_history/test_state.py index fd08e9a..3bfbcd7 100644 --- a/tests/file_history/test_state.py +++ b/tests/file_history/test_state.py @@ -4,15 +4,13 @@ import pytest from mex.drop.file_history.state import ListState -from mex.drop.state import User +from mex.drop.state import State @pytest.fixture -def setup_list_state(get_test_key) -> ListState: +def setup_list_state(app_state: State) -> ListState: """Fixture to set up ListState with a mock user.""" - return ListState( - user=User(x_system="test_system", api_key=get_test_key("test_system")) - ) + return ListState(parent_state=app_state) @patch("mex.drop.file_history.state.pathlib.Path") diff --git a/tests/login/test_main.py b/tests/login/test_main.py index 8da3751..a7d3ad7 100644 --- a/tests/login/test_main.py +++ b/tests/login/test_main.py @@ -9,9 +9,11 @@ def test_login_page(page: Page, get_test_key) -> None: page.get_by_placeholder("X-System").fill("test") page.screenshot(path="tests_test_login_test_login_success.jpeg") - page.get_by_text("Log in").click() - expect(page.locator("text=Logout")).to_be_visible() + page.get_by_test_id("login-button").click() + expect(page.get_by_test_id("nav-bar")).to_be_visible() page.screenshot(path="tests_test_login_test_login_success_after.jpeg") + page.get_by_test_id("user-menu").click() + expect(page.locator("text=Logout")).to_be_visible() page.get_by_text("Logout").click() - expect(page.locator("text=Log in")).to_be_visible() + expect(page.get_by_test_id("login-button")).to_be_visible() diff --git a/tests/upload/test_main.py b/tests/upload/test_main.py index 37e51bd..9519e46 100644 --- a/tests/upload/test_main.py +++ b/tests/upload/test_main.py @@ -8,9 +8,9 @@ def login(page: Page, get_test_key) -> None: - page.get_by_placeholder("API key").fill(get_test_key("test")) + page.get_by_placeholder("API Key").fill(get_test_key("test")) page.get_by_placeholder("X-System").fill("test") - page.get_by_text("Log in").click() + page.get_by_test_id("login-button").click() @pytest.mark.integration diff --git a/tests/upload/test_state.py b/tests/upload/test_state.py index 11ea3db..cdcf806 100644 --- a/tests/upload/test_state.py +++ b/tests/upload/test_state.py @@ -4,8 +4,8 @@ import nest_asyncio import pytest -from mex.drop.state import User -from mex.drop.upload.state import AppState, TempFile +from mex.drop.state import State +from mex.drop.upload.state import TempFile, UploadState nest_asyncio.apply() @@ -16,21 +16,22 @@ def anyio_backend(): @pytest.fixture -def app_state() -> AppState: - return AppState() +def upload_state(app_state: State) -> UploadState: + """Fixture to set up UploadState with a mock user.""" + return UploadState(parent_state=app_state) -def test_cancel_upload(app_state: AppState) -> None: - app_state.temp_files = [MagicMock(title="file1"), MagicMock(title="file2")] +def test_cancel_upload(upload_state: UploadState) -> None: + upload_state.temp_files = [MagicMock(title="file1"), MagicMock(title="file2")] - app_state.cancel_upload("file1") + upload_state.cancel_upload("file1") - assert len(app_state.temp_files) == 1 - assert app_state.temp_files[0].title == "file2" + assert len(upload_state.temp_files) == 1 + assert upload_state.temp_files[0].title == "file2" @pytest.mark.anyio -async def test_handle_upload(app_state: AppState) -> None: +async def test_handle_upload(upload_state: UploadState) -> None: file1 = MagicMock() file1.filename = "file1.csv" file1.content_type = "text/csv" @@ -41,35 +42,32 @@ async def test_handle_upload(app_state: AppState) -> None: file2.content_type = "application/xml" file2.read = AsyncMock(return_value=b"content2") - await app_state.handle_upload([file1, file2]) + await upload_state.handle_upload([file1, file2]) - assert len(app_state.temp_files) == 2 - assert app_state.temp_files[0].title == "file1.csv" - assert app_state.temp_files[0].content == b"content1" - assert app_state.temp_files[1].title == "file2.xml" - assert app_state.temp_files[1].content == b"content2" + assert len(upload_state.temp_files) == 2 + assert upload_state.temp_files[0].title == "file1.csv" + assert upload_state.temp_files[0].content == b"content1" + assert upload_state.temp_files[1].title == "file2.xml" + assert upload_state.temp_files[1].content == b"content2" @pytest.mark.anyio -async def test_handle_upload_duplicate(app_state: AppState) -> None: +async def test_handle_upload_duplicate(upload_state: UploadState) -> None: file1 = MagicMock() file1.filename = "file1.xml" file1.content_type = "application/xml" file1.read = AsyncMock(return_value=b"content1") - app_state.temp_files.append(TempFile(title="file1.xml", content=b"content1")) + upload_state.temp_files.append(TempFile(title="file1.xml", content=b"content1")) - await app_state.handle_upload([file1]) + await upload_state.handle_upload([file1]) - assert len(app_state.temp_files) == 1 + assert len(upload_state.temp_files) == 1 @pytest.mark.anyio -async def test_submit_data(get_test_key) -> None: - app_state = AppState( - user=User(x_system="test_system", api_key=get_test_key("test_system")) - ) - app_state.temp_files = [TempFile(title="file1.xml", content=b"content1")] +async def test_submit_data(upload_state: UploadState) -> None: + upload_state.temp_files = [TempFile(title="file1.xml", content=b"content1")] with ( patch( @@ -81,10 +79,10 @@ async def test_submit_data(get_test_key) -> None: ), patch("reflex.toast.success") as mock_toast_success, ): - result = await app_state.submit_data() + result = await upload_state.submit_data() mock_write_to_file.assert_called_once_with( b"content1", pathlib.Path("/mock/path/test_system/file1.xml") ) - assert len(app_state.temp_files) == 0 + assert len(upload_state.temp_files) == 0 mock_toast_success.assert_called_once_with("File upload successful!") assert result == mock_toast_success.return_value