-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #151 from jhonabreul/feat-cloud-project-delete-com…
…mand Add delete project command
- Loading branch information
Showing
6 changed files
with
247 additions
and
10 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,61 @@ | ||
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. | ||
# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# 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. | ||
|
||
from pathlib import Path | ||
|
||
import click | ||
|
||
from lean.click import LeanCommand | ||
from lean.container import container | ||
|
||
|
||
@click.command(cls=LeanCommand) | ||
@click.argument("project", type=str) | ||
def delete_project(project: str) -> None: | ||
"""Delete a project locally and in the cloud if it exists. | ||
The project is selected by name or cloud id. | ||
""" | ||
# Remove project from cloud | ||
api_client = container.api_client() | ||
all_projects = api_client.projects.get_all() | ||
project_manager = container.project_manager() | ||
logger = container.logger() | ||
|
||
project_id = None | ||
try: | ||
project_id = int(project) | ||
except ValueError: | ||
pass | ||
|
||
projects = [] | ||
try: | ||
projects = project_manager.get_projects_by_name_or_id(all_projects, project_id or project) | ||
except RuntimeError: | ||
# If searching by id, we cannot try lo locate the project locally | ||
if project_id is not None: | ||
raise RuntimeError(f"The project with ID {project_id} was not found in the cloud.") | ||
|
||
# The project might only be local, look for the path | ||
logger.info(f"The project {project} was not found in the cloud. It will be removed locally if it exists.") | ||
|
||
full_project = next(iter(projects), None) | ||
|
||
if full_project is not None: | ||
api_client.projects.delete(full_project.projectId) | ||
|
||
# Remove project locally | ||
project_path = full_project.name if full_project is not None else project | ||
project_manager.delete_project(Path(project_path)) | ||
|
||
logger.info(f"Successfully deleted project '{project_path}'") |
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,117 @@ | ||
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. | ||
# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# 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. | ||
|
||
from pathlib import Path | ||
from typing import List | ||
from unittest import mock | ||
|
||
import pytest | ||
from click.testing import CliRunner | ||
|
||
from lean.commands import lean | ||
from lean.components.api.project_client import ProjectClient | ||
from lean.models.api import QCProject | ||
from tests.test_helpers import create_fake_lean_cli_directory, create_api_project | ||
|
||
|
||
def assert_project_exists(path: str) -> None: | ||
project_dir = (Path.cwd() / path) | ||
|
||
assert project_dir.exists() | ||
assert (project_dir / "main.py").exists() | ||
assert (project_dir / "research.ipynb").exists() | ||
|
||
|
||
def assert_project_does_not_exist(path: str) -> None: | ||
project_dir = (Path.cwd() / path) | ||
assert not project_dir.exists() | ||
|
||
|
||
def create_cloud_projects(count: int = 10) -> List[QCProject]: | ||
return [create_api_project(i, f"Python Project {i}") for i in range(1, count + 1)] | ||
|
||
|
||
def test_delete_project_locally_that_does_not_have_cloud_counterpart() -> None: | ||
create_fake_lean_cli_directory() | ||
|
||
path = "Python Project" | ||
assert_project_exists(path) | ||
|
||
cloud_projects = create_cloud_projects() | ||
assert not any(project.name == path for project in cloud_projects) | ||
|
||
with mock.patch.object(ProjectClient, 'get_all', return_value=cloud_projects) as mock_get_all,\ | ||
mock.patch.object(ProjectClient, 'delete', return_value=None) as mock_delete: | ||
result = CliRunner().invoke(lean, ["delete-project", path]) | ||
assert result.exit_code == 0 | ||
|
||
mock_get_all.assert_called_once() | ||
mock_delete.assert_not_called() | ||
assert_project_does_not_exist(path) | ||
|
||
|
||
@pytest.mark.parametrize("name_or_id", ["Python Project", "11"]) | ||
def test_delete_project_deletes_in_cloud(name_or_id: str) -> None: | ||
create_fake_lean_cli_directory() | ||
|
||
path = "Python Project" | ||
assert_project_exists(path) | ||
|
||
cloud_projects = create_cloud_projects(10) | ||
assert not any(project.name == path for project in cloud_projects) | ||
|
||
cloud_project = create_api_project(len(cloud_projects) + 1, path) | ||
cloud_projects.append(cloud_project) | ||
|
||
with mock.patch.object(ProjectClient, 'get_all', return_value=cloud_projects) as mock_get_all,\ | ||
mock.patch.object(ProjectClient, 'delete', return_value=None) as mock_delete: | ||
result = CliRunner().invoke(lean, ["delete-project", name_or_id]) | ||
assert result.exit_code == 0 | ||
|
||
mock_get_all.assert_called_once() | ||
mock_delete.assert_called_once_with(cloud_project.projectId) | ||
assert_project_does_not_exist(cloud_project.name) | ||
|
||
|
||
def test_delete_project_aborts_when_path_does_not_exist() -> None: | ||
create_fake_lean_cli_directory() | ||
|
||
path = "Non Existing Project" | ||
assert_project_does_not_exist(path) | ||
|
||
with mock.patch.object(ProjectClient, 'get_all', return_value=create_cloud_projects()) as mock_get_all,\ | ||
mock.patch.object(ProjectClient, 'delete', return_value=None) as mock_delete: | ||
result = CliRunner().invoke(lean, ["delete-project", path]) | ||
assert result.exit_code != 0 | ||
|
||
mock_get_all.assert_called_once() | ||
mock_delete.assert_not_called() | ||
|
||
|
||
def test_delete_project_by_id_aborts_when_not_found_in_cloud() -> None: | ||
create_fake_lean_cli_directory() | ||
|
||
path = "Python Project" | ||
assert_project_exists(path) | ||
|
||
cloud_projects = create_cloud_projects(10) | ||
project_id = str(len(cloud_projects) + 1) | ||
|
||
with mock.patch.object(ProjectClient, 'get_all', return_value=[]) as mock_get_all,\ | ||
mock.patch.object(ProjectClient, 'delete', return_value=None) as mock_delete: | ||
result = CliRunner().invoke(lean, ["delete-project", project_id]) | ||
assert result.exit_code != 0 | ||
|
||
mock_get_all.assert_called_once() | ||
mock_delete.assert_not_called() | ||
assert_project_exists(path) |