Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lean/components/cloud/cloud_project_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from lean.components.util.path_manager import PathManager
from lean.components.util.project_manager import ProjectManager
from lean.models.api import QCProject
from lean.models.errors import PushFailedError


class CloudProjectManager:
Expand Down Expand Up @@ -68,7 +69,8 @@ def get_cloud_project(self, input: str, push: bool) -> QCProject:
local_path = Path.cwd() / input
if self._project_config_manager.try_get_project_config(local_path):
if push:
self._push_manager.push_projects([local_path])
if not self._push_manager.push_projects([local_path]):
raise PushFailedError(input)

cloud_id = self._project_config_manager.get_project_config(local_path).get("cloud-id")
if cloud_id is None:
Expand All @@ -92,7 +94,8 @@ def get_cloud_project(self, input: str, push: bool) -> QCProject:
if push:
local_path = self._project_manager.get_local_project_path(cloud_project.name, cloud_project.projectId)
if local_path.exists():
self._push_manager.push_projects([local_path])
if not self._push_manager.push_projects([local_path]):
raise PushFailedError(input)
return self._api_client.projects.get(cloud_project.projectId, organization_id)

return cloud_project
Expand Down
10 changes: 8 additions & 2 deletions lean/components/cloud/push_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,20 @@ def push_project(self, project: Path, encryption_action: Optional[ActionType]=No
libraries = self._project_manager.get_project_libraries(project)
self.push_projects([project], libraries, encryption_action, encryption_key, force)

def push_projects(self, projects_to_push: List[Path], associated_libraries_to_push: Optional[List[Path]]=[], encryption_action: Optional[ActionType]=None, encryption_key: Optional[Path]=None, force: Optional[bool]=False) -> None:
def push_projects(self, projects_to_push: List[Path], associated_libraries_to_push: Optional[List[Path]]=[], encryption_action: Optional[ActionType]=None, encryption_key: Optional[Path]=None, force: Optional[bool]=False) -> bool:
"""Pushes the given projects from the local drive to the cloud.

It will also push every library referenced by each project and add or remove references.

:param projects_to_push: a list of directories containing the local projects that need to be pushed
:return: True if every project and library was pushed, False if any of them could not be pushed
"""
all_projects_to_push = associated_libraries_to_push + projects_to_push

if len(all_projects_to_push) == 0:
return
return True

all_pushed = True

organization_id = self._organization_manager.try_get_working_organization_id()

Expand All @@ -85,6 +88,9 @@ def push_projects(self, projects_to_push: List[Path], associated_libraries_to_pu
self._logger.warn(f"Cannot push '{relative_path}': {ex}")
if "write permission" in str(ex).lower():
self._logger.info("Please pull any required changes and push with --force")
all_pushed = False

return all_pushed

def _get_local_libraries_cloud_ids(self, project_dir: Path) -> List[int]:
project_config = self._project_config_manager.get_project_config(project_dir)
Expand Down
11 changes: 11 additions & 0 deletions lean/models/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,14 @@ def __init__(self, response=None) -> None:
"https://www.lean.io/docs/v2/lean-cli/initialization/authenticating-accounts#02-Log-In")

self.response = response


class PushFailedError(Exception):
"""An error indicating that local changes the user asked to push could not be pushed to the cloud."""

def __init__(self, project: str) -> None:
"""Creates a new PushFailedError instance.

:param project: the name or id of the project that could not be pushed
"""
super().__init__(f"Cannot push '{project}' to the cloud, aborting so the cloud does not run outdated files")
27 changes: 27 additions & 0 deletions tests/commands/cloud/test_backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
# limitations under the License.

from datetime import datetime
from pathlib import Path
from unittest import mock

from click.testing import CliRunner

from lean.commands import lean
from lean.container import container
from lean.models.errors import PushFailedError
from lean.models.api import QCBacktest
from tests.test_helpers import create_api_project, create_fake_lean_cli_directory
from tests.conftest import initialize_container
Expand Down Expand Up @@ -200,6 +203,30 @@ def test_cloud_backtest_pushes_nothing_when_project_does_not_exist_locally() ->
push_manager.push_projects.assert_not_called()


def test_cloud_backtest_aborts_when_push_of_project_with_cloud_id_fails() -> None:
create_fake_lean_cli_directory()

project = create_api_project(1, "Python Project")

api_client = mock.Mock()
api_client.projects.get_all.return_value = [project]
api_client.projects.get.return_value = project
api_client.projects.update.side_effect = RuntimeError("You don't have write permission for this project")

cloud_runner = mock.Mock()
initialize_container(api_client_to_use=api_client, cloud_runner_to_use=cloud_runner)
container.project_config_manager.get_project_config(Path.cwd() / "Python Project").set("cloud-id", project.projectId)

result = CliRunner().invoke(lean, ["cloud", "backtest", "Python Project", "--push"])

assert result.exit_code != 0
assert isinstance(result.exception, PushFailedError)

api_client.projects.update.assert_called_once()
cloud_runner.compile_project.assert_not_called()
cloud_runner.run_backtest.assert_not_called()


def test_cloud_backtest_aborts_when_backtest_fails() -> None:
create_fake_lean_cli_directory()

Expand Down
45 changes: 45 additions & 0 deletions tests/components/cloud/test_cloud_project_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
from pathlib import Path
from unittest import mock

import pytest

from lean.container import container
from lean.models.errors import PushFailedError
from tests.test_helpers import create_api_project, create_fake_lean_cli_directory
from tests.conftest import initialize_container

Expand Down Expand Up @@ -49,3 +52,45 @@ def test_get_cloud_project_pushing_new_project():

api_client.projects.get.assert_called_with(cloud_project.projectId, "abc")
push_manager.push_projects.assert_called_once_with([Path.cwd() / "Python Project"])


def test_get_cloud_project_raises_when_push_of_project_with_cloud_id_fails() -> None:
create_fake_lean_cli_directory()

cloud_project = create_api_project(20, "Python Project")

api_client = mock.Mock()
api_client.projects.get.return_value = cloud_project

push_manager = mock.Mock()
push_manager.push_projects = mock.Mock(return_value=False)

initialize_container(api_client_to_use=api_client, push_manager_to_use=push_manager)
container.project_config_manager.get_project_config(Path.cwd() / "Python Project").set("cloud-id", cloud_project.projectId)

with pytest.raises(PushFailedError):
container.cloud_project_manager.get_cloud_project("Python Project", push=True)

push_manager.push_projects.assert_called_once_with([Path.cwd() / "Python Project"])
api_client.projects.get.assert_not_called()


def test_get_cloud_project_raises_when_push_of_project_given_by_id_fails() -> None:
create_fake_lean_cli_directory()

cloud_project = create_api_project(20, "Python Project")

api_client = mock.Mock()
api_client.projects.get_all.return_value = [cloud_project]

push_manager = mock.Mock()
push_manager.push_projects = mock.Mock(return_value=False)

initialize_container(api_client_to_use=api_client, push_manager_to_use=push_manager)
container.project_config_manager.get_project_config(Path.cwd() / "Python Project").set("cloud-id", cloud_project.projectId)

with pytest.raises(PushFailedError):
container.cloud_project_manager.get_cloud_project(str(cloud_project.projectId), push=True)

push_manager.push_projects.assert_called_once_with([Path.cwd() / "Python Project"])
api_client.projects.get.assert_not_called()
33 changes: 32 additions & 1 deletion tests/components/util/test_push_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,35 @@ def test_push_projects_updates_name_locally_if_updated_by_cloud() -> None:
push_manager.push_projects([project_path])

assert not (Path.cwd() / project_path).exists()
assert (Path.cwd() / project_name_renamed_by_cloud).exists()
assert (Path.cwd() / project_name_renamed_by_cloud).exists()

def test_push_projects_returns_true_when_every_project_is_pushed() -> None:
create_fake_lean_cli_directory()

project_path = Path.cwd() / "Python Project"
cloud_project = create_api_project(1, project_path.name)
container.project_config_manager.get_project_config(project_path).set("cloud-id", cloud_project.projectId)

api_client = mock.Mock()
api_client.projects.get = mock.MagicMock(return_value=cloud_project)

push_manager = _create_push_manager(api_client, container.project_manager)

assert push_manager.push_projects([project_path])
api_client.projects.update.assert_called_once()


def test_push_projects_returns_false_when_a_project_with_cloud_id_cannot_be_pushed() -> None:
create_fake_lean_cli_directory()

project_path = Path.cwd() / "Python Project"
cloud_project = create_api_project(1, project_path.name)
container.project_config_manager.get_project_config(project_path).set("cloud-id", cloud_project.projectId)

api_client = mock.Mock()
api_client.projects.get = mock.MagicMock(return_value=cloud_project)
api_client.projects.update = mock.MagicMock(side_effect=RuntimeError("You don't have write permission"))

push_manager = _create_push_manager(api_client, container.project_manager)

assert not push_manager.push_projects([project_path])
Loading