diff --git a/lean/commands/backtest.py b/lean/commands/backtest.py index c56981c2..4899496e 100644 --- a/lean/commands/backtest.py +++ b/lean/commands/backtest.py @@ -17,9 +17,8 @@ from pathlib import Path from typing import Optional import click - from lean.click import LeanCommand, PathParameter -from lean.constants import DEFAULT_ENGINE_IMAGE +from lean.constants import DEFAULT_ENGINE_IMAGE, LEAN_ROOT_PATH from lean.container import container from lean.models.api import QCMinimalOrganization from lean.models.utils import DebuggingMethod @@ -61,7 +60,7 @@ def _migrate_python_pycharm(project_dir: Path) -> None: library_dir = None for mapping in path_mappings.findall(".//mapping"): - if mapping.get("local-root") == "$PROJECT_DIR$" and mapping.get("remote-root") == "/Lean/Launcher/bin/Debug": + if mapping.get("local-root") == "$PROJECT_DIR$" and mapping.get("remote-root") == LEAN_ROOT_PATH: mapping.set("remote-root", "/LeanCLI") made_changes = True @@ -108,7 +107,7 @@ def _migrate_python_vscode(project_dir: Path) -> None: library_dir = None for mapping in config["pathMappings"]: - if mapping["localRoot"] == "${workspaceFolder}" and mapping["remoteRoot"] == "/Lean/Launcher/bin/Debug": + if mapping["localRoot"] == "${workspaceFolder}" and mapping["remoteRoot"] == LEAN_ROOT_PATH: mapping["remoteRoot"] = "/LeanCLI" made_changes = True diff --git a/lean/commands/cloud/backtest.py b/lean/commands/cloud/backtest.py index ed7b3c31..75885d95 100644 --- a/lean/commands/cloud/backtest.py +++ b/lean/commands/cloud/backtest.py @@ -13,12 +13,11 @@ import webbrowser from typing import Optional - import click - from lean.click import LeanCommand from lean.container import container - +from pathlib import Path +from lean.models.errors import RequestFailedError @click.command(cls=LeanCommand) @click.argument("project", type=str) @@ -43,7 +42,16 @@ def backtest(project: str, name: Optional[str], push: bool, open_browser: bool) logger = container.logger() cloud_project_manager = container.cloud_project_manager() - cloud_project = cloud_project_manager.get_cloud_project(project, push) + try: + cloud_project = cloud_project_manager.get_cloud_project(project, push) + except RuntimeError as e: + if cloud_project_manager._project_config_manager.try_get_project_config(Path.cwd() / project, + cloud_project_manager._path_manager): + error_message = f'No project with the given name or id "{project}" found in your cloud projects.' + error_message += f" Please use `lean cloud backtest --push {project}` to backtest in cloud." + else: + error_message = f'No project with the given name or id "{project}" found in your cloud or local projects.' + raise RuntimeError(error_message) if name is None: name = container.name_generator().generate_name() diff --git a/lean/commands/init.py b/lean/commands/init.py index fb198ec4..681ea604 100644 --- a/lean/commands/init.py +++ b/lean/commands/init.py @@ -112,6 +112,7 @@ def init() -> None: cli_config_manager = container.cli_config_manager() if cli_config_manager.default_language.get_value() is None: default_language = click.prompt("What should the default language for new projects be?", + default=cli_config_manager.default_language.default_value, type=click.Choice(cli_config_manager.default_language.allowed_values)) cli_config_manager.default_language.set_value(default_language) @@ -121,8 +122,8 @@ def init() -> None: - {DEFAULT_DATA_DIRECTORY_NAME}/ contains the data that is used when running the LEAN engine locally The following documentation pages may be useful: -- Setting up local autocomplete: https://www.lean.io/docs/lean-cli/projects/autocomplete -- Synchronizing projects with the cloud: https://www.lean.io/docs/lean-cli/projects/cloud-synchronization +- Setting up local autocomplete: https://www.lean.io/docs/v2/lean-cli/projects/autocomplete +- Synchronizing projects with the cloud: https://www.lean.io/docs/v2/lean-cli/projects/cloud-synchronization Here are some commands to get you going: - Run `lean create-project "My Project"` to create a new project with starter code diff --git a/lean/commands/research.py b/lean/commands/research.py index 75117594..288bb991 100644 --- a/lean/commands/research.py +++ b/lean/commands/research.py @@ -18,11 +18,12 @@ from docker.errors import APIError from docker.types import Mount from lean.click import LeanCommand, PathParameter -from lean.constants import DEFAULT_RESEARCH_IMAGE +from lean.constants import DEFAULT_RESEARCH_IMAGE, LEAN_ROOT_PATH from lean.container import container from lean.models.data_providers import QuantConnectDataProvider, all_data_providers from lean.components.util.name_extraction import convert_to_class_name + def _check_docker_output(chunk: str, port: int) -> None: """Checks the output of the Docker container and opens the browser if Jupyter Lab has started. @@ -80,7 +81,7 @@ def research(project: Path, lean_config_manager = container.lean_config_manager() lean_config = lean_config_manager.get_complete_lean_config("backtesting", algorithm_file, None) - lean_config["composer-dll-directory"] = "/Lean/Launcher/bin/Debug" + lean_config["composer-dll-directory"] = LEAN_ROOT_PATH lean_config["research-object-store-name"] = algorithm_name if download_data: @@ -103,7 +104,7 @@ def research(project: Path, # Mount the config in the notebooks directory as well local_config_path = next(m["Source"] for m in run_options["mounts"] if m["Target"].endswith("config.json")) - run_options["mounts"].append(Mount(target="/Lean/Launcher/bin/Debug/Notebooks/config.json", + run_options["mounts"].append(Mount(target=f"{LEAN_ROOT_PATH}/Notebooks/config.json", source=str(local_config_path), type="bind", read_only=True)) @@ -120,7 +121,7 @@ def research(project: Path, # Mount the project to the notebooks directory run_options["volumes"][str(project)] = { - "bind": "/Lean/Launcher/bin/Debug/Notebooks", + "bind": f"{LEAN_ROOT_PATH}/Notebooks", "mode": "rw" } diff --git a/lean/components/api/api_client.py b/lean/components/api/api_client.py index c3a02ec8..23ba3ec7 100644 --- a/lean/components/api/api_client.py +++ b/lean/components/api/api_client.py @@ -168,7 +168,8 @@ def _parse_response(self, response: requests.Response) -> Any: if "errors" in data and len(data["errors"]) > 0: if data["errors"][0].startswith("Hash doesn't match."): raise AuthenticationError() - + if data["errors"][0].startswith('UserID not valid'): + data["errors"].append('Please login to your account. https://www.quantconnect.com/docs/v2/lean-cli/api-reference/lean-login') raise RequestFailedError(response, "\n".join(data["errors"])) if "messages" in data and len(data["messages"]) > 0: diff --git a/lean/components/cloud/cloud_project_manager.py b/lean/components/cloud/cloud_project_manager.py index 8ce45b7f..0d1b9cac 100644 --- a/lean/components/cloud/cloud_project_manager.py +++ b/lean/components/cloud/cloud_project_manager.py @@ -57,9 +57,7 @@ def get_cloud_project(self, input: str, push: bool) -> QCProject: """ # If the given input is a valid project directory, we try to use that project local_path = Path.cwd() / input - if self._path_manager.is_path_valid(local_path) \ - and local_path.is_dir() \ - and self._project_config_manager.get_project_config(local_path).file.exists(): + if self._project_config_manager.try_get_project_config(local_path, self._path_manager): if push: self._push_manager.push_projects([local_path]) diff --git a/lean/components/config/cli_config_manager.py b/lean/components/config/cli_config_manager.py index 39f62229..d49e6426 100644 --- a/lean/components/config/cli_config_manager.py +++ b/lean/components/config/cli_config_manager.py @@ -43,7 +43,8 @@ def __init__(self, general_storage: Storage, credentials_storage: Storage) -> No "The default language used when creating new projects.", ["python", "csharp"], False, - general_storage) + general_storage, + "python") self.engine_image = Option("engine-image", f"The Docker image used when running the LEAN engine ({DEFAULT_ENGINE_IMAGE} if not set).", diff --git a/lean/components/config/project_config_manager.py b/lean/components/config/project_config_manager.py index 2a9ae7a1..e8b2bbaf 100644 --- a/lean/components/config/project_config_manager.py +++ b/lean/components/config/project_config_manager.py @@ -17,6 +17,7 @@ from lean.components.config.storage import Storage from lean.components.util.xml_manager import XMLManager +from lean.components.util.path_manager import PathManager from lean.constants import PROJECT_CONFIG_FILE_NAME from lean.models.utils import CSharpLibrary @@ -31,6 +32,19 @@ def __init__(self, xml_manager: XMLManager) -> None: """ self._xml_manager = xml_manager + def try_get_project_config(self, project_directory: Path, path_manager: PathManager) -> Storage: + """Returns a Storage instance to get/set the configuration for a project. + + :param project_directory: the path to the project to retrieve the configuration of + :return: the Storage instance containing the project-specific configuration of the given project + """ + if path_manager.is_path_valid(project_directory) \ + and project_directory.is_dir() \ + and self.get_project_config(project_directory).file.exists(): + return Storage(str(project_directory / PROJECT_CONFIG_FILE_NAME)) + else: + return False + def get_project_config(self, project_directory: Path) -> Storage: """Returns a Storage instance to get/set the configuration for a project. diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py index 24a901c6..efd4558b 100644 --- a/lean/components/docker/lean_runner.py +++ b/lean/components/docker/lean_runner.py @@ -30,10 +30,11 @@ from lean.components.util.project_manager import ProjectManager from lean.components.util.temp_manager import TempManager from lean.components.util.xml_manager import XMLManager -from lean.constants import MODULES_DIRECTORY, TERMINAL_LINK_PRODUCT_ID +from lean.constants import MODULES_DIRECTORY, TERMINAL_LINK_PRODUCT_ID, LEAN_ROOT_PATH from lean.models.docker import DockerImage from lean.models.utils import DebuggingMethod + class LeanRunner: """The LeanRunner class contains the code that runs the LEAN engine locally.""" @@ -314,7 +315,7 @@ def get_basic_docker_config(self, file.write(json.dumps(lean_config, indent=4)) # Mount the Lean config - run_options["mounts"].append(Mount(target="/Lean/Launcher/bin/Debug/config.json", + run_options["mounts"].append(Mount(target=f"{LEAN_ROOT_PATH}/config.json", source=str(config_path), type="bind", read_only=True)) @@ -495,7 +496,7 @@ def set_up_csharp_options(self, project_dir: Path, run_options: Dict[str, Any], # Copy over the algorithm DLL # Copy over the project reference DLLs' # Copy over all output DLLs that don't already exist in /Lean/Launcher/bin/Debug - run_options["commands"].append("cp -R -n /Compile/bin/. /Lean/Launcher/bin/Debug/") + run_options["commands"].append(f"cp -R -n /Compile/bin/. {LEAN_ROOT_PATH}/") # Copy over all library DLLs that don't already exist in /Lean/Launcher/bin/Debug # CopyLocalLockFileAssemblies does not copy the OS-specific DLLs to the output directory @@ -652,7 +653,7 @@ def _ensure_csproj_uses_correct_lean(self, package_reference.clear() package_reference.tag = "Reference" - package_reference.set("Include", "/Lean/Launcher/bin/Debug/*.dll") + package_reference.set("Include", f"{LEAN_ROOT_PATH}/*.dll") package_reference.append(self._xml_manager.parse("False")) include_added = True diff --git a/lean/constants.py b/lean/constants.py index bf90e1a8..3df17e41 100644 --- a/lean/constants.py +++ b/lean/constants.py @@ -16,6 +16,9 @@ # Due to the way the filesystem is mocked in unit tests, values should not be Path instances. +# The file in which general CLI configuration is stored +LEAN_ROOT_PATH = "/Lean/Launcher/bin/Debug" + # The file in which general CLI configuration is stored GENERAL_CONFIG_PATH = str(Path("~/.lean/config").expanduser()) diff --git a/lean/models/options.py b/lean/models/options.py index f80ba5dc..4716e224 100644 --- a/lean/models/options.py +++ b/lean/models/options.py @@ -68,7 +68,8 @@ def __init__(self, description: str, allowed_values: List[str], is_sensitive: bool, - storage: Storage) -> None: + storage: Storage, + default_value: str = None) -> None: """Creates a new ChoiceOption instance. :param key: the name of the key of the option in the given file, should use hyphens for separation @@ -76,8 +77,10 @@ def __init__(self, :param allowed_values: the values which can be set :param is_sensitive: whether the contents of this option may be logged without masking it :param storage: the Storage instance to store this option in + :param default_value: the default value to use for the choices """ self.allowed_values = allowed_values + self.default_value = default_value if description.endswith("."): description = description[:-1] diff --git a/tests/commands/test_optimize.py b/tests/commands/test_optimize.py index a9d194ca..ad1576de 100644 --- a/tests/commands/test_optimize.py +++ b/tests/commands/test_optimize.py @@ -21,7 +21,7 @@ from lean.commands import lean from lean.components.config.storage import Storage -from lean.constants import DEFAULT_ENGINE_IMAGE +from lean.constants import DEFAULT_ENGINE_IMAGE, LEAN_ROOT_PATH from lean.container import container from lean.models.docker import DockerImage from lean.models.optimizer import (OptimizationConstraint, OptimizationExtremum, OptimizationParameter, @@ -136,7 +136,7 @@ def test_optimize_mounts_lean_config() -> None: docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args - assert any([mount["Target"] == "/Lean/Launcher/bin/Debug/config.json" for mount in kwargs["mounts"]]) + assert any([mount["Target"] == f"{LEAN_ROOT_PATH}/config.json" for mount in kwargs["mounts"]]) def test_optimize_mounts_data_directory() -> None: diff --git a/tests/commands/test_research.py b/tests/commands/test_research.py index 2a40c50a..c6fd09ab 100644 --- a/tests/commands/test_research.py +++ b/tests/commands/test_research.py @@ -19,7 +19,7 @@ from dependency_injector import providers from lean.commands import lean -from lean.constants import DEFAULT_RESEARCH_IMAGE +from lean.constants import DEFAULT_RESEARCH_IMAGE, LEAN_ROOT_PATH from lean.container import container from lean.models.docker import DockerImage from tests.test_helpers import create_fake_lean_cli_directory @@ -56,8 +56,8 @@ def test_research_mounts_lean_config_to_notebooks_directory_as_well() -> None: docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args - lean_config = next(m["Source"] for m in kwargs["mounts"] if m["Target"] == "/Lean/Launcher/bin/Debug/config.json") - assert any(m["Source"] == lean_config and m["Target"] == "/Lean/Launcher/bin/Debug/Notebooks/config.json" for m in + lean_config = next(m["Source"] for m in kwargs["mounts"] if m["Target"] == f"{LEAN_ROOT_PATH}/config.json") + assert any(m["Source"] == lean_config and m["Target"] == f"{LEAN_ROOT_PATH}/Notebooks/config.json" for m in kwargs["mounts"]) @@ -77,7 +77,7 @@ def test_research_adds_credentials_to_project_config() -> None: docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args - mount = [m for m in kwargs["mounts"] if m["Target"] == "/Lean/Launcher/bin/Debug/Notebooks/config.json"][0] + mount = [m for m in kwargs["mounts"] if m["Target"] == f"{LEAN_ROOT_PATH}/Notebooks/config.json"][0] with open(mount["Source"]) as file: config = json.load(file) diff --git a/tests/components/docker/test_lean_runner.py b/tests/components/docker/test_lean_runner.py index 5832e278..e7996e86 100644 --- a/tests/components/docker/test_lean_runner.py +++ b/tests/components/docker/test_lean_runner.py @@ -25,7 +25,7 @@ from lean.components.util.project_manager import ProjectManager from lean.components.util.temp_manager import TempManager from lean.components.util.xml_manager import XMLManager -from lean.constants import DEFAULT_ENGINE_IMAGE +from lean.constants import DEFAULT_ENGINE_IMAGE, LEAN_ROOT_PATH from lean.models.utils import DebuggingMethod from lean.models.docker import DockerImage from lean.models.modules import NuGetPackage @@ -163,7 +163,7 @@ def test_run_lean_mounts_config_file() -> None: docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args - assert any([mount["Target"] == "/Lean/Launcher/bin/Debug/config.json" for mount in kwargs["mounts"]]) + assert any([mount["Target"] == f"{LEAN_ROOT_PATH}/config.json" for mount in kwargs["mounts"]]) def test_run_lean_mounts_data_directory() -> None: