diff --git a/lean/components/config/project_config_manager.py b/lean/components/config/project_config_manager.py index e66c2875..e68f26c4 100644 --- a/lean/components/config/project_config_manager.py +++ b/lean/components/config/project_config_manager.py @@ -77,8 +77,8 @@ def get_project_id_from_project_config(self, project_directory: Path) -> int: it indicates the directory is unavailable. Returns: - int: Returns the 'cloud-id' if available. - If 'cloud-id' is missing, returns the negative of 'local-id'. + int: Returns the 'cloud-id' if available and the project it refers to still exists in the cloud. + Otherwise returns the negative of 'local-id'. If neither is found nor if project_directory is None, returns -1. """ if project_directory is None: @@ -87,14 +87,30 @@ def get_project_id_from_project_config(self, project_directory: Path) -> int: project_config = self.get_project_config(project_directory) cloud_id = project_config.get("cloud-id") - if cloud_id is not None: + if cloud_id is not None and self._cloud_project_exists(cloud_id): return cloud_id - local_id = project_config.get("local-id") - if local_id is not None: - return -local_id # Local ID must be negative. + return -abs(int(project_config.get("local-id", 1))) - return -1 # Return -1 if no valid IDs are found + def _cloud_project_exists(self, cloud_id: int) -> bool: + """Returns whether a project still exists in the cloud. + + A 'cloud-id' left behind by a project that no longer exists in the cloud identifies nothing, + and neither does one this user cannot reach, so it cannot be used as the id of the project that runs. + + :param cloud_id: the cloud id to look up + :return: True if the cloud project exists, False if it does not or if it could not be looked up + """ + from lean.container import container + + try: + organization_id = container.organization_manager.try_get_working_organization_id() + container.api_client.projects.get(cloud_id, organization_id) + return True + except Exception as e: + container.logger.debug(f"ProjectConfigManager._cloud_project_exists(): cloud project {cloud_id} " + f"is unavailable, falling back to the local id: {e}") + return False def get_latest_live_directory(self, project_directory: Path) -> Path: """Returns the path of the latest live directory. diff --git a/tests/components/config/test_lean_config_manager.py b/tests/components/config/test_lean_config_manager.py index fdb4cfa0..e55aa473 100644 --- a/tests/components/config/test_lean_config_manager.py +++ b/tests/components/config/test_lean_config_manager.py @@ -27,6 +27,7 @@ from lean.components.util.xml_manager import XMLManager from lean.container import container from lean.models.utils import DebuggingMethod +from tests.conftest import initialize_container from tests.test_helpers import create_fake_lean_cli_directory @@ -388,6 +389,21 @@ def test_get_complete_lean_config_returns_dict_with_all_keys_removed_in_clean_le assert key in config +def test_get_complete_lean_config_sets_the_project_id_of_the_project_being_run() -> None: + create_fake_lean_cli_directory() + # the id of the project being run is only used when the Lean config does not set one + (Path.cwd() / "lean.json").write_text('{"data-folder": "data", "organization-id": "abc"}', encoding="utf-8") + initialize_container() + + project_directory = Path.cwd() / "Python Project" + ProjectConfigManager(XMLManager()).get_project_config(project_directory).set("cloud-id", 1234) + + manager = _create_lean_config_manager() + config = manager.get_complete_lean_config("backtesting", project_directory / "main.py", None) + + assert config["project-id"] == 1234 + + def test_get_complete_lean_config_sets_environment() -> None: create_fake_lean_cli_directory() diff --git a/tests/components/config/test_project_config_manager.py b/tests/components/config/test_project_config_manager.py index 48bd26ec..0c69d7d2 100644 --- a/tests/components/config/test_project_config_manager.py +++ b/tests/components/config/test_project_config_manager.py @@ -12,8 +12,11 @@ # limitations under the License. from pathlib import Path +from unittest import mock from lean.components.config.project_config_manager import ProjectConfigManager +from lean.models.errors import RequestFailedError +from tests.conftest import initialize_container from lean.components.util.xml_manager import XMLManager from lean.models.utils import CSharpLibrary from tests.test_helpers import create_fake_lean_cli_directory @@ -118,3 +121,46 @@ def test_get_csharp_libraries_skips_invalid_package_reference_tags() -> None: assert len(libraries) == 1 assert CSharpLibrary(name="QuantConnect.Lean", version="2.5.11586") in libraries + + +def test_get_project_id_from_project_config_returns_negative_local_id() -> None: + create_fake_lean_cli_directory() + + project_config_manager = ProjectConfigManager(XMLManager()) + project_directory = Path.cwd() / "Python Project" + local_id = project_config_manager.get_local_id(project_directory) + + assert project_config_manager.get_project_id_from_project_config(project_directory) == -local_id + + +def test_get_project_id_from_project_config_returns_cloud_id_when_the_cloud_project_exists() -> None: + create_fake_lean_cli_directory() + initialize_container() + + project_config_manager = ProjectConfigManager(XMLManager()) + project_directory = Path.cwd() / "Python Project" + project_config_manager.get_project_config(project_directory).set("cloud-id", 1234) + + assert project_config_manager.get_project_id_from_project_config(project_directory) == 1234 + + +def test_get_project_id_from_project_config_falls_back_to_local_id_when_the_cloud_project_is_gone() -> None: + """A cloud-id left behind by a project deleted in the cloud identifies nothing.""" + create_fake_lean_cli_directory() + container = initialize_container() + container.api_client.projects.get.side_effect = RequestFailedError(mock.MagicMock()) + + project_config_manager = ProjectConfigManager(XMLManager()) + project_directory = Path.cwd() / "Python Project" + project_config_manager.get_project_config(project_directory).set("cloud-id", 1234) + local_id = project_config_manager.get_local_id(project_directory) + + assert project_config_manager.get_project_id_from_project_config(project_directory) == -local_id + + +def test_get_project_id_from_project_config_returns_minus_one_without_a_project_directory() -> None: + create_fake_lean_cli_directory() + + project_config_manager = ProjectConfigManager(XMLManager()) + + assert project_config_manager.get_project_id_from_project_config(None) == -1