Skip to content

Commit

Permalink
fix(CLI): Ask the user to create the workspace.yaml if not found (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
qgerome authored Jan 11, 2024
1 parent 2c80d32 commit 1da1a04
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 16 deletions.
38 changes: 38 additions & 0 deletions openhexa/cli/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ def graphql(config, query: str, variables=None, token=None):
return data["data"]


def get_skeleton_dir():
"""Get the path to the skeleton directory."""
return Path(__file__).parent / "skeleton"


def get_workspace(config, slug: str, token: str):
"""Get a single workspace."""
return graphql(
Expand Down Expand Up @@ -230,6 +235,39 @@ def ensure_is_pipeline_dir(pipeline_path: str):
return True


# This is easier to mock in the tests than trying to mock click.confirm
def ask_pipeline_config_creation():
"""Mockable function to ask the user if he wants to create a pipeline config file.
Returns
-------
bool: True if the user wants to create a pipeline config file, False otherwise.
"""
return click.confirm(
"No workspace.yaml file found. Do you want to create one?",
default=True,
)


def ensure_pipeline_config_exists(pipeline_path: Path):
"""Ensure that there is a workspace.yaml file in the directory. If it does not exist, it asks the user if he wants to create it.
Args:
pipeline_path (Path): Base directory of the pipeline
"""
if (pipeline_path / "workspace.yaml").exists():
return True

if ask_pipeline_config_creation():
content = open(get_skeleton_dir() / "workspace.yaml").read()
with open(pipeline_path / "workspace.yaml", "w") as f:
f.write(content)
return True
else:
raise Exception("No workspace.yaml file found")


def upload_pipeline(config, pipeline_directory_path: typing.Union[str, Path]):
"""Upload the pipeline contained in the provided directory using the GraphQL API.
Expand Down
29 changes: 18 additions & 11 deletions openhexa/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
create_pipeline,
delete_pipeline,
ensure_is_pipeline_dir,
ensure_pipeline_config_exists,
get_pipeline,
get_skeleton_dir,
get_workspace,
is_debug,
list_pipelines,
Expand Down Expand Up @@ -178,7 +180,7 @@ def pipelines_init(name: str):
sys.exit(1)

# Load samples
sample_directory_path = Path(__file__).parent / Path("skeleton")
sample_directory_path = get_skeleton_dir()
with open(sample_directory_path / Path(".gitignore")) as sample_ignore_file:
sample_ignore_content = sample_ignore_file.read()
with open(sample_directory_path / Path("pipeline.py")) as sample_pipeline_file:
Expand All @@ -193,12 +195,12 @@ def pipelines_init(name: str):

# Create directory
new_pipeline_path.mkdir(exist_ok=False)
(new_pipeline_path / Path("workspace")).mkdir(exist_ok=False)
with open(new_pipeline_path / Path(".gitignore"), "w") as ignore_file:
(new_pipeline_path / "workspace").mkdir(exist_ok=False)
with open(new_pipeline_path / ".gitignore", "w") as ignore_file:
ignore_file.write(sample_ignore_content)
with open(new_pipeline_path / Path("pipeline.py"), "w") as pipeline_file:
with open(new_pipeline_path / "pipeline.py", "w") as pipeline_file:
pipeline_file.write(sample_pipeline_content)
with open(new_pipeline_path / Path("workspace.yaml"), "w") as workspace_file:
with open(new_pipeline_path / "workspace.yaml", "w") as workspace_file:
workspace_file.write(sample_workspace_content)

# Success
Expand Down Expand Up @@ -231,6 +233,7 @@ def pipelines_push(path: str):
sys.exit(1)

ensure_is_pipeline_dir(path)
ensure_pipeline_config_exists(Path(path))

try:
pipeline = import_pipeline(path)
Expand Down Expand Up @@ -342,7 +345,12 @@ def pipelines_delete(code: str):
default=None,
help="Configuration JSON file",
)
@click.option("--image", type=str, help="Docker image to use", default="blsq/openhexa-base-notebook:latest")
@click.option(
"--image",
type=str,
help="Docker image to use",
default="blsq/openhexa-base-notebook:latest",
)
def pipelines_run(
path: str,
image: str,
Expand All @@ -352,14 +360,14 @@ def pipelines_run(
"""Run a pipeline locally."""
from subprocess import Popen

path = Path(path)
user_config = open_config()
ensure_is_pipeline_dir(path)
ensure_pipeline_config_exists(path)
env_vars = get_local_workspace_config(path)

env_vars = get_local_workspace_config(Path(path))

# Prepare the mount for the workspace's files
# # Prepare the mount for the workspace's files
mount_files_path = Path(env_vars["WORKSPACE_FILES_PATH"]).absolute()

cmd = [
"docker",
"run",
Expand Down Expand Up @@ -405,7 +413,6 @@ def pipelines_run(
cmd,
close_fds=True,
)

return proc.wait()


Expand Down
7 changes: 3 additions & 4 deletions openhexa/sdk/pipelines/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def get_local_workspace_config(path: Path):
# (We will have to find another approach for tests or running the pipeline using the CLI)
local_workspace_config_path = path / Path("workspace.yaml")
if not local_workspace_config_path.exists():
raise ValueError(
raise FileNotFoundError(
"To work with pipelines locally, you need a workspace.yaml file in the same directory as your pipeline file"
)

Expand Down Expand Up @@ -57,9 +57,8 @@ def get_local_workspace_config(path: Path):
try:
files_path = path / Path(local_workspace_config["files"]["path"])
if not files_path.exists():
raise LocalWorkspaceConfigError(
f"The {files_path} files path does not exist. " f"Did you forget to create it?"
)
# Let's create the folder if it doesn't exist
files_path.mkdir(parents=True)
env_vars["WORKSPACE_FILES_PATH"] = str(files_path.resolve())
except KeyError:
exception_message = (
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ openhexa = "openhexa.cli:app"


[project.optional-dependencies]
dev = [ "ruff~=0.0.278", "pytest~=7.3.0", "build>=0.10,<1.1", "pytest-cov>=4.0,<4.2" , "pre-commit"]
dev = [ "ruff~=0.1.11", "pytest~=7.3.0", "build>=0.10,<1.1", "pytest-cov>=4.0,<4.2" , "pre-commit"]
examples= ["geopandas~=0.12.2", "pandas~=2.0.0", "rasterio~=1.3.6", "rasterstats>=0.18,<0.20", "setuptools>=67.6.1,<69.1.0", "SQLAlchemy~=2.0.9", "psycopg2"]


Expand Down
88 changes: 88 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""CLI test module."""

from configparser import ConfigParser
from pathlib import Path
from tempfile import mkdtemp
from unittest import TestCase
from unittest.mock import MagicMock, patch

from click.testing import CliRunner

from openhexa.cli.cli import pipelines_run


class CliRunTest(TestCase):
"""Test the CLI."""

runner = None
user_config = None

def setUp(self):
"""Configure the CLI runner and the user config."""
self.runner = CliRunner()
self.user_config = ConfigParser()
self.user_config.read_string(
"""
[openhexa]
url=https://test.openhexa.org
current_workspace=test_workspace
[workspaces]
test_workspace = WORKSPACE_TOKEN
"""
)
self.patch_user_config = patch("openhexa.cli.cli.open_config", return_value=self.user_config)
self.patch_user_config.start()
return super().setUp()

def tearDown(self) -> None:
"""Tear down the CLI runner and the user config.
Returns
-------
None
"""
self.patch_user_config.stop()
return super().tearDown()

@patch("openhexa.cli.api.ask_pipeline_config_creation", return_value=True)
def test_no_pipeline(self, *args):
"""Test running a pipeline without a pipeline.py file."""
with self.runner.isolated_filesystem():
result = self.runner.invoke(pipelines_run, [mkdtemp()])
assert result.exit_code == 1
self.assertTrue("does not contain a pipeline.py file" in str(result.exception))

@patch("openhexa.cli.api.ask_pipeline_config_creation", return_value=True)
def test_no_config_file(self, mock_ask_config):
"""Test running a pipeline without a workspace.yaml file.
It should ask the user if they want to create the config file.
Two tests are performed:
- The user accepts the config creation
- The user refuses the config creation
"""
with self.runner.isolated_filesystem() as tmp:
with open("pipeline.py", "w") as f:
f.write("")

self.assertFalse((Path(tmp) / "workspace.yaml").exists())
with patch("subprocess.Popen") as mock_popen:
mock_process = MagicMock()
mock_process.wait.return_value = MagicMock()

mock_popen.return_value = mock_process

# First without accepting the config creation
mock_ask_config.return_value = False
result = self.runner.invoke(pipelines_run, [tmp])
self.assertEqual(result.exit_code, 1)
self.assertFalse((Path(tmp) / "workspace.yaml").exists())
mock_popen.assert_not_called()

# This time we accept the config creation
mock_ask_config.return_value = True
result = self.runner.invoke(pipelines_run, [tmp])
self.assertEqual(result.exit_code, 0)
self.assertTrue((Path(tmp) / "workspace.yaml").exists())
mock_popen.assert_called_once()

0 comments on commit 1da1a04

Please sign in to comment.