diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py index d56da5f43f4f..2a7f0487871a 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py @@ -2,14 +2,17 @@ from fastapi import FastAPI from ...core.settings import ApplicationSettings +from . import routes_external_scheduler, routes_internal_scheduler from ._utils import set_parent_app -from .routes import router def initialize_frontend(app: FastAPI) -> None: settings: ApplicationSettings = app.state.settings - nicegui.app.include_router(router) + if settings.DYNAMIC_SCHEDULER_USE_INTERNAL_SCHEDULER: + nicegui.app.include_router(routes_internal_scheduler.router) + else: + nicegui.app.include_router(routes_external_scheduler.router) nicegui.ui.run_with( app, diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/__init__.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/__init__.py similarity index 100% rename from services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/__init__.py rename to services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/__init__.py diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_index.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_index.py similarity index 97% rename from services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_index.py rename to services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_index.py index b6f7d5b1c919..99b6f3eecf74 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_index.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_index.py @@ -1,3 +1,4 @@ +import arrow import httpx from common_library.json_serialization import json_dumps, json_loads from fastapi import FastAPI @@ -10,7 +11,7 @@ from ....services.service_tracker import TrackedServiceModel, get_all_tracked_services from ....services.service_tracker._models import SchedulerServiceState from .._utils import get_parent_app, get_settings -from ._render_utils import base_page, get_iso_formatted_date +from ._render_utils import base_page router = APIRouter() @@ -21,7 +22,7 @@ def _render_service_details(node_id: NodeID, service: TrackedServiceModel) -> No "Display State": ("label", service.current_state), "Last State Change": ( "label", - get_iso_formatted_date(service.last_state_change), + arrow.get(service.last_state_change).isoformat(), ), "UserID": ("copy", f"{service.user_id}"), "ProjectID": ("copy", f"{service.project_id}"), diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_render_utils.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_render_utils.py similarity index 81% rename from services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_render_utils.py rename to services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_render_utils.py index c3a315be2d70..d34191d21f48 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_render_utils.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_render_utils.py @@ -1,7 +1,6 @@ from collections.abc import Iterator from contextlib import contextmanager -import arrow from nicegui import ui @@ -17,7 +16,3 @@ def base_page(*, title: str | None = None) -> Iterator[None]: ui.label(display_title) yield None - - -def get_iso_formatted_date(timestamp: float) -> str: - return arrow.get(timestamp).isoformat() diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_service.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_service.py similarity index 98% rename from services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_service.py rename to services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_service.py index ac073072a44a..c0a734f5baba 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_service.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_external_scheduler/_service.py @@ -9,9 +9,9 @@ stop_dynamic_service, ) from settings_library.utils_service import DEFAULT_FASTAPI_PORT -from simcore_service_dynamic_scheduler.services.rabbitmq import get_rabbitmq_rpc_client from ....core.settings import ApplicationSettings +from ....services.rabbitmq import get_rabbitmq_rpc_client from ....services.service_tracker import get_tracked_service, remove_tracked_service from .._utils import get_parent_app, get_settings from ._render_utils import base_page diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_internal_scheduler/__init__.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_internal_scheduler/__init__.py new file mode 100644 index 000000000000..9b40dcf6ddd2 --- /dev/null +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_internal_scheduler/__init__.py @@ -0,0 +1,9 @@ +from nicegui import APIRouter + +from . import _index + +router = APIRouter() + +router.include_router(_index.router) + +__all__: tuple[str, ...] = ("router",) diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_internal_scheduler/_index.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_internal_scheduler/_index.py new file mode 100644 index 000000000000..7c16cdbc7817 --- /dev/null +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes_internal_scheduler/_index.py @@ -0,0 +1,8 @@ +from nicegui import APIRouter, ui + +router = APIRouter() + + +@router.page("/") +async def index(): + ui.label("PLACEHOLDER for internal scheduler UI") diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/_routes_internal_scheduler/conftest.py b/services/dynamic-scheduler/tests/unit/api_frontend/_routes_internal_scheduler/conftest.py new file mode 100644 index 000000000000..cf8b4f75d70e --- /dev/null +++ b/services/dynamic-scheduler/tests/unit/api_frontend/_routes_internal_scheduler/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture +def use_internal_scheduler() -> bool: + return True diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/_routes_internal_scheduler/test__index_.py b/services/dynamic-scheduler/tests/unit/api_frontend/_routes_internal_scheduler/test__index_.py new file mode 100644 index 000000000000..b919e371ba1c --- /dev/null +++ b/services/dynamic-scheduler/tests/unit/api_frontend/_routes_internal_scheduler/test__index_.py @@ -0,0 +1,26 @@ +# pylint:disable=redefined-outer-name +# pylint:disable=unused-argument + +from helpers import assert_contains_text +from playwright.async_api import Page +from simcore_service_dynamic_scheduler.api.frontend._utils import get_settings + +pytest_simcore_core_services_selection = [ + "postgres", + "rabbit", + "redis", +] + +pytest_simcore_ops_services_selection = [ + "redis-commander", +] + + +async def test_placeholder_index( + app_runner: None, async_page: Page, server_host_port: str +): + await async_page.goto( + f"{server_host_port}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}" + ) + + await assert_contains_text(async_page, "PLACEHOLDER for internal scheduler UI") diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/conftest.py b/services/dynamic-scheduler/tests/unit/api_frontend/conftest.py index 663091247d10..3aef679dfa3f 100644 --- a/services/dynamic-scheduler/tests/unit/api_frontend/conftest.py +++ b/services/dynamic-scheduler/tests/unit/api_frontend/conftest.py @@ -5,10 +5,7 @@ import subprocess from collections.abc import AsyncIterable from contextlib import suppress -from typing import Final -from unittest.mock import AsyncMock -import nicegui import pytest import sqlalchemy as sa from fastapi import FastAPI, status @@ -17,6 +14,7 @@ from hypercorn.config import Config from playwright.async_api import Page, async_playwright from pytest_mock import MockerFixture +from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.postgres_tools import PostgresTestConfig from pytest_simcore.helpers.typing_env import EnvVarsDict from settings_library.rabbit import RabbitSettings @@ -26,37 +24,24 @@ from simcore_service_dynamic_scheduler.core.settings import ApplicationSettings from tenacity import AsyncRetrying, stop_after_delay, wait_fixed -_MODULE: Final["str"] = "simcore_service_dynamic_scheduler" - @pytest.fixture def disable_status_monitor_background_task(mocker: MockerFixture) -> None: mocker.patch( - f"{_MODULE}.services.status_monitor._monitor.Monitor._worker_check_services_require_status_update" + "simcore_service_dynamic_scheduler.services.status_monitor._monitor.Monitor._worker_check_services_require_status_update" ) @pytest.fixture -def mock_stop_dynamic_service(mocker: MockerFixture) -> AsyncMock: - async_mock = AsyncMock() - mocker.patch( - f"{_MODULE}.api.frontend.routes._service.stop_dynamic_service", async_mock - ) - return async_mock - - -@pytest.fixture -def mock_remove_tracked_service(mocker: MockerFixture) -> AsyncMock: - async_mock = AsyncMock() - mocker.patch( - f"{_MODULE}.api.frontend.routes._service.remove_tracked_service", async_mock - ) - return async_mock +def use_internal_scheduler() -> bool: + pytest.fail("please define use_internal_scheduler fixture in your tests folder") @pytest.fixture def app_environment( + monkeypatch: pytest.MonkeyPatch, app_environment: EnvVarsDict, + use_internal_scheduler: bool, postgres_db: sa.engine.Engine, postgres_host_config: PostgresTestConfig, disable_status_monitor_background_task: None, @@ -64,7 +49,11 @@ def app_environment( redis_service: RedisSettings, remove_redis_data: None, ) -> EnvVarsDict: - return app_environment + to_set = { + "DYNAMIC_SCHEDULER_USE_INTERNAL_SCHEDULER": f"{use_internal_scheduler}", + } + setenvs_from_dict(monkeypatch, to_set) + return {**app_environment, **to_set} @pytest.fixture @@ -72,11 +61,39 @@ def server_host_port() -> str: return f"127.0.0.1:{DEFAULT_FASTAPI_PORT}" +def _reset_nicegui_app() -> None: + # forces rebuild of middleware stack on next test + + # below is based on nicegui.testing.general_fixtures.nicegui_reset_globals + + from nicegui import Client, app + from starlette.routing import Route + + for route in list(app.routes): + if isinstance(route, Route) and route.path.startswith("/_nicegui/auto/static/"): + app.remove_route(route.path) + + all_page_routes = set(Client.page_routes.values()) + all_page_routes.add("/") + for path in all_page_routes: + app.remove_route(path) + + for route in list(app.routes): + if ( + isinstance(route, Route) + and "{" in route.path + and "}" in route.path + and not route.path.startswith("/_nicegui/") + ): + app.remove_route(route.path) + + app.middleware_stack = None + app.user_middleware.clear() + + @pytest.fixture def not_initialized_app(app_environment: EnvVarsDict) -> FastAPI: - # forces rebuild of middleware stack on next test - nicegui.app.user_middleware.clear() - nicegui.app.middleware_stack = None + _reset_nicegui_app() return create_app() diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/helpers.py b/services/dynamic-scheduler/tests/unit/api_frontend/helpers.py index 53c89ac9a865..79cf9ad27ebc 100644 --- a/services/dynamic-scheduler/tests/unit/api_frontend/helpers.py +++ b/services/dynamic-scheduler/tests/unit/api_frontend/helpers.py @@ -98,6 +98,5 @@ def get_new_style_service_status(state: str) -> DynamicServiceGet: def get_legacy_service_status(state: str) -> NodeGet: return TypeAdapter(NodeGet).validate_python( - NodeGet.model_config["json_schema_extra"]["examples"][0] - | {"service_state": state} + NodeGet.model_json_schema()["examples"][0] | {"service_state": state} ) diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/conftest.py b/services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/conftest.py new file mode 100644 index 000000000000..233f9a5afec8 --- /dev/null +++ b/services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/conftest.py @@ -0,0 +1,30 @@ +# pylint:disable=redefined-outer-name + +from typing import Final +from unittest.mock import AsyncMock + +import pytest +from pytest_mock import MockerFixture + +_MODULE: Final["str"] = ( + "simcore_service_dynamic_scheduler.api.frontend.routes_external_scheduler._service" +) + + +@pytest.fixture +def use_internal_scheduler() -> bool: + return False + + +@pytest.fixture +def mock_stop_dynamic_service(mocker: MockerFixture) -> AsyncMock: + async_mock = AsyncMock() + mocker.patch(f"{_MODULE}.stop_dynamic_service", async_mock) + return async_mock + + +@pytest.fixture +def mock_remove_tracked_service(mocker: MockerFixture) -> AsyncMock: + async_mock = AsyncMock() + mocker.patch(f"{_MODULE}.remove_tracked_service", async_mock) + return async_mock diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_index.py b/services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/test__index.py similarity index 99% rename from services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_index.py rename to services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/test__index.py index 8ba68fbe632d..d59d1468b5f4 100644 --- a/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_index.py +++ b/services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/test__index.py @@ -38,7 +38,7 @@ ] pytest_simcore_ops_services_selection = [ - # "redis-commander", + "redis-commander", ] diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_service.py b/services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/test__service.py similarity index 99% rename from services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_service.py rename to services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/test__service.py index a4f0c3993d02..52524e7ad6da 100644 --- a/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_service.py +++ b/services/dynamic-scheduler/tests/unit/api_frontend/routes_external_scheduler/test__service.py @@ -34,7 +34,7 @@ ] pytest_simcore_ops_services_selection = [ - # "redis-commander", + "redis-commander", ]