-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
Expose programmatic equivalent of CLI commands (#1854)
* export `covalent_start` and `covalent_stop` * check server stopped * update changelog * move commands to main namespace * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * improve docstrings * fix `covalent_is_running` to return bool * reorder `covalent_is_running` conditions * `quiet` mode to suppress stdout; more docstrings * use poll function instead of while loop * explain package * add api docs entry * update api docs * restore import from `._programmatic` * update api docs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add test for new functions * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add test for `covalent_is_running` * removing covalent's dependency on dispatcher * ignore pip reqs in new package * refactor docstrings * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update docs * revert api docs * include 'Returns' in docstrings so maybe docs will render 🤷 pls * remove useless 'Returns' from docstrings 🤦♂️ * try autofunction refs to main namespace instead * revert using main namespace refs * add more logging and edit messages * refactor hanging tests * refactor tests into functional tests * Revert "refactor tests into functional tests" This reverts commit f308f8e. * create global var for timeout * use mock start and stop commands * renamed server check function and added import error check tests * None wasn't an acceptable value to redirect_stdout * refactor to use subprocess * refactor as multiple tests w/ patched start/stop * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add nopycln inside new tests * renaming things a bit --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: sankalp <[email protected]>
1 parent
21f8c0c
commit 85bd30d
Showing
8 changed files
with
370 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# 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. | ||
|
||
""" | ||
NOTE: This package exists to avoid circular imports that would be encountered if | ||
`covalent` imports from `covalent_dispatcher._cli`. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
# 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 subprocess | ||
from typing import List, Optional | ||
|
||
import psutil | ||
|
||
from .._shared_files import logger | ||
from .._shared_files.config import get_config | ||
|
||
__all__ = ["is_covalent_running", "covalent_start", "covalent_stop"] | ||
|
||
|
||
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: | ||
""" | ||
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) | ||
|
||
|
||
def is_covalent_running() -> bool: | ||
""" | ||
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 | ||
|
||
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 ModuleNotFoundError: | ||
# If the covalent_dispatcher is not installed, assume Covalent is not running. | ||
app_log.warning(_MISSING_SERVER_WARNING) | ||
return False | ||
|
||
|
||
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, | ||
*, | ||
quiet: bool = False, | ||
) -> None: | ||
""" | ||
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 :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 :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`. | ||
""" | ||
|
||
if is_covalent_running(): | ||
msg = "Covalent server is already running." | ||
if not quiet: | ||
print(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"] | ||
cmd.extend(flag for flag, value in flags.items() if value) | ||
|
||
for arg, value in args.items(): | ||
cmd.extend((arg, 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: | ||
""" | ||
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 :code:`False`. | ||
""" | ||
|
||
if not is_covalent_running(): | ||
msg = "Covalent server is not running." | ||
if not quiet: | ||
print(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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
# 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 | ||
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: | ||
pytest.xfail("`covalent_dispatcher` not installed") | ||
|
||
# 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"}, | ||
) | ||
assert ct.is_covalent_running() | ||
|
||
# Simulate server stopped | ||
mocker.patch("covalent_dispatcher._cli.service._read_pid", return_value=-1) | ||
assert not ct.is_covalent_running() | ||
|
||
|
||
def test_is_covalent_running_import_error(mocker): | ||
"""Check that `is_covalent_running` catches the `ModuleNotFoundError`.""" | ||
try: | ||
from covalent_dispatcher._cli.service import _read_pid # nopycln: import | ||
except ModuleNotFoundError: | ||
pytest.xfail("`covalent_dispatcher` not installed") | ||
|
||
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_SERVER_WARNING) | ||
|
||
|
||
def test_covalent_start(mocker): | ||
"""Test the `covalent_start` function without actually starting server.""" | ||
|
||
mocker.patch("subprocess.run") | ||
|
||
# Simulate server running | ||
mocker.patch( | ||
"covalent._programmatic.commands.is_covalent_running", | ||
return_value=True, | ||
) | ||
ct.covalent_start() | ||
|
||
# Simulate server stopped | ||
mocker.patch( | ||
"covalent._programmatic.commands.is_covalent_running", | ||
return_value=False, | ||
) | ||
ct.covalent_start() | ||
|
||
|
||
def test_covalent_start_quiet(mocker): | ||
"""Test the `covalent_start` function without actually starting server.""" | ||
|
||
mocker.patch("subprocess.run") | ||
|
||
# Simulate server running | ||
mocker.patch( | ||
"covalent._programmatic.commands.is_covalent_running", | ||
return_value=True, | ||
) | ||
ct.covalent_start(quiet=True) | ||
|
||
# Simulate server stopped | ||
mocker.patch( | ||
"covalent._programmatic.commands.is_covalent_running", | ||
return_value=False, | ||
) | ||
ct.covalent_start(quiet=True) | ||
|
||
|
||
def test_covalent_stop(mocker): | ||
"""Test the `covalent_start` function without actually starting server.""" | ||
|
||
mocker.patch("subprocess.run") | ||
|
||
# Simulate server running | ||
mocker.patch( | ||
"covalent._programmatic.commands.is_covalent_running", | ||
return_value=True, | ||
) | ||
ct.covalent_stop() | ||
|
||
# Simulate server stopped | ||
mocker.patch( | ||
"covalent._programmatic.commands.is_covalent_running", | ||
return_value=False, | ||
) | ||
ct.covalent_stop() | ||
|
||
|
||
def test_covalent_stop_quiet(mocker): | ||
"""Test the `covalent_start` function without actually starting server.""" | ||
|
||
mocker.patch("subprocess.run") | ||
|
||
# Simulate server running | ||
mocker.patch( | ||
"covalent._programmatic.commands.is_covalent_running", | ||
return_value=True, | ||
) | ||
ct.covalent_stop(quiet=True) | ||
|
||
# Simulate server stopped | ||
mocker.patch( | ||
"covalent._programmatic.commands.is_covalent_running", | ||
return_value=False, | ||
) | ||
ct.covalent_stop(quiet=True) |