From d030306067225b313d53f40423280ab74f97ba7c Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Mon, 20 Nov 2023 15:09:35 -0500 Subject: [PATCH 01/42] export `covalent_start` and `covalent_stop` --- covalent_dispatcher/__init__.py | 1 + covalent_dispatcher/programmatic.py | 76 +++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 covalent_dispatcher/programmatic.py diff --git a/covalent_dispatcher/__init__.py b/covalent_dispatcher/__init__.py index 0ad60669e..ad75ca249 100644 --- a/covalent_dispatcher/__init__.py +++ b/covalent_dispatcher/__init__.py @@ -15,3 +15,4 @@ # limitations under the License. from .entry_point import cancel_running_dispatch, run_dispatcher +from .programmatic import covalent_is_running, covalent_start, covalent_stop diff --git a/covalent_dispatcher/programmatic.py b/covalent_dispatcher/programmatic.py new file mode 100644 index 000000000..8b8f66e88 --- /dev/null +++ b/covalent_dispatcher/programmatic.py @@ -0,0 +1,76 @@ +"""Functions providing programmatic access to Covalent CLI commands.""" +import time +from typing import Any, Dict, Optional + +import click +import psutil + +from covalent import get_config +from covalent._shared_files import logger + +from ._cli.service import _read_pid, start, stop + +app_log = logger.app_log + + +def _call_cli_command(func: click.Command, **kwargs: Dict[str, Any]) -> Any: + """Call the CLI command ``func`` with the specified kwargs.""" + ctx = click.Context(func) + ctx.invoke(func, **kwargs) + + +def covalent_is_running() -> bool: + """Return True if the Covalent server is in a ready state.""" + pid = _read_pid(get_config("dispatcher.cache_dir") + "/ui.pid") + return ( + psutil.pid_exists(pid) + and pid != -1 + and get_config("dispatcher.address") + and get_config("dispatcher.port") + ) + + +def covalent_start( + develop: bool = False, + port: Optional[str] = None, + mem_per_worker: Optional[str] = None, + workers: Optional[int] = None, + threads_per_worker: Optional[int] = None, + ignore_migrations: bool = False, + no_cluster: bool = False, + no_triggers: bool = False, + triggers_only: bool = False, +): + """ + Start the Covalent server. Wrapper for the `covalent start` CLI command. + """ + if covalent_is_running(): + return + + kwargs = { + "develop": develop, + "port": port or get_config("dispatcher.port"), + "mem_per_worker": mem_per_worker or get_config("dask.mem_per_worker"), + "workers": workers or get_config("dask.num_workers"), + "threads_per_worker": threads_per_worker or get_config("dask.threads_per_worker"), + "ignore_migrations": ignore_migrations, + "no_cluster": no_cluster, + "no_triggers": no_triggers, + "triggers_only": triggers_only, + } + + _call_cli_command(start, **kwargs) + + while not covalent_is_running(): + app_log.debug("Waiting for Covalent Server to be to dispatch-ready...") + time.sleep(1) + + +def covalent_stop(): + """ + Stop the Covalent server. Wrapper for the `covalent stop` CLI command. + """ + if not covalent_is_running(): + return + + _call_cli_command(stop) From 290481f45d928ec1de199932e6c94df3892021d6 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Mon, 20 Nov 2023 15:13:25 -0500 Subject: [PATCH 02/42] check server stopped --- covalent_dispatcher/programmatic.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/covalent_dispatcher/programmatic.py b/covalent_dispatcher/programmatic.py index 8b8f66e88..ca80d0f38 100644 --- a/covalent_dispatcher/programmatic.py +++ b/covalent_dispatcher/programmatic.py @@ -74,3 +74,7 @@ def covalent_stop(): return _call_cli_command(stop) + + while covalent_is_running(): + app_log.debug("Waiting for Covalent Server to stop...") + time.sleep(1) From fab67cd5ce10ec9a9ee3a75960bb9321ad2891bc Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Mon, 20 Nov 2023 15:17:14 -0500 Subject: [PATCH 03/42] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d9959a1..736c48f6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] +### Added + +- Programmatic equivalents of CLI commands `covalent start` and `covalent stop` + ### Fixed - Lattice-default metadata attributes are now applied correctly From 46b395c8600281524de8dc00bed3602394a01af8 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Mon, 20 Nov 2023 15:40:55 -0500 Subject: [PATCH 04/42] move commands to main namespace --- covalent/__init__.py | 1 + covalent/_programmatic/__init__.py | 15 +++++++++++++ .../_programmatic/commands.py | 22 ++++++++++++++++--- covalent_dispatcher/__init__.py | 1 - 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 covalent/_programmatic/__init__.py rename covalent_dispatcher/programmatic.py => covalent/_programmatic/commands.py (73%) diff --git a/covalent/__init__.py b/covalent/__init__.py index aed63a724..4208aded9 100644 --- a/covalent/__init__.py +++ b/covalent/__init__.py @@ -25,6 +25,7 @@ from ._dispatcher_plugins import local_redispatch as redispatch # nopycln: import from ._dispatcher_plugins import stop_triggers # nopycln: import from ._file_transfer import strategies as fs_strategies # nopycln: import +from ._programmatic.commands import covalent_is_running, covalent_start, covalent_stop from ._results_manager.results_manager import ( # nopycln: import cancel, get_result, diff --git a/covalent/_programmatic/__init__.py b/covalent/_programmatic/__init__.py new file mode 100644 index 000000000..cfc23bfdf --- /dev/null +++ b/covalent/_programmatic/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the Apache License 2.0 (the "License"). A copy of the +# License may be obtained with this software package or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Use of this file is prohibited except in compliance with the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/covalent_dispatcher/programmatic.py b/covalent/_programmatic/commands.py similarity index 73% rename from covalent_dispatcher/programmatic.py rename to covalent/_programmatic/commands.py index ca80d0f38..ac81f9c2d 100644 --- a/covalent_dispatcher/programmatic.py +++ b/covalent/_programmatic/commands.py @@ -1,3 +1,19 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the Apache License 2.0 (the "License"). A copy of the +# License may be obtained with this software package or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Use of this file is prohibited except in compliance with the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Functions providing programmatic access to Covalent CLI commands.""" import time from typing import Any, Dict, Optional @@ -5,10 +21,10 @@ import click import psutil -from covalent import get_config -from covalent._shared_files import logger +from covalent_dispatcher._cli.service import _read_pid, start, stop -from ._cli.service import _read_pid, start, stop +from .._shared_files import logger +from .._shared_files.config import get_config app_log = logger.app_log diff --git a/covalent_dispatcher/__init__.py b/covalent_dispatcher/__init__.py index ad75ca249..0ad60669e 100644 --- a/covalent_dispatcher/__init__.py +++ b/covalent_dispatcher/__init__.py @@ -15,4 +15,3 @@ # limitations under the License. from .entry_point import cancel_running_dispatch, run_dispatcher -from .programmatic import covalent_is_running, covalent_start, covalent_stop From f706575e908400bd3c07c6bf0d2b324de9f98c4c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:41:42 +0000 Subject: [PATCH 05/42] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- covalent/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/covalent/__init__.py b/covalent/__init__.py index 4208aded9..aed63a724 100644 --- a/covalent/__init__.py +++ b/covalent/__init__.py @@ -25,7 +25,6 @@ from ._dispatcher_plugins import local_redispatch as redispatch # nopycln: import from ._dispatcher_plugins import stop_triggers # nopycln: import from ._file_transfer import strategies as fs_strategies # nopycln: import -from ._programmatic.commands import covalent_is_running, covalent_start, covalent_stop from ._results_manager.results_manager import ( # nopycln: import cancel, get_result, From afb9aad84ef28b1e5f84b71abc0b32f745856b46 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 09:05:59 -0500 Subject: [PATCH 06/42] improve docstrings --- covalent/_programmatic/commands.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index ac81f9c2d..186825fcc 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -56,9 +56,19 @@ def covalent_start( no_cluster: bool = False, no_triggers: bool = False, triggers_only: bool = False, -): - """ - Start the Covalent server. Wrapper for the `covalent start` CLI command. +) -> None: + """Start the Covalent server. Wrapper for the `covalent start` CLI command. + + Args: + develop: Start local server in develop mode. Defaults to False. + port: Local server port number. Defaults to 48008. + mem_per_worker: Memory limit per worker in GB. Defaults to auto. + workers: Number of Dask workers. Defaults to 8. + threads_per_worker: Number of threads per Dask worker. Defaults to 1. + ignore_migrations: Start server without database migrations. Defaults to False. + no_cluster: Start server without Dask cluster. Defaults to False. + no_triggers: Start server without a triggers server. Defaults to False. + triggers_only: Start only the triggers server. Defaults to False. """ if covalent_is_running(): return @@ -82,10 +92,8 @@ def covalent_start( time.sleep(1) -def covalent_stop(): - """ - Stop the Covalent server. Wrapper for the `covalent stop` CLI command. - """ +def covalent_stop() -> None: + """Stop the Covalent server. Wrapper for the `covalent stop` CLI command.""" if not covalent_is_running(): return From 9eba72abab1ea1516d43908ef593ef9534d927d8 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 09:08:17 -0500 Subject: [PATCH 07/42] fix `covalent_is_running` to return bool --- covalent/_programmatic/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 186825fcc..e811ac222 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -41,8 +41,8 @@ def covalent_is_running() -> bool: return ( psutil.pid_exists(pid) and pid != -1 - and get_config("dispatcher.address") - and get_config("dispatcher.port") + and get_config("dispatcher.address") != '' + and get_config("dispatcher.port") != '' ) From a1004e3942f5767546337b8b54a9b6e5ebfdbc70 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 09:10:05 -0500 Subject: [PATCH 08/42] reorder `covalent_is_running` conditions --- covalent/_programmatic/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index e811ac222..a7c07766e 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -39,8 +39,8 @@ def covalent_is_running() -> bool: """Return True if the Covalent server is in a ready state.""" pid = _read_pid(get_config("dispatcher.cache_dir") + "/ui.pid") return ( - psutil.pid_exists(pid) - and pid != -1 + pid != -1 + and psutil.pid_exists(pid) and get_config("dispatcher.address") != '' and get_config("dispatcher.port") != '' ) From 8622ac30c24408b00457dd57f4b22aac9ea9ca48 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 09:31:25 -0500 Subject: [PATCH 09/42] `quiet` mode to suppress stdout; more docstrings --- covalent/_programmatic/commands.py | 38 +++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index a7c07766e..ae4a3c1cb 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -15,6 +15,8 @@ # limitations under the License. """Functions providing programmatic access to Covalent CLI commands.""" +import contextlib +import sys import time from typing import Any, Dict, Optional @@ -29,10 +31,21 @@ app_log = logger.app_log -def _call_cli_command(func: click.Command, **kwargs: Dict[str, Any]) -> Any: - """Call the CLI command ``func`` with the specified kwargs.""" - ctx = click.Context(func) - ctx.invoke(func, **kwargs) +def _call_cli_command( + cmd: click.Command, + *, + quiet: bool = False, + **kwargs: Dict[str, Any] +) -> None: + """Call a CLI command with the specified kwargs. + + Args: + func: The CLI command to call. + quiet: Suppress stdout. Defaults to False. + """ + with contextlib.redirect_stdout(None if quiet else sys.stdout): + ctx = click.Context(cmd) + ctx.invoke(cmd, **kwargs) def covalent_is_running() -> bool: @@ -56,8 +69,11 @@ def covalent_start( no_cluster: bool = False, no_triggers: bool = False, triggers_only: bool = False, + *, + quiet: bool = False, ) -> None: """Start the Covalent server. Wrapper for the `covalent start` CLI command. + This function returns immediately if the local Covalent server is already running. Args: develop: Start local server in develop mode. Defaults to False. @@ -69,6 +85,7 @@ def covalent_start( no_cluster: Start server without Dask cluster. Defaults to False. no_triggers: Start server without a triggers server. Defaults to False. triggers_only: Start only the triggers server. Defaults to False. + quiet: Suppress stdout. Defaults to False. """ if covalent_is_running(): return @@ -85,19 +102,24 @@ def covalent_start( "triggers_only": triggers_only, } - _call_cli_command(start, **kwargs) + _call_cli_command(start, quiet=quiet, **kwargs) while not covalent_is_running(): app_log.debug("Waiting for Covalent Server to be to dispatch-ready...") time.sleep(1) -def covalent_stop() -> None: - """Stop the Covalent server. Wrapper for the `covalent stop` CLI command.""" +def covalent_stop(*, quiet: bool = False) -> None: + """Stop the Covalent server. Wrapper for the `covalent stop` CLI command. + This function returns immediately if the local Covalent server is not running. + + Args: + quiet: Suppress stdout. Defaults to False. + """ if not covalent_is_running(): return - _call_cli_command(stop) + _call_cli_command(stop, quiet=quiet) while covalent_is_running(): app_log.debug("Waiting for Covalent Server to stop...") From 7e4117b9cddf76241888d6f6bb4f591f2552d1b3 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 10:43:07 -0500 Subject: [PATCH 10/42] use poll function instead of while loop --- covalent/_programmatic/commands.py | 72 +++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index ae4a3c1cb..020bcec71 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -18,7 +18,7 @@ import contextlib import sys import time -from typing import Any, Dict, Optional +from typing import Any, Callable, Dict, Optional import click import psutil @@ -31,6 +31,17 @@ app_log = logger.app_log +def covalent_is_running() -> bool: + """Return True if the Covalent server is in a ready state.""" + pid = _read_pid(get_config("dispatcher.cache_dir") + "/ui.pid") + return ( + pid != -1 + and psutil.pid_exists(pid) + and get_config("dispatcher.address") != '' + and get_config("dispatcher.port") != '' + ) + + def _call_cli_command( cmd: click.Command, *, @@ -48,15 +59,34 @@ def _call_cli_command( ctx.invoke(cmd, **kwargs) -def covalent_is_running() -> bool: - """Return True if the Covalent server is in a ready state.""" - pid = _read_pid(get_config("dispatcher.cache_dir") + "/ui.pid") - return ( - pid != -1 - and psutil.pid_exists(pid) - and get_config("dispatcher.address") != '' - and get_config("dispatcher.port") != '' - ) +def _poll_with_timeout( + callable_: Callable[[], bool], + *, + waiting_msg: str, + timeout_msg: str, + timeout: int, +) -> None: + """Poll a callable once per second, until it returns True or a timeout is reached. + + Args: + callable_: The callable to poll. + waiting_msg: Log message to display while waiting. + timeout_msg: Error message to display if timeout is reached. + timeout: Timeout in seconds. + + Raises: + TimeoutError: _description_ + """ + _num_wait = 0 + _max_wait = timeout + + while not callable_(): + app_log.debug(waiting_msg) + + time.sleep(1) + _num_wait += 1 + if _num_wait >= _max_wait: + raise TimeoutError(timeout_msg) def covalent_start( @@ -102,11 +132,16 @@ def covalent_start( "triggers_only": triggers_only, } + # Run the `covalent start [OPTIONS]` command. _call_cli_command(start, quiet=quiet, **kwargs) - while not covalent_is_running(): - app_log.debug("Waiting for Covalent Server to be to dispatch-ready...") - time.sleep(1) + # Wait to confirm Covalent server is running. + _poll_with_timeout( + covalent_is_running, + waiting_msg="Waiting for Covalent Server to start...", + timeout_msg="Covalent Server failed to start!", + timeout=10, + ) def covalent_stop(*, quiet: bool = False) -> None: @@ -119,8 +154,13 @@ def covalent_stop(*, quiet: bool = False) -> None: if not covalent_is_running(): return + # Run the `covalent stop` command. _call_cli_command(stop, quiet=quiet) - while covalent_is_running(): - app_log.debug("Waiting for Covalent Server to stop...") - time.sleep(1) + # Wait to confirm Covalent server is stopped. + _poll_with_timeout( + lambda: not covalent_is_running(), + waiting_msg="Waiting for Covalent server to stop...", + timeout_msg="Failed to stop Covalent server!", + timeout=10, + ) From 58a48b23bc7aa813eefea13850dd99d0088197ee Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 10:55:24 -0500 Subject: [PATCH 11/42] explain package --- covalent/_programmatic/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/covalent/_programmatic/__init__.py b/covalent/_programmatic/__init__.py index cfc23bfdf..fa08678aa 100644 --- a/covalent/_programmatic/__init__.py +++ b/covalent/_programmatic/__init__.py @@ -13,3 +13,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +""" +NOTE: This package exists to avoid circular imports that would be encountered if +`covalent` imports from `covalent_dispatcher._cli`. +""" From bd367cb1db8117596f53c66642973c06da5bd5e8 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 12:42:22 -0500 Subject: [PATCH 12/42] add api docs entry --- doc/source/api/api.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/source/api/api.rst b/doc/source/api/api.rst index 2a603c728..d4841f52f 100644 --- a/doc/source/api/api.rst +++ b/doc/source/api/api.rst @@ -6,6 +6,7 @@ Covalent API The following API documentation describes how to use Covalent. +- The :ref:`covalent_server` manages workflow dispatch, orchestration, and metadata - :ref:`electrons_api` and :ref:`lattices_api` are used for constructing workflows - :ref:`qelectrons_api` are used to customize and track quantum circuit execution - :ref:`qclusters_api` are used to distribute Quantum Electrons across multiple quantum backends. @@ -22,6 +23,27 @@ The following API documentation describes how to use Covalent. - :ref:`dispatcher_interface` is used for dispatching workflows and stopping triggered dispatches - The :ref:`dispatcher_server_api` is used for interfacing with the Covalent server +.. _starting_covalent: + +Covalent Server +""""""""""""""""""""""""""" +Either a local or self-hosted Covalent server must be running in order to dispatch workflows. The covalent CLI provides various utilities for starting, stopping, and managing a Covalent server. For more information, see: + +.. code-block:: bash + + covalent --help + +The Covalent SDK also includes a Python interface for starting and stopping the covalent server. + +.. autofunction:: covalent._programmatic.commands.covalent_is_running + +.. autofunction:: covalent._programmatic.commands.covalent_start + +.. autofunction:: covalent._programmatic.commands.covalent_stop + + +---------------------------------------------------------------- + .. _electrons_api: Electron From c784193a23bc6c8f982c7c199402cf18b6750414 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 12:59:17 -0500 Subject: [PATCH 13/42] update api docs --- doc/source/api/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/api/api.rst b/doc/source/api/api.rst index d4841f52f..7b30b5de6 100644 --- a/doc/source/api/api.rst +++ b/doc/source/api/api.rst @@ -23,11 +23,11 @@ The following API documentation describes how to use Covalent. - :ref:`dispatcher_interface` is used for dispatching workflows and stopping triggered dispatches - The :ref:`dispatcher_server_api` is used for interfacing with the Covalent server -.. _starting_covalent: +.. _covalent_server: Covalent Server """"""""""""""""""""""""""" -Either a local or self-hosted Covalent server must be running in order to dispatch workflows. The covalent CLI provides various utilities for starting, stopping, and managing a Covalent server. For more information, see: +A Covalent server must be running in order to dispatch workflows. The Covalent CLI provides various utilities for starting, stopping, and managing a Covalent server. For more information, see: .. code-block:: bash From e430db57e350b53d8c327e28edf48597219ea087 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 13:03:15 -0500 Subject: [PATCH 14/42] restore import from `._programmatic` --- covalent/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/covalent/__init__.py b/covalent/__init__.py index aed63a724..4208aded9 100644 --- a/covalent/__init__.py +++ b/covalent/__init__.py @@ -25,6 +25,7 @@ from ._dispatcher_plugins import local_redispatch as redispatch # nopycln: import from ._dispatcher_plugins import stop_triggers # nopycln: import from ._file_transfer import strategies as fs_strategies # nopycln: import +from ._programmatic.commands import covalent_is_running, covalent_start, covalent_stop from ._results_manager.results_manager import ( # nopycln: import cancel, get_result, From 10cd66e177929de1bde346abe1cf818abceb7e8e Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 13:20:05 -0500 Subject: [PATCH 15/42] update api docs --- doc/source/api/api.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/api/api.rst b/doc/source/api/api.rst index 7b30b5de6..3eb6269b7 100644 --- a/doc/source/api/api.rst +++ b/doc/source/api/api.rst @@ -37,8 +37,10 @@ The Covalent SDK also includes a Python interface for starting and stopping the .. autofunction:: covalent._programmatic.commands.covalent_is_running + .. autofunction:: covalent._programmatic.commands.covalent_start + .. autofunction:: covalent._programmatic.commands.covalent_stop From e18852d367c9ca8174c33d839e15d9dbdaea5f6e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:22:54 +0000 Subject: [PATCH 16/42] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- covalent/_programmatic/commands.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 020bcec71..fba4e8d7b 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -37,16 +37,13 @@ def covalent_is_running() -> bool: return ( pid != -1 and psutil.pid_exists(pid) - and get_config("dispatcher.address") != '' - and get_config("dispatcher.port") != '' + and get_config("dispatcher.address") != "" + and get_config("dispatcher.port") != "" ) def _call_cli_command( - cmd: click.Command, - *, - quiet: bool = False, - **kwargs: Dict[str, Any] + cmd: click.Command, *, quiet: bool = False, **kwargs: Dict[str, Any] ) -> None: """Call a CLI command with the specified kwargs. From 8ea150ab5ae33f18835e8d396adc28d431034528 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 13:40:52 -0500 Subject: [PATCH 17/42] add test for new functions --- tests/covalent_tests/programmatic/__init__.py | 15 ++++ .../programmatic/commands_test.py | 73 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 tests/covalent_tests/programmatic/__init__.py create mode 100644 tests/covalent_tests/programmatic/commands_test.py diff --git a/tests/covalent_tests/programmatic/__init__.py b/tests/covalent_tests/programmatic/__init__.py new file mode 100644 index 000000000..cfc23bfdf --- /dev/null +++ b/tests/covalent_tests/programmatic/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the Apache License 2.0 (the "License"). A copy of the +# License may be obtained with this software package or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Use of this file is prohibited except in compliance with the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py new file mode 100644 index 000000000..eb8880c6a --- /dev/null +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -0,0 +1,73 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the Apache License 2.0 (the "License"). A copy of the +# License may be obtained with this software package or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Use of this file is prohibited except in compliance with the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import covalent as ct + + +def test_covalent_start_and_stop(): + """Test that `covalent_start` successfully starts a local Covalent Server""" + + import requests + + from covalent_dispatcher._cli import _is_server_running + + # Start Covalent + ct.covalent_start(quiet=True) + assert _is_server_running() is True + + # Re-issue start command (should do nothing and exit immediately) + ct.covalent_start(quiet=True) + assert _is_server_running() is True + + # Run a dummy workflow + @ct.lattice + @ct.electron + def dummy_1(): + return "success" + + dispatch_id = ct.dispatch(dummy_1)() + assert ct.get_result(dispatch_id, wait=True).result == "success" + + # Stop Covalent + ct.covalent_stop(quiet=True) + assert _is_server_running() is False + + # Re-issue stop command (should do nothing and exit immediately) + ct.covalent_stop(quiet=True) + assert _is_server_running() is False + + # Try running the dummy workflow again (should fail) + with pytest.raises(requests.exceptions.ConnectionError): + ct.dispatch(dummy_1)() + + # Re-start Covalent after stopping + ct.covalent_start(quiet=False) + assert _is_server_running() is True + + # Run another dummy workflow + @ct.lattice + @ct.electron + def dummy_2(): + return "success again" + + dispatch_id = ct.dispatch(dummy_2)() + assert ct.get_result(dispatch_id, wait=True).result == "success again" + + # Finally, stop Covalent + ct.covalent_stop(quiet=False) + assert _is_server_running() is False From e023bde80e85e374d7894684cb886dfea790a324 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:41:49 +0000 Subject: [PATCH 18/42] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- covalent/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/covalent/__init__.py b/covalent/__init__.py index 4208aded9..aed63a724 100644 --- a/covalent/__init__.py +++ b/covalent/__init__.py @@ -25,7 +25,6 @@ from ._dispatcher_plugins import local_redispatch as redispatch # nopycln: import from ._dispatcher_plugins import stop_triggers # nopycln: import from ._file_transfer import strategies as fs_strategies # nopycln: import -from ._programmatic.commands import covalent_is_running, covalent_start, covalent_stop from ._results_manager.results_manager import ( # nopycln: import cancel, get_result, From 60f9ca2cb3de78c835e310256a1fcdc7a3c3dc7d Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 13:47:57 -0500 Subject: [PATCH 19/42] add test for `covalent_is_running` --- tests/covalent_tests/programmatic/commands_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py index eb8880c6a..d634ca52b 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -71,3 +71,17 @@ def dummy_2(): # Finally, stop Covalent ct.covalent_stop(quiet=False) assert _is_server_running() is False + + +def test_covalent_is_running(): + """Test that the `covalent_is_running` function agrees with the CLI status check""" + + from covalent_dispatcher._cli import _is_server_running + + # Start Covalent + ct.covalent_start(quiet=True) + assert ct.covalent_is_running() is _is_server_running() is True + + # Stop Covalent + ct.covalent_stop(quiet=True) + assert ct.covalent_is_running() is _is_server_running() is False From 112c5f4cef2a99f0b6b06bfe3846e8094f331178 Mon Sep 17 00:00:00 2001 From: sankalp Date: Tue, 21 Nov 2023 14:09:05 -0500 Subject: [PATCH 20/42] removing covalent's dependency on dispatcher --- covalent/__init__.py | 5 ++ covalent/_programmatic/commands.py | 115 ++++++++++++++++++----------- 2 files changed, 76 insertions(+), 44 deletions(-) diff --git a/covalent/__init__.py b/covalent/__init__.py index aed63a724..fa4a08577 100644 --- a/covalent/__init__.py +++ b/covalent/__init__.py @@ -25,6 +25,11 @@ from ._dispatcher_plugins import local_redispatch as redispatch # nopycln: import from ._dispatcher_plugins import stop_triggers # nopycln: import from ._file_transfer import strategies as fs_strategies # nopycln: import +from ._programmatic.commands import ( # nopycln: import + covalent_is_running, + covalent_start, + covalent_stop, +) from ._results_manager.results_manager import ( # nopycln: import cancel, get_result, diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index fba4e8d7b..e9b9d0157 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -23,23 +23,32 @@ import click import psutil -from covalent_dispatcher._cli.service import _read_pid, start, stop - from .._shared_files import logger from .._shared_files.config import get_config +__all__ = ["covalent_is_running", "covalent_start", "covalent_stop"] + + app_log = logger.app_log def covalent_is_running() -> bool: """Return True if the Covalent server is in a ready state.""" - pid = _read_pid(get_config("dispatcher.cache_dir") + "/ui.pid") - return ( - pid != -1 - and psutil.pid_exists(pid) - and get_config("dispatcher.address") != "" - and get_config("dispatcher.port") != "" - ) + try: + from covalent_dispatcher._cli.service import _read_pid + + pid = _read_pid(get_config("dispatcher.cache_dir") + "/ui.pid") + return ( + pid != -1 + and psutil.pid_exists(pid) + and get_config("dispatcher.address") != "" + and get_config("dispatcher.port") != "" + ) + + except ImportError: + # If the covalent_dispatcher is not installed, assume Covalent is not running. + app_log.warning("Covalent has not been installed with the server component.") + return False def _call_cli_command( @@ -114,31 +123,40 @@ def covalent_start( triggers_only: Start only the triggers server. Defaults to False. quiet: Suppress stdout. Defaults to False. """ - if covalent_is_running(): - return - kwargs = { - "develop": develop, - "port": port or get_config("dispatcher.port"), - "mem_per_worker": mem_per_worker or get_config("dask.mem_per_worker"), - "workers": workers or get_config("dask.num_workers"), - "threads_per_worker": threads_per_worker or get_config("dask.threads_per_worker"), - "ignore_migrations": ignore_migrations, - "no_cluster": no_cluster, - "no_triggers": no_triggers, - "triggers_only": triggers_only, - } - - # Run the `covalent start [OPTIONS]` command. - _call_cli_command(start, quiet=quiet, **kwargs) - - # Wait to confirm Covalent server is running. - _poll_with_timeout( - covalent_is_running, - waiting_msg="Waiting for Covalent Server to start...", - timeout_msg="Covalent Server failed to start!", - timeout=10, - ) + try: + from covalent_dispatcher._cli.service import start + + if covalent_is_running(): + return + + kwargs = { + "develop": develop, + "port": port or get_config("dispatcher.port"), + "mem_per_worker": mem_per_worker or get_config("dask.mem_per_worker"), + "workers": workers or get_config("dask.num_workers"), + "threads_per_worker": threads_per_worker or get_config("dask.threads_per_worker"), + "ignore_migrations": ignore_migrations, + "no_cluster": no_cluster, + "no_triggers": no_triggers, + "triggers_only": triggers_only, + } + + # Run the `covalent start [OPTIONS]` command. + _call_cli_command(start, quiet=quiet, **kwargs) + + # Wait to confirm Covalent server is running. + _poll_with_timeout( + covalent_is_running, + waiting_msg="Waiting for Covalent Server to start...", + timeout_msg="Covalent Server failed to start!", + timeout=10, + ) + + except ImportError: + # If the covalent_dispatcher is not installed, show warning and return. + app_log.warning("Covalent has not been installed with the server component.") + return def covalent_stop(*, quiet: bool = False) -> None: @@ -148,16 +166,25 @@ def covalent_stop(*, quiet: bool = False) -> None: Args: quiet: Suppress stdout. Defaults to False. """ - if not covalent_is_running(): - return - # Run the `covalent stop` command. - _call_cli_command(stop, quiet=quiet) + try: + from covalent_dispatcher._cli.service import stop + + if not covalent_is_running(): + return - # Wait to confirm Covalent server is stopped. - _poll_with_timeout( - lambda: not covalent_is_running(), - waiting_msg="Waiting for Covalent server to stop...", - timeout_msg="Failed to stop Covalent server!", - timeout=10, - ) + # Run the `covalent stop` command. + _call_cli_command(stop, quiet=quiet) + + # Wait to confirm Covalent server is stopped. + _poll_with_timeout( + lambda: not covalent_is_running(), + waiting_msg="Waiting for Covalent server to stop...", + timeout_msg="Failed to stop Covalent server!", + timeout=10, + ) + + except ImportError: + # If the covalent_dispatcher is not installed, show warning and return. + app_log.warning("Covalent has not been installed with the server component.") + return From d7c6c05b140adbe71b54677fc4fafb009e39d070 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 14:24:32 -0500 Subject: [PATCH 21/42] ignore pip reqs in new package --- .github/workflows/requirements.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/requirements.yml b/.github/workflows/requirements.yml index 8d692b0fe..30198b6be 100644 --- a/.github/workflows/requirements.yml +++ b/.github/workflows/requirements.yml @@ -54,6 +54,7 @@ jobs: --ignore-file=covalent/triggers/** --ignore-file=covalent/cloud_resource_manager/** --ignore-file=covalent/quantum/qserver/** + --ignore-file=covalent/_programmatic/** covalent - name: Check missing dispatcher requirements From 2b4d947837ac0eb3b75f9614fcf8740149a169a2 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 14:41:44 -0500 Subject: [PATCH 22/42] refactor docstrings --- covalent/_programmatic/commands.py | 41 ++++++++++++++++++------------ 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index e9b9d0157..da991e356 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -33,7 +33,9 @@ def covalent_is_running() -> bool: - """Return True if the Covalent server is in a ready state.""" + """ + Returns :code:`True` if the Covalent server is in a ready state, :code:`False` otherwise. + """ try: from covalent_dispatcher._cli.service import _read_pid @@ -52,13 +54,17 @@ def covalent_is_running() -> bool: def _call_cli_command( - cmd: click.Command, *, quiet: bool = False, **kwargs: Dict[str, Any] + cmd: click.Command, + *, + quiet: bool = False, + **kwargs: Dict[str, Any] ) -> None: - """Call a CLI command with the specified kwargs. + """ + Call a CLI command with the specified kwargs. Args: func: The CLI command to call. - quiet: Suppress stdout. Defaults to False. + quiet: Suppress stdout. Defaults to :code:`False`. """ with contextlib.redirect_stdout(None if quiet else sys.stdout): ctx = click.Context(cmd) @@ -72,7 +78,8 @@ def _poll_with_timeout( timeout_msg: str, timeout: int, ) -> None: - """Poll a callable once per second, until it returns True or a timeout is reached. + """ + Poll a callable once per second, until it returns :code:`True` or the timeout is reached. Args: callable_: The callable to poll. @@ -81,7 +88,7 @@ def _poll_with_timeout( timeout: Timeout in seconds. Raises: - TimeoutError: _description_ + TimeoutError: When the timeout is reached. """ _num_wait = 0 _max_wait = timeout @@ -108,20 +115,21 @@ def covalent_start( *, quiet: bool = False, ) -> None: - """Start the Covalent server. Wrapper for the `covalent start` CLI command. + """ + Start the Covalent server. Wrapper for the :code:`covalent start` CLI command. This function returns immediately if the local Covalent server is already running. Args: - develop: Start local server in develop mode. Defaults to False. - port: Local server port number. Defaults to 48008. + develop: Start local server in develop mode. Defaults to :code:`False`. + port: Local server port number. Defaults to :code:`"48008"`. mem_per_worker: Memory limit per worker in GB. Defaults to auto. workers: Number of Dask workers. Defaults to 8. threads_per_worker: Number of threads per Dask worker. Defaults to 1. - ignore_migrations: Start server without database migrations. Defaults to False. - no_cluster: Start server without Dask cluster. Defaults to False. - no_triggers: Start server without a triggers server. Defaults to False. - triggers_only: Start only the triggers server. Defaults to False. - quiet: Suppress stdout. Defaults to False. + ignore_migrations: Start server without database migrations. Defaults to :code:`False`. + no_cluster: Start server without Dask cluster. Defaults to :code:`False`. + no_triggers: Start server without a triggers server. Defaults to :code:`False`. + triggers_only: Start only the triggers server. Defaults to :code:`False`. + quiet: Suppress stdout. Defaults to :code:`False`. """ try: @@ -160,11 +168,12 @@ def covalent_start( def covalent_stop(*, quiet: bool = False) -> None: - """Stop the Covalent server. Wrapper for the `covalent stop` CLI command. + """ + Stop the Covalent server. Wrapper for the :code:`covalent stop` CLI command. This function returns immediately if the local Covalent server is not running. Args: - quiet: Suppress stdout. Defaults to False. + quiet: Suppress stdout. Defaults to :code:`False`. """ try: From bca8bc034fa1e6d5750148dd07ba497bf95643e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 19:43:16 +0000 Subject: [PATCH 23/42] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- covalent/_programmatic/commands.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index da991e356..a5360ff09 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -54,10 +54,7 @@ def covalent_is_running() -> bool: def _call_cli_command( - cmd: click.Command, - *, - quiet: bool = False, - **kwargs: Dict[str, Any] + cmd: click.Command, *, quiet: bool = False, **kwargs: Dict[str, Any] ) -> None: """ Call a CLI command with the specified kwargs. From 7d18fa466c366970bec8ae71923a9d390d124196 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 15:09:58 -0500 Subject: [PATCH 24/42] update docs --- doc/source/api/api.rst | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/source/api/api.rst b/doc/source/api/api.rst index 3eb6269b7..d41269542 100644 --- a/doc/source/api/api.rst +++ b/doc/source/api/api.rst @@ -27,13 +27,9 @@ The following API documentation describes how to use Covalent. Covalent Server """"""""""""""""""""""""""" -A Covalent server must be running in order to dispatch workflows. The Covalent CLI provides various utilities for starting, stopping, and managing a Covalent server. For more information, see: +A Covalent server must be running in order to dispatch workflows. The Covalent CLI provides various utilities for starting, stopping, and managing a Covalent server. For more information, see the Covalent CLI help menu at :code:`covalent --help`. -.. code-block:: bash - - covalent --help - -The Covalent SDK also includes a Python interface for starting and stopping the covalent server. +The Covalent SDK also includes a Python interface for starting and stopping the Covalent server. .. autofunction:: covalent._programmatic.commands.covalent_is_running From f6e0e3a418c88df860f736b06a7c0b3229cfec5a Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 15:28:13 -0500 Subject: [PATCH 25/42] revert api docs --- doc/source/api/api.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/source/api/api.rst b/doc/source/api/api.rst index d41269542..bbdf1f577 100644 --- a/doc/source/api/api.rst +++ b/doc/source/api/api.rst @@ -27,16 +27,18 @@ The following API documentation describes how to use Covalent. Covalent Server """"""""""""""""""""""""""" -A Covalent server must be running in order to dispatch workflows. The Covalent CLI provides various utilities for starting, stopping, and managing a Covalent server. For more information, see the Covalent CLI help menu at :code:`covalent --help`. +A Covalent server must be running in order to dispatch workflows. The Covalent CLI provides various utilities for starting, stopping, and managing a Covalent server. For more information, see: + +.. code-block:: bash + + covalent --help The Covalent SDK also includes a Python interface for starting and stopping the Covalent server. .. autofunction:: covalent._programmatic.commands.covalent_is_running - .. autofunction:: covalent._programmatic.commands.covalent_start - .. autofunction:: covalent._programmatic.commands.covalent_stop From d54ee565cd2bfcd856511ff15e8a14d46ce5e563 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 15:29:37 -0500 Subject: [PATCH 26/42] =?UTF-8?q?include=20'Returns'=20in=20docstrings=20s?= =?UTF-8?q?o=20maybe=20docs=20will=20render=20=F0=9F=A4=B7=20pls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- covalent/_programmatic/commands.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index a5360ff09..399a3e570 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -34,7 +34,10 @@ def covalent_is_running() -> bool: """ - Returns :code:`True` if the Covalent server is in a ready state, :code:`False` otherwise. + Check if the Covalent server is running. + + Returns: + :code:`True` if the Covalent server is in a ready state, :code:`False` otherwise. """ try: from covalent_dispatcher._cli.service import _read_pid @@ -127,6 +130,9 @@ def covalent_start( no_triggers: Start server without a triggers server. Defaults to :code:`False`. triggers_only: Start only the triggers server. Defaults to :code:`False`. quiet: Suppress stdout. Defaults to :code:`False`. + + Returns: + None """ try: @@ -171,6 +177,9 @@ def covalent_stop(*, quiet: bool = False) -> None: Args: quiet: Suppress stdout. Defaults to :code:`False`. + + Returns: + None """ try: From ea1c6a6ba5afb53c1366c6c2f2f11e5c6ed8784a Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 15:39:31 -0500 Subject: [PATCH 27/42] =?UTF-8?q?remove=20useless=20'Returns'=20from=20doc?= =?UTF-8?q?strings=20=F0=9F=A4=A6=E2=80=8D=E2=99=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- covalent/_programmatic/commands.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 399a3e570..54b104b23 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -130,9 +130,6 @@ def covalent_start( no_triggers: Start server without a triggers server. Defaults to :code:`False`. triggers_only: Start only the triggers server. Defaults to :code:`False`. quiet: Suppress stdout. Defaults to :code:`False`. - - Returns: - None """ try: @@ -177,9 +174,6 @@ def covalent_stop(*, quiet: bool = False) -> None: Args: quiet: Suppress stdout. Defaults to :code:`False`. - - Returns: - None """ try: From d7ef4639eaf24af15139bcebd3fc0a73fb30512f Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 15:53:49 -0500 Subject: [PATCH 28/42] try autofunction refs to main namespace instead --- doc/source/api/api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/api/api.rst b/doc/source/api/api.rst index bbdf1f577..260e47aac 100644 --- a/doc/source/api/api.rst +++ b/doc/source/api/api.rst @@ -35,11 +35,11 @@ A Covalent server must be running in order to dispatch workflows. The Covalent C The Covalent SDK also includes a Python interface for starting and stopping the Covalent server. -.. autofunction:: covalent._programmatic.commands.covalent_is_running +.. autofunction:: covalent.covalent_is_running -.. autofunction:: covalent._programmatic.commands.covalent_start +.. autofunction:: covalent.covalent_start -.. autofunction:: covalent._programmatic.commands.covalent_stop +.. autofunction:: covalent.covalent_stop ---------------------------------------------------------------- From 8b107eb1b1470b45099ab2c6ea221bc630ddd3a3 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Tue, 21 Nov 2023 16:12:48 -0500 Subject: [PATCH 29/42] revert using main namespace refs --- doc/source/api/api.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/source/api/api.rst b/doc/source/api/api.rst index 260e47aac..b4ef1e9b1 100644 --- a/doc/source/api/api.rst +++ b/doc/source/api/api.rst @@ -35,11 +35,13 @@ A Covalent server must be running in order to dispatch workflows. The Covalent C The Covalent SDK also includes a Python interface for starting and stopping the Covalent server. -.. autofunction:: covalent.covalent_is_running +.. autofunction:: covalent._programmatic.commands.covalent_is_running -.. autofunction:: covalent.covalent_start -.. autofunction:: covalent.covalent_stop +.. autofunction:: covalent._programmatic.commands.covalent_start + + +.. autofunction:: covalent._programmatic.commands.covalent_stop ---------------------------------------------------------------- From de284c1b1f98a2bb674fd6bd212334f2f968bcf7 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Wed, 22 Nov 2023 10:43:48 -0500 Subject: [PATCH 30/42] add more logging and edit messages --- covalent/_programmatic/commands.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 54b104b23..8910dbca5 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -136,6 +136,7 @@ def covalent_start( from covalent_dispatcher._cli.service import start if covalent_is_running(): + app_log.debug("Covalent server is already running.") return kwargs = { @@ -151,13 +152,14 @@ def covalent_start( } # Run the `covalent start [OPTIONS]` command. + app_log.debug("Starting Covalent server programmatically...") _call_cli_command(start, quiet=quiet, **kwargs) # Wait to confirm Covalent server is running. _poll_with_timeout( covalent_is_running, waiting_msg="Waiting for Covalent Server to start...", - timeout_msg="Covalent Server failed to start!", + timeout_msg="Failed to start Covalent server programmatically!", timeout=10, ) @@ -180,16 +182,18 @@ def covalent_stop(*, quiet: bool = False) -> None: from covalent_dispatcher._cli.service import stop if not covalent_is_running(): + app_log.debug("Covalent server is not running.") return # Run the `covalent stop` command. + app_log.debug("Stopping Covalent server programmatically...") _call_cli_command(stop, quiet=quiet) # Wait to confirm Covalent server is stopped. _poll_with_timeout( lambda: not covalent_is_running(), waiting_msg="Waiting for Covalent server to stop...", - timeout_msg="Failed to stop Covalent server!", + timeout_msg="Failed to stop Covalent server programmatically!", timeout=10, ) From 13bfdb671ca5c16bd210ed5f57f468e24b7a8445 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Wed, 22 Nov 2023 13:51:29 -0500 Subject: [PATCH 31/42] refactor hanging tests --- .../programmatic/commands_test.py | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py index d634ca52b..e93feea34 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -26,6 +26,8 @@ def test_covalent_start_and_stop(): from covalent_dispatcher._cli import _is_server_running + _starting_state = _is_server_running() + # Start Covalent ct.covalent_start(quiet=True) assert _is_server_running() is True @@ -34,15 +36,6 @@ def test_covalent_start_and_stop(): ct.covalent_start(quiet=True) assert _is_server_running() is True - # Run a dummy workflow - @ct.lattice - @ct.electron - def dummy_1(): - return "success" - - dispatch_id = ct.dispatch(dummy_1)() - assert ct.get_result(dispatch_id, wait=True).result == "success" - # Stop Covalent ct.covalent_stop(quiet=True) assert _is_server_running() is False @@ -53,24 +46,12 @@ def dummy_1(): # Try running the dummy workflow again (should fail) with pytest.raises(requests.exceptions.ConnectionError): - ct.dispatch(dummy_1)() - - # Re-start Covalent after stopping - ct.covalent_start(quiet=False) - assert _is_server_running() is True - - # Run another dummy workflow - @ct.lattice - @ct.electron - def dummy_2(): - return "success again" + ct.dispatch(ct.lattice(ct.electron(lambda: "result")))() - dispatch_id = ct.dispatch(dummy_2)() - assert ct.get_result(dispatch_id, wait=True).result == "success again" - - # Finally, stop Covalent - ct.covalent_stop(quiet=False) - assert _is_server_running() is False + if _starting_state: + # Re-start Covalent after stopping + ct.covalent_start(quiet=False) + assert _is_server_running() is True def test_covalent_is_running(): @@ -78,10 +59,25 @@ def test_covalent_is_running(): from covalent_dispatcher._cli import _is_server_running - # Start Covalent - ct.covalent_start(quiet=True) - assert ct.covalent_is_running() is _is_server_running() is True + _starting_state = _is_server_running() - # Stop Covalent - ct.covalent_stop(quiet=True) - assert ct.covalent_is_running() is _is_server_running() is False + # Stop then start Covalent or vice versa, depending on starting state. + if _starting_state: + # Stop Covalent + ct.covalent_stop(quiet=True) + assert ct.covalent_is_running() is _is_server_running() is False + + # Re-start Covalent + ct.covalent_start(quiet=True) + assert ct.covalent_is_running() is _is_server_running() is True + else: + # Start Covalent + ct.covalent_start(quiet=True) + assert ct.covalent_is_running() is _is_server_running() is True + + # Re-stop Covalent + ct.covalent_stop(quiet=True) + assert ct.covalent_is_running() is _is_server_running() is False + + # Check that Covalent server is back in its starting state. + assert ct.covalent_is_running() is _starting_state From f308f8e737d0a43b20eccc848338c2b25e164ff9 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Wed, 22 Nov 2023 14:43:22 -0500 Subject: [PATCH 32/42] refactor tests into functional tests --- tests/covalent_tests/programmatic/__init__.py | 15 --------------- .../programmatic_commands_test.py} | 10 ++++++++++ 2 files changed, 10 insertions(+), 15 deletions(-) delete mode 100644 tests/covalent_tests/programmatic/__init__.py rename tests/{covalent_tests/programmatic/commands_test.py => functional_tests/programmatic_commands_test.py} (91%) diff --git a/tests/covalent_tests/programmatic/__init__.py b/tests/covalent_tests/programmatic/__init__.py deleted file mode 100644 index cfc23bfdf..000000000 --- a/tests/covalent_tests/programmatic/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2021 Agnostiq Inc. -# -# This file is part of Covalent. -# -# Licensed under the Apache License 2.0 (the "License"). A copy of the -# License may be obtained with this software package or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Use of this file is prohibited except in compliance with the License. -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/functional_tests/programmatic_commands_test.py similarity index 91% rename from tests/covalent_tests/programmatic/commands_test.py rename to tests/functional_tests/programmatic_commands_test.py index e93feea34..6585c0025 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/functional_tests/programmatic_commands_test.py @@ -36,6 +36,16 @@ def test_covalent_start_and_stop(): ct.covalent_start(quiet=True) assert _is_server_running() is True + # Run a dummy workflow + @ct.lattice + @ct.electron + def dummy_workflow(): + return "success" + + dispatch_id = ct.dispatch(dummy_workflow)() + result = ct.get_result(dispatch_id, wait=True) + assert result.result == "success" + # Stop Covalent ct.covalent_stop(quiet=True) assert _is_server_running() is False From e7709eaea5ab40928373841f33c7000b948e3540 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Wed, 22 Nov 2023 22:04:39 -0500 Subject: [PATCH 33/42] Revert "refactor tests into functional tests" This reverts commit f308f8e737d0a43b20eccc848338c2b25e164ff9. --- tests/covalent_tests/programmatic/__init__.py | 15 +++++++++++++++ .../programmatic/commands_test.py} | 10 ---------- 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 tests/covalent_tests/programmatic/__init__.py rename tests/{functional_tests/programmatic_commands_test.py => covalent_tests/programmatic/commands_test.py} (91%) diff --git a/tests/covalent_tests/programmatic/__init__.py b/tests/covalent_tests/programmatic/__init__.py new file mode 100644 index 000000000..cfc23bfdf --- /dev/null +++ b/tests/covalent_tests/programmatic/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the Apache License 2.0 (the "License"). A copy of the +# License may be obtained with this software package or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Use of this file is prohibited except in compliance with the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/functional_tests/programmatic_commands_test.py b/tests/covalent_tests/programmatic/commands_test.py similarity index 91% rename from tests/functional_tests/programmatic_commands_test.py rename to tests/covalent_tests/programmatic/commands_test.py index 6585c0025..e93feea34 100644 --- a/tests/functional_tests/programmatic_commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -36,16 +36,6 @@ def test_covalent_start_and_stop(): ct.covalent_start(quiet=True) assert _is_server_running() is True - # Run a dummy workflow - @ct.lattice - @ct.electron - def dummy_workflow(): - return "success" - - dispatch_id = ct.dispatch(dummy_workflow)() - result = ct.get_result(dispatch_id, wait=True) - assert result.result == "success" - # Stop Covalent ct.covalent_stop(quiet=True) assert _is_server_running() is False From cf6f286718164ed034d3af20c8b3854deb2574f5 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Thu, 23 Nov 2023 10:17:24 -0500 Subject: [PATCH 34/42] create global var for timeout --- covalent/_programmatic/commands.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 8910dbca5..73bf029a6 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -31,6 +31,9 @@ app_log = logger.app_log +# Maximum time in seconds to spend verifying covalent start and stop. +_TIMEOUT = 10 + def covalent_is_running() -> bool: """ @@ -160,7 +163,7 @@ def covalent_start( covalent_is_running, waiting_msg="Waiting for Covalent Server to start...", timeout_msg="Failed to start Covalent server programmatically!", - timeout=10, + timeout=_TIMEOUT, ) except ImportError: @@ -194,7 +197,7 @@ def covalent_stop(*, quiet: bool = False) -> None: lambda: not covalent_is_running(), waiting_msg="Waiting for Covalent server to stop...", timeout_msg="Failed to stop Covalent server programmatically!", - timeout=10, + timeout=_TIMEOUT, ) except ImportError: From 07fa34edbbb572e1e6d9183389f6a432de07b0a6 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Thu, 23 Nov 2023 10:17:47 -0500 Subject: [PATCH 35/42] use mock start and stop commands --- .../programmatic/commands_test.py | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py index e93feea34..ed7e278a4 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -19,65 +19,71 @@ import covalent as ct -def test_covalent_start_and_stop(): +def test_covalent_start_and_stop(mocker): """Test that `covalent_start` successfully starts a local Covalent Server""" - import requests + def _flipping_retval(): + retval = _flipping_retval.state + _flipping_retval.state = not _flipping_retval.state + return retval - from covalent_dispatcher._cli import _is_server_running + # Disable server actions in test environment + mocker.patch("click.Context.invoke", return_value=None) + + # Assumer server is not running + covalent_is_running_patch = mocker.patch( + "covalent._programmatic.commands.covalent_is_running", + ) + covalent_is_running_patch.side_effect = _flipping_retval - _starting_state = _is_server_running() + # Since `covalent_is_running` is called at least twice in both start and stop: + # once to check check status and again to check for change in status. - # Start Covalent + # Start Covalent as if not running. + _flipping_retval.state = False ct.covalent_start(quiet=True) - assert _is_server_running() is True - # Re-issue start command (should do nothing and exit immediately) + # Re-issue start command as if already running. + _flipping_retval.state = True ct.covalent_start(quiet=True) - assert _is_server_running() is True - # Stop Covalent + # Stop Covalent as if running. + _flipping_retval.state = True ct.covalent_stop(quiet=True) - assert _is_server_running() is False - # Re-issue stop command (should do nothing and exit immediately) + # Re-issue stop command as if already stopped. + _flipping_retval.state = False ct.covalent_stop(quiet=True) - assert _is_server_running() is False - # Try running the dummy workflow again (should fail) - with pytest.raises(requests.exceptions.ConnectionError): - ct.dispatch(ct.lattice(ct.electron(lambda: "result")))() - if _starting_state: - # Re-start Covalent after stopping - ct.covalent_start(quiet=False) - assert _is_server_running() is True +def test_covalent_start_and_stop_timeouts(mocker): + """Test that `covalent_start` successfully starts a local Covalent Server""" + # Disable server actions in test environment + mocker.patch("click.Context.invoke", return_value=None) -def test_covalent_is_running(): - """Test that the `covalent_is_running` function agrees with the CLI status check""" + # Make timeout shorter for testing + mocker.patch("covalent._programmatic.commands._TIMEOUT", 0.1) - from covalent_dispatcher._cli import _is_server_running + # Assumer server is not running + covalent_is_running_patch = mocker.patch( + "covalent._programmatic.commands.covalent_is_running", + ) - _starting_state = _is_server_running() + covalent_is_running_patch.return_value = False + with pytest.raises(TimeoutError): + ct.covalent_start(quiet=True) - # Stop then start Covalent or vice versa, depending on starting state. - if _starting_state: - # Stop Covalent + covalent_is_running_patch.return_value = True + with pytest.raises(TimeoutError): ct.covalent_stop(quiet=True) - assert ct.covalent_is_running() is _is_server_running() is False - # Re-start Covalent - ct.covalent_start(quiet=True) - assert ct.covalent_is_running() is _is_server_running() is True - else: - # Start Covalent - ct.covalent_start(quiet=True) - assert ct.covalent_is_running() is _is_server_running() is True - # Re-stop Covalent - ct.covalent_stop(quiet=True) - assert ct.covalent_is_running() is _is_server_running() is False +def test_covalent_is_running(): + """Test that the `covalent_is_running` function agrees with the CLI status check""" + + from covalent_dispatcher._cli import _is_server_running - # Check that Covalent server is back in its starting state. - assert ct.covalent_is_running() is _starting_state + # TODO: Seems we can't start/stop the server in the test environment. + # Check here is not complete, but at least it is necessary. + assert ct.covalent_is_running() is _is_server_running() From 61c27b8c7dd215531ad4b6b4ba4e0051ea95109c Mon Sep 17 00:00:00 2001 From: Sankalp Sanand Date: Fri, 24 Nov 2023 04:07:01 -0500 Subject: [PATCH 36/42] renamed server check function and added import error check tests --- covalent/__init__.py | 2 +- covalent/_programmatic/commands.py | 21 +++--- doc/source/api/api.rst | 2 +- .../programmatic/commands_test.py | 70 ++++++++++++++++--- 4 files changed, 73 insertions(+), 22 deletions(-) diff --git a/covalent/__init__.py b/covalent/__init__.py index fa4a08577..524952776 100644 --- a/covalent/__init__.py +++ b/covalent/__init__.py @@ -26,9 +26,9 @@ from ._dispatcher_plugins import stop_triggers # nopycln: import from ._file_transfer import strategies as fs_strategies # nopycln: import from ._programmatic.commands import ( # nopycln: import - covalent_is_running, covalent_start, covalent_stop, + is_covalent_running, ) from ._results_manager.results_manager import ( # nopycln: import cancel, diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 73bf029a6..28364dcfa 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -26,7 +26,7 @@ from .._shared_files import logger from .._shared_files.config import get_config -__all__ = ["covalent_is_running", "covalent_start", "covalent_stop"] +__all__ = ["is_covalent_running", "covalent_start", "covalent_stop"] app_log = logger.app_log @@ -35,7 +35,10 @@ _TIMEOUT = 10 -def covalent_is_running() -> bool: +WARNING_MSG = "Covalent has not been installed with the server component." + + +def is_covalent_running() -> bool: """ Check if the Covalent server is running. @@ -55,7 +58,7 @@ def covalent_is_running() -> bool: except ImportError: # If the covalent_dispatcher is not installed, assume Covalent is not running. - app_log.warning("Covalent has not been installed with the server component.") + app_log.warning(WARNING_MSG) return False @@ -138,7 +141,7 @@ def covalent_start( try: from covalent_dispatcher._cli.service import start - if covalent_is_running(): + if is_covalent_running(): app_log.debug("Covalent server is already running.") return @@ -160,7 +163,7 @@ def covalent_start( # Wait to confirm Covalent server is running. _poll_with_timeout( - covalent_is_running, + is_covalent_running, waiting_msg="Waiting for Covalent Server to start...", timeout_msg="Failed to start Covalent server programmatically!", timeout=_TIMEOUT, @@ -168,7 +171,7 @@ def covalent_start( except ImportError: # If the covalent_dispatcher is not installed, show warning and return. - app_log.warning("Covalent has not been installed with the server component.") + app_log.warning(WARNING_MSG) return @@ -184,7 +187,7 @@ def covalent_stop(*, quiet: bool = False) -> None: try: from covalent_dispatcher._cli.service import stop - if not covalent_is_running(): + if not is_covalent_running(): app_log.debug("Covalent server is not running.") return @@ -194,7 +197,7 @@ def covalent_stop(*, quiet: bool = False) -> None: # Wait to confirm Covalent server is stopped. _poll_with_timeout( - lambda: not covalent_is_running(), + lambda: not is_covalent_running(), waiting_msg="Waiting for Covalent server to stop...", timeout_msg="Failed to stop Covalent server programmatically!", timeout=_TIMEOUT, @@ -202,5 +205,5 @@ def covalent_stop(*, quiet: bool = False) -> None: except ImportError: # If the covalent_dispatcher is not installed, show warning and return. - app_log.warning("Covalent has not been installed with the server component.") + app_log.warning(WARNING_MSG) return diff --git a/doc/source/api/api.rst b/doc/source/api/api.rst index b4ef1e9b1..01b694676 100644 --- a/doc/source/api/api.rst +++ b/doc/source/api/api.rst @@ -35,7 +35,7 @@ A Covalent server must be running in order to dispatch workflows. The Covalent C The Covalent SDK also includes a Python interface for starting and stopping the Covalent server. -.. autofunction:: covalent._programmatic.commands.covalent_is_running +.. autofunction:: covalent._programmatic.commands.is_covalent_running .. autofunction:: covalent._programmatic.commands.covalent_start diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py index ed7e278a4..d1db37f55 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -17,6 +17,7 @@ import pytest import covalent as ct +from covalent._programmatic.commands import WARNING_MSG def test_covalent_start_and_stop(mocker): @@ -31,12 +32,12 @@ def _flipping_retval(): mocker.patch("click.Context.invoke", return_value=None) # Assumer server is not running - covalent_is_running_patch = mocker.patch( - "covalent._programmatic.commands.covalent_is_running", + is_covalent_running_patch = mocker.patch( + "covalent._programmatic.commands.is_covalent_running", ) - covalent_is_running_patch.side_effect = _flipping_retval + is_covalent_running_patch.side_effect = _flipping_retval - # Since `covalent_is_running` is called at least twice in both start and stop: + # Since `is_covalent_running` is called at least twice in both start and stop: # once to check check status and again to check for change in status. # Start Covalent as if not running. @@ -66,24 +67,71 @@ def test_covalent_start_and_stop_timeouts(mocker): mocker.patch("covalent._programmatic.commands._TIMEOUT", 0.1) # Assumer server is not running - covalent_is_running_patch = mocker.patch( - "covalent._programmatic.commands.covalent_is_running", + is_covalent_running_patch = mocker.patch( + "covalent._programmatic.commands.is_covalent_running", ) - covalent_is_running_patch.return_value = False + is_covalent_running_patch.return_value = False with pytest.raises(TimeoutError): ct.covalent_start(quiet=True) - covalent_is_running_patch.return_value = True + is_covalent_running_patch.return_value = True with pytest.raises(TimeoutError): ct.covalent_stop(quiet=True) -def test_covalent_is_running(): - """Test that the `covalent_is_running` function agrees with the CLI status check""" +def test_covalent_start_import_error(mocker): + """Test that `covalent_start` shows a warning if covalent_dispatcher is not installed""" + + mocker.patch("click.Context.invoke", return_value=None) + + mock_app_log_warning = mocker.patch("covalent._programmatic.commands.app_log.warning") + + mocker.patch( + "covalent._programmatic.commands.is_covalent_running", + side_effect=ImportError, + ) + + ct.covalent_start(quiet=True) + mock_app_log_warning.assert_called_once_with(WARNING_MSG) + + +def test_covalent_stop_import_error(mocker): + """Test that `covalent_stop` shows a warning if covalent_dispatcher is not installed""" + + mocker.patch("click.Context.invoke", return_value=None) + + mock_app_log_warning = mocker.patch("covalent._programmatic.commands.app_log.warning") + + mocker.patch( + "covalent._programmatic.commands.is_covalent_running", + side_effect=ImportError, + ) + + ct.covalent_stop(quiet=True) + mock_app_log_warning.assert_called_once_with(WARNING_MSG) + + +def test_is_covalent_running(): + """Test that the `is_covalent_running` function agrees with the CLI status check""" from covalent_dispatcher._cli import _is_server_running # TODO: Seems we can't start/stop the server in the test environment. # Check here is not complete, but at least it is necessary. - assert ct.covalent_is_running() is _is_server_running() + assert ct.is_covalent_running() == _is_server_running() + + +def test_is_covalent_running_import_error(mocker): + """Test that `is_covalent_running` returns False if covalent_dispatcher is not installed""" + + mock_app_log_warning = mocker.patch("covalent._programmatic.commands.app_log.warning") + + # _read_pid isn't importable + mocker.patch( + "covalent_dispatcher._cli.service._read_pid", + side_effect=ImportError, + ) + + assert not ct.is_covalent_running() + mock_app_log_warning.assert_called_once_with(WARNING_MSG) From 243982b91f6b5a844b5409079f207227e4a86b5f Mon Sep 17 00:00:00 2001 From: Sankalp Sanand Date: Fri, 24 Nov 2023 06:24:23 -0500 Subject: [PATCH 37/42] None wasn't an acceptable value to redirect_stdout --- covalent/_programmatic/commands.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 28364dcfa..59a989da8 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -16,6 +16,7 @@ """Functions providing programmatic access to Covalent CLI commands.""" import contextlib +import os import sys import time from typing import Any, Callable, Dict, Optional @@ -72,9 +73,11 @@ def _call_cli_command( func: The CLI command to call. quiet: Suppress stdout. Defaults to :code:`False`. """ - with contextlib.redirect_stdout(None if quiet else sys.stdout): - ctx = click.Context(cmd) - ctx.invoke(cmd, **kwargs) + + with open(os.devnull, "w") as fnull: + with contextlib.redirect_stdout(fnull if quiet else sys.stdout): + ctx = click.Context(cmd) + ctx.invoke(cmd, **kwargs) def _poll_with_timeout( From 0e1e07cbb9d87593b807cbeaa85da09100b35026 Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Fri, 24 Nov 2023 10:43:53 -0500 Subject: [PATCH 38/42] refactor to use subprocess --- covalent/_programmatic/commands.py | 184 +++++++++++------------------ 1 file changed, 72 insertions(+), 112 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 59a989da8..a8a502532 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -15,13 +15,9 @@ # limitations under the License. """Functions providing programmatic access to Covalent CLI commands.""" -import contextlib -import os -import sys -import time -from typing import Any, Callable, Dict, Optional +import subprocess +from typing import List, Optional -import click import psutil from .._shared_files import logger @@ -32,8 +28,32 @@ app_log = logger.app_log -# Maximum time in seconds to spend verifying covalent start and stop. -_TIMEOUT = 10 + +def _call_cli_command( + cmd: List[str], + *, + quiet: bool = False +) -> subprocess.CompletedProcess: + """ + Call a CLI command with the specified kwargs. + + Args: + func: The CLI command to call. + quiet: Suppress stdout. Defaults to :code:`False`. + """ + + if quiet: + return subprocess.run( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + + return subprocess.run(cmd, check=True) + + +_MISSING_SDK_WARNING = "Covalent has not been installed with the server component." WARNING_MSG = "Covalent has not been installed with the server component." @@ -57,60 +77,12 @@ def is_covalent_running() -> bool: and get_config("dispatcher.port") != "" ) - except ImportError: + except ModuleNotFoundError: # If the covalent_dispatcher is not installed, assume Covalent is not running. - app_log.warning(WARNING_MSG) + app_log.warning(_MISSING_SDK_WARNING) return False -def _call_cli_command( - cmd: click.Command, *, quiet: bool = False, **kwargs: Dict[str, Any] -) -> None: - """ - Call a CLI command with the specified kwargs. - - Args: - func: The CLI command to call. - quiet: Suppress stdout. Defaults to :code:`False`. - """ - - with open(os.devnull, "w") as fnull: - with contextlib.redirect_stdout(fnull if quiet else sys.stdout): - ctx = click.Context(cmd) - ctx.invoke(cmd, **kwargs) - - -def _poll_with_timeout( - callable_: Callable[[], bool], - *, - waiting_msg: str, - timeout_msg: str, - timeout: int, -) -> None: - """ - Poll a callable once per second, until it returns :code:`True` or the timeout is reached. - - Args: - callable_: The callable to poll. - waiting_msg: Log message to display while waiting. - timeout_msg: Error message to display if timeout is reached. - timeout: Timeout in seconds. - - Raises: - TimeoutError: When the timeout is reached. - """ - _num_wait = 0 - _max_wait = timeout - - while not callable_(): - app_log.debug(waiting_msg) - - time.sleep(1) - _num_wait += 1 - if _num_wait >= _max_wait: - raise TimeoutError(timeout_msg) - - def covalent_start( develop: bool = False, port: Optional[str] = None, @@ -141,42 +113,42 @@ def covalent_start( quiet: Suppress stdout. Defaults to :code:`False`. """ - try: - from covalent_dispatcher._cli.service import start - - if is_covalent_running(): - app_log.debug("Covalent server is already running.") - return - - kwargs = { - "develop": develop, - "port": port or get_config("dispatcher.port"), - "mem_per_worker": mem_per_worker or get_config("dask.mem_per_worker"), - "workers": workers or get_config("dask.num_workers"), - "threads_per_worker": threads_per_worker or get_config("dask.threads_per_worker"), - "ignore_migrations": ignore_migrations, - "no_cluster": no_cluster, - "no_triggers": no_triggers, - "triggers_only": triggers_only, - } - - # Run the `covalent start [OPTIONS]` command. - app_log.debug("Starting Covalent server programmatically...") - _call_cli_command(start, quiet=quiet, **kwargs) - - # Wait to confirm Covalent server is running. - _poll_with_timeout( - is_covalent_running, - waiting_msg="Waiting for Covalent Server to start...", - timeout_msg="Failed to start Covalent server programmatically!", - timeout=_TIMEOUT, - ) + if is_covalent_running(): + msg = "Covalent server is already running." + if not quiet: + print(msg) - except ImportError: - # If the covalent_dispatcher is not installed, show warning and return. - app_log.warning(WARNING_MSG) + app_log.debug(msg) return + flags = { + "--develop": develop, + "--ignore-migrations": ignore_migrations, + "--no-cluster": no_cluster, + "--no-triggers": no_triggers, + "--triggers-only": triggers_only, + } + + args = { + "--port": port or get_config("dispatcher.port"), + "--mem-per-worker": mem_per_worker or get_config("dask.mem_per_worker"), + "--workers": workers or get_config("dask.num_workers"), + "--threads-per-worker": threads_per_worker or get_config("dask.threads_per_worker"), + } + + cmd = ["covalent", "start"] + for flag, value in flags.items(): + if value: + cmd.append(flag) + + for arg, value in args.items(): + cmd.append(arg) + cmd.append(str(value)) + + # Run the `covalent start [OPTIONS]` command. + app_log.debug("Starting Covalent server programmatically...") + _call_cli_command(cmd, quiet=quiet) + def covalent_stop(*, quiet: bool = False) -> None: """ @@ -187,26 +159,14 @@ def covalent_stop(*, quiet: bool = False) -> None: quiet: Suppress stdout. Defaults to :code:`False`. """ - try: - from covalent_dispatcher._cli.service import stop - - if not is_covalent_running(): - app_log.debug("Covalent server is not running.") - return - - # Run the `covalent stop` command. - app_log.debug("Stopping Covalent server programmatically...") - _call_cli_command(stop, quiet=quiet) - - # Wait to confirm Covalent server is stopped. - _poll_with_timeout( - lambda: not is_covalent_running(), - waiting_msg="Waiting for Covalent server to stop...", - timeout_msg="Failed to stop Covalent server programmatically!", - timeout=_TIMEOUT, - ) + if not is_covalent_running(): + msg = "Covalent server is not running." + if not quiet: + print(msg) - except ImportError: - # If the covalent_dispatcher is not installed, show warning and return. - app_log.warning(WARNING_MSG) + app_log.debug(msg) return + + # Run the `covalent stop` command. + app_log.debug("Stopping Covalent server programmatically...") + _call_cli_command(["covalent", "stop"], quiet=quiet) From ea53935dd5ac2c60c533ef94f1c7c36c340da83d Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Fri, 24 Nov 2023 10:44:19 -0500 Subject: [PATCH 39/42] refactor as multiple tests w/ patched start/stop --- covalent/_programmatic/commands.py | 3 - .../programmatic/commands_test.py | 157 +++++++++--------- 2 files changed, 80 insertions(+), 80 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index a8a502532..6064e6982 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -56,9 +56,6 @@ def _call_cli_command( _MISSING_SDK_WARNING = "Covalent has not been installed with the server component." -WARNING_MSG = "Covalent has not been installed with the server component." - - def is_covalent_running() -> bool: """ Check if the Covalent server is running. diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py index d1db37f55..cdd0a4210 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -17,121 +17,124 @@ import pytest import covalent as ct -from covalent._programmatic.commands import WARNING_MSG -def test_covalent_start_and_stop(mocker): - """Test that `covalent_start` successfully starts a local Covalent Server""" +def test_is_covalent_running(mocker): + """Check that `is_covalent_running` returns True when the server is running.""" + try: + from covalent_dispatcher._cli.service import _read_pid + except (ModuleNotFoundError, ImportError): + pytest.xfail("`covalent_dispatcher` not installed") - def _flipping_retval(): - retval = _flipping_retval.state - _flipping_retval.state = not _flipping_retval.state - return retval - - # Disable server actions in test environment - mocker.patch("click.Context.invoke", return_value=None) - - # Assumer server is not running - is_covalent_running_patch = mocker.patch( - "covalent._programmatic.commands.is_covalent_running", + # Simulate server running + mocker.patch("covalent_dispatcher._cli.service._read_pid", return_value=1) + mocker.patch("psutil.pid_exists", return_value=True) + mocker.patch( + "covalent._shared_files.config.get_config", + return_value={"port": 48008, "host": "localhost"} ) - is_covalent_running_patch.side_effect = _flipping_retval + assert ct.is_covalent_running() - # Since `is_covalent_running` is called at least twice in both start and stop: - # once to check check status and again to check for change in status. + # Simulate server stopped + mocker.patch("covalent_dispatcher._cli.service._read_pid", return_value=-1) + assert not ct.is_covalent_running() - # Start Covalent as if not running. - _flipping_retval.state = False - ct.covalent_start(quiet=True) - # Re-issue start command as if already running. - _flipping_retval.state = True - ct.covalent_start(quiet=True) +def test_is_covalent_running_import_error(mocker): + """Check that `is_covalent_running` catches the `ModuleNotFoundError`.""" + from covalent._programmatic.commands import _MISSING_SDK_WARNING - # Stop Covalent as if running. - _flipping_retval.state = True - ct.covalent_stop(quiet=True) + try: + from covalent_dispatcher._cli.service import _read_pid + except (ModuleNotFoundError, ImportError): + pytest.xfail("`covalent_dispatcher` not installed") - # Re-issue stop command as if already stopped. - _flipping_retval.state = False - ct.covalent_stop(quiet=True) + mocker.patch( + "covalent_dispatcher._cli.service._read_pid", + side_effect=ModuleNotFoundError(), + ) + + mock_app_log = mocker.patch("covalent._programmatic.commands.app_log") + assert not ct.is_covalent_running() + mock_app_log.warning.assert_called_once_with(_MISSING_SDK_WARNING) -def test_covalent_start_and_stop_timeouts(mocker): - """Test that `covalent_start` successfully starts a local Covalent Server""" - # Disable server actions in test environment - mocker.patch("click.Context.invoke", return_value=None) +def test_covalent_start(mocker): + """Test the `covalent_start` function without actually starting server.""" - # Make timeout shorter for testing - mocker.patch("covalent._programmatic.commands._TIMEOUT", 0.1) + mocker.patch("subprocess.run") - # Assumer server is not running - is_covalent_running_patch = mocker.patch( + # Simulate server running + mocker.patch( "covalent._programmatic.commands.is_covalent_running", + return_value=True, ) + ct.covalent_start() - is_covalent_running_patch.return_value = False - with pytest.raises(TimeoutError): - ct.covalent_start(quiet=True) - - is_covalent_running_patch.return_value = True - with pytest.raises(TimeoutError): - ct.covalent_stop(quiet=True) - + # Simulate server stopped + mocker.patch( + "covalent._programmatic.commands.is_covalent_running", + return_value=False, + ) + ct.covalent_start() -def test_covalent_start_import_error(mocker): - """Test that `covalent_start` shows a warning if covalent_dispatcher is not installed""" - mocker.patch("click.Context.invoke", return_value=None) +def test_covalent_start_quiet(mocker): + """Test the `covalent_start` function without actually starting server.""" - mock_app_log_warning = mocker.patch("covalent._programmatic.commands.app_log.warning") + mocker.patch("subprocess.run") + # Simulate server running mocker.patch( "covalent._programmatic.commands.is_covalent_running", - side_effect=ImportError, + return_value=True, ) - ct.covalent_start(quiet=True) - mock_app_log_warning.assert_called_once_with(WARNING_MSG) - - -def test_covalent_stop_import_error(mocker): - """Test that `covalent_stop` shows a warning if covalent_dispatcher is not installed""" - - mocker.patch("click.Context.invoke", return_value=None) - - mock_app_log_warning = mocker.patch("covalent._programmatic.commands.app_log.warning") + # Simulate server stopped mocker.patch( "covalent._programmatic.commands.is_covalent_running", - side_effect=ImportError, + return_value=False, ) + ct.covalent_start(quiet=True) - ct.covalent_stop(quiet=True) - mock_app_log_warning.assert_called_once_with(WARNING_MSG) +def test_covalent_stop(mocker): + """Test the `covalent_start` function without actually starting server.""" -def test_is_covalent_running(): - """Test that the `is_covalent_running` function agrees with the CLI status check""" + mocker.patch("subprocess.run") - from covalent_dispatcher._cli import _is_server_running + # Simulate server running + mocker.patch( + "covalent._programmatic.commands.is_covalent_running", + return_value=True, + ) + ct.covalent_stop() - # TODO: Seems we can't start/stop the server in the test environment. - # Check here is not complete, but at least it is necessary. - assert ct.is_covalent_running() == _is_server_running() + # Simulate server stopped + mocker.patch( + "covalent._programmatic.commands.is_covalent_running", + return_value=False, + ) + ct.covalent_stop() -def test_is_covalent_running_import_error(mocker): - """Test that `is_covalent_running` returns False if covalent_dispatcher is not installed""" +def test_covalent_stop_quiet(mocker): + """Test the `covalent_start` function without actually starting server.""" - mock_app_log_warning = mocker.patch("covalent._programmatic.commands.app_log.warning") + mocker.patch("subprocess.run") - # _read_pid isn't importable + # Simulate server running mocker.patch( - "covalent_dispatcher._cli.service._read_pid", - side_effect=ImportError, + "covalent._programmatic.commands.is_covalent_running", + return_value=True, ) + ct.covalent_stop(quiet=True) - assert not ct.is_covalent_running() - mock_app_log_warning.assert_called_once_with(WARNING_MSG) + # Simulate server stopped + mocker.patch( + "covalent._programmatic.commands.is_covalent_running", + return_value=False, + ) + ct.covalent_stop(quiet=True) From cf6e48ae376211f9581488e99f59ad5b31c4ed52 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:34:02 +0000 Subject: [PATCH 40/42] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- covalent/_programmatic/commands.py | 6 +----- tests/covalent_tests/programmatic/commands_test.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 6064e6982..5ef92558d 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -29,11 +29,7 @@ app_log = logger.app_log -def _call_cli_command( - cmd: List[str], - *, - quiet: bool = False -) -> subprocess.CompletedProcess: +def _call_cli_command(cmd: List[str], *, quiet: bool = False) -> subprocess.CompletedProcess: """ Call a CLI command with the specified kwargs. diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py index cdd0a4210..86f5f513f 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -31,7 +31,7 @@ def test_is_covalent_running(mocker): mocker.patch("psutil.pid_exists", return_value=True) mocker.patch( "covalent._shared_files.config.get_config", - return_value={"port": 48008, "host": "localhost"} + return_value={"port": 48008, "host": "localhost"}, ) assert ct.is_covalent_running() From 0491ab1f3e38221c8ab0237af69afbb02419707d Mon Sep 17 00:00:00 2001 From: Ara Ghukasyan Date: Fri, 24 Nov 2023 12:14:31 -0500 Subject: [PATCH 41/42] add nopycln inside new tests --- tests/covalent_tests/programmatic/commands_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py index 86f5f513f..d7ccd1344 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -22,7 +22,7 @@ def test_is_covalent_running(mocker): """Check that `is_covalent_running` returns True when the server is running.""" try: - from covalent_dispatcher._cli.service import _read_pid + from covalent_dispatcher._cli.service import _read_pid # nopycln: import except (ModuleNotFoundError, ImportError): pytest.xfail("`covalent_dispatcher` not installed") @@ -45,7 +45,7 @@ def test_is_covalent_running_import_error(mocker): from covalent._programmatic.commands import _MISSING_SDK_WARNING try: - from covalent_dispatcher._cli.service import _read_pid + from covalent_dispatcher._cli.service import _read_pid # nopycln: import except (ModuleNotFoundError, ImportError): pytest.xfail("`covalent_dispatcher` not installed") From 2b7f126e0008f0b043fe18536a2f87ad1d2b1b53 Mon Sep 17 00:00:00 2001 From: Sankalp Sanand Date: Fri, 24 Nov 2023 13:30:52 -0500 Subject: [PATCH 42/42] renaming things a bit --- covalent/_programmatic/commands.py | 14 +++++--------- tests/covalent_tests/programmatic/commands_test.py | 9 ++++----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/covalent/_programmatic/commands.py b/covalent/_programmatic/commands.py index 5ef92558d..2ea5f68d3 100644 --- a/covalent/_programmatic/commands.py +++ b/covalent/_programmatic/commands.py @@ -28,6 +28,8 @@ app_log = logger.app_log +_MISSING_SERVER_WARNING = "Covalent has not been installed with the server component." + def _call_cli_command(cmd: List[str], *, quiet: bool = False) -> subprocess.CompletedProcess: """ @@ -49,9 +51,6 @@ def _call_cli_command(cmd: List[str], *, quiet: bool = False) -> subprocess.Comp return subprocess.run(cmd, check=True) -_MISSING_SDK_WARNING = "Covalent has not been installed with the server component." - - def is_covalent_running() -> bool: """ Check if the Covalent server is running. @@ -72,7 +71,7 @@ def is_covalent_running() -> bool: except ModuleNotFoundError: # If the covalent_dispatcher is not installed, assume Covalent is not running. - app_log.warning(_MISSING_SDK_WARNING) + app_log.warning(_MISSING_SERVER_WARNING) return False @@ -130,13 +129,10 @@ def covalent_start( } cmd = ["covalent", "start"] - for flag, value in flags.items(): - if value: - cmd.append(flag) + cmd.extend(flag for flag, value in flags.items() if value) for arg, value in args.items(): - cmd.append(arg) - cmd.append(str(value)) + cmd.extend((arg, str(value))) # Run the `covalent start [OPTIONS]` command. app_log.debug("Starting Covalent server programmatically...") diff --git a/tests/covalent_tests/programmatic/commands_test.py b/tests/covalent_tests/programmatic/commands_test.py index d7ccd1344..324984113 100644 --- a/tests/covalent_tests/programmatic/commands_test.py +++ b/tests/covalent_tests/programmatic/commands_test.py @@ -17,13 +17,14 @@ import pytest import covalent as ct +from covalent._programmatic.commands import _MISSING_SERVER_WARNING def test_is_covalent_running(mocker): """Check that `is_covalent_running` returns True when the server is running.""" try: from covalent_dispatcher._cli.service import _read_pid # nopycln: import - except (ModuleNotFoundError, ImportError): + except ModuleNotFoundError: pytest.xfail("`covalent_dispatcher` not installed") # Simulate server running @@ -42,11 +43,9 @@ def test_is_covalent_running(mocker): def test_is_covalent_running_import_error(mocker): """Check that `is_covalent_running` catches the `ModuleNotFoundError`.""" - from covalent._programmatic.commands import _MISSING_SDK_WARNING - try: from covalent_dispatcher._cli.service import _read_pid # nopycln: import - except (ModuleNotFoundError, ImportError): + except ModuleNotFoundError: pytest.xfail("`covalent_dispatcher` not installed") mocker.patch( @@ -57,7 +56,7 @@ def test_is_covalent_running_import_error(mocker): mock_app_log = mocker.patch("covalent._programmatic.commands.app_log") assert not ct.is_covalent_running() - mock_app_log.warning.assert_called_once_with(_MISSING_SDK_WARNING) + mock_app_log.warning.assert_called_once_with(_MISSING_SERVER_WARNING) def test_covalent_start(mocker):