예제 #1
0
def test_config_get_aborts_when_option_is_sensitive() -> None:
    container.cli_config_manager().user_id.set_value("123")

    result = CliRunner().invoke(lean, ["config", "get", "user-id"])

    assert result.exit_code != 0
    assert "123" not in result.output
예제 #2
0
def test_live_passes_custom_image_to_lean_runner_when_given_as_option() -> None:
    create_fake_lean_cli_directory()
    create_fake_environment("live-paper", True)

    docker_manager = mock.Mock()
    container.docker_manager.override(providers.Object(docker_manager))

    lean_runner = mock.Mock()
    container.lean_runner.override(providers.Object(lean_runner))

    container.cli_config_manager().engine_image.set_value("custom/lean:123")

    result = CliRunner().invoke(lean,
                                ["live", "Python Project", "--environment", "live-paper", "--image", "custom/lean:456"])

    assert result.exit_code == 0

    lean_runner.run_lean.assert_called_once_with(mock.ANY,
                                                 "live-paper",
                                                 Path("Python Project/main.py").resolve(),
                                                 mock.ANY,
                                                 DockerImage(name="custom/lean", tag="456"),
                                                 None,
                                                 False,
                                                 False)
예제 #3
0
def test_research_adds_credentials_to_project_config() -> None:
    create_fake_lean_cli_directory()

    docker_manager = mock.Mock()
    container.docker_manager.override(providers.Object(docker_manager))

    container.cli_config_manager().user_id.set_value("123")
    container.cli_config_manager().api_token.set_value("456")

    result = CliRunner().invoke(lean, ["research", "Python Project"])

    assert result.exit_code == 0

    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]

    with open(mount["Source"]) as file:
        config = json.load(file)

    assert config["job-user-id"] == "123"
    assert config["api-access-token"] == "456"
예제 #4
0
def test_config_unset_removes_the_value_of_the_option() -> None:
    container.cli_config_manager().user_id.set_value("12345")

    result = CliRunner().invoke(lean, ["config", "unset", "user-id"])

    assert result.exit_code == 0

    assert container.cli_config_manager().user_id.get_value() is None
예제 #5
0
def test_config_get_prints_the_value_of_the_option_with_the_given_key() -> None:
    container.cli_config_manager().default_language.set_value("python")

    runner = CliRunner()
    result = runner.invoke(lean, ["config", "get", "default-language"])

    assert result.exit_code == 0
    assert result.output == "python\n"
예제 #6
0
def test_logout_deletes_credentials_storage_file() -> None:
    container.cli_config_manager().user_id.set_value("123")
    assert Path("~/.lean/credentials").expanduser().exists()

    result = CliRunner().invoke(lean, ["logout"])

    assert result.exit_code == 0

    assert not Path("~/.lean/credentials").expanduser().exists()
예제 #7
0
def test_create_project_creates_python_project_when_default_language_set_to_python(
) -> None:
    container.cli_config_manager().default_language.set_value("python")

    result = CliRunner().invoke(lean, ["create-project", "My First Project"])

    assert result.exit_code == 0

    assert_python_project_exists()
예제 #8
0
def test_login_aborts_when_credentials_are_invalid() -> None:
    api_client = mock.Mock()
    api_client.is_authenticated.return_value = False
    container.api_client.override(providers.Object(api_client))

    result = CliRunner().invoke(
        lean, ["login", "--user-id", "123", "--api-token", "456"])

    assert result.exit_code != 0

    assert container.cli_config_manager().user_id.get_value() is None
    assert container.cli_config_manager().api_token.get_value() is None
예제 #9
0
def test_login_logs_in_with_options_when_given() -> None:
    api_client = mock.Mock()
    api_client.is_authenticated.return_value = True
    container.api_client.override(providers.Object(api_client))

    result = CliRunner().invoke(
        lean, ["login", "--user-id", "123", "--api-token", "456"])

    assert result.exit_code == 0

    assert container.cli_config_manager().user_id.get_value() == "123"
    assert container.cli_config_manager().api_token.get_value() == "456"
예제 #10
0
def test_login_prompts_when_api_token_not_given() -> None:
    api_client = mock.Mock()
    api_client.is_authenticated.return_value = True
    container.api_client.override(providers.Object(api_client))

    result = CliRunner().invoke(lean, ["login", "--user-id", "123"],
                                input="456\n")

    assert result.exit_code == 0
    assert "API token:" in result.output

    assert container.cli_config_manager().user_id.get_value() == "123"
    assert container.cli_config_manager().api_token.get_value() == "456"
예제 #11
0
def test_config_list_does_not_show_complete_values_of_sensitive_options(
        size) -> None:
    container.cli_config_manager().user_id.set_value("123")
    container.cli_config_manager().api_token.set_value(
        "abcdefghijklmnopqrstuvwxyz")

    size.return_value = ConsoleDimensions(1000, 1000)

    result = CliRunner().invoke(lean, ["config", "list"])

    assert result.exit_code == 0

    assert "123" not in result.output
    assert "abcdefghijklmnopqrstuvwxyz" not in result.output
예제 #12
0
파일: login.py 프로젝트: valmac/lean-cli
def login(user_id: Optional[str], api_token: Optional[str]) -> None:
    """Log in with a QuantConnect account.

    If user id or API token is not provided an interactive prompt will show.

    Credentials are stored in ~/.lean/credentials and are removed upon running `lean logout`.
    """
    logger = container.logger()
    credentials_storage = container.credentials_storage()

    if user_id is None or api_token is None:
        logger.info(
            "Your user id and API token are needed to make authenticated requests to the QuantConnect API"
        )
        logger.info(
            "You can request these credentials on https://www.quantconnect.com/account"
        )
        logger.info(f"Both will be saved in {credentials_storage.file}")

    if user_id is None:
        user_id = click.prompt("User id")

    if api_token is None:
        api_token = click.prompt("API token")

    api_client = container.api_client(user_id=user_id, api_token=api_token)
    if not api_client.is_authenticated():
        raise RuntimeError("Credentials are invalid")

    cli_config_manager = container.cli_config_manager()
    cli_config_manager.user_id.set_value(user_id)
    cli_config_manager.api_token.set_value(api_token)

    logger.info("Successfully logged in")
예제 #13
0
def whoami() -> None:
    """Display who is logged in."""
    logger = container.logger()
    api_client = container.api_client()
    cli_config_manager = container.cli_config_manager()

    if cli_config_manager.user_id.get_value(
    ) is not None and cli_config_manager.api_token.get_value() is not None:
        try:
            organizations = api_client.organizations.get_all()
            logged_in = True
        except AuthenticationError:
            logged_in = False
    else:
        logged_in = False

    if not logged_in:
        logger.info("You are not logged in")
        return

    personal_organization_id = next(o.id for o in organizations
                                    if o.ownerName == "You")
    personal_organization = api_client.organizations.get(
        personal_organization_id)
    member = next(m for m in personal_organization.members if m.isAdmin)

    logger.info(f"You are logged in as {member.name} ({member.email})")
예제 #14
0
def test_config_set_updates_the_value_of_the_option() -> None:
    runner = CliRunner()
    result = runner.invoke(lean, ["config", "set", "user-id", "12345"])

    assert result.exit_code == 0

    assert container.cli_config_manager().user_id.get_value() == "12345"
예제 #15
0
def test_init_prompts_for_default_language_when_not_set_yet() -> None:
    result = CliRunner().invoke(lean, ["init"], input="csharp\n")

    assert result.exit_code == 0

    assert container.cli_config_manager().default_language.get_value(
    ) == "csharp"
예제 #16
0
def test_research_runs_custom_image_when_given_as_option() -> None:
    create_fake_lean_cli_directory()

    docker_manager = mock.Mock()
    container.docker_manager.override(providers.Object(docker_manager))

    container.cli_config_manager().research_image.set_value("custom/research:123")

    result = CliRunner().invoke(lean, ["research", "Python Project", "--image", "custom/research:456"])

    assert result.exit_code == 0

    docker_manager.run_image.assert_called_once()
    args, kwargs = docker_manager.run_image.call_args

    assert args[0] == DockerImage(name="custom/research", tag="456")
예제 #17
0
def create_project(name: str, language: str) -> None:
    """Create a new project containing starter code.

    If NAME is a path containing subdirectories those will be created automatically.

    The default language can be set using `lean config set default-language python/csharp`.
    """
    cli_config_manager = container.cli_config_manager()

    language = language if language is not None else cli_config_manager.default_language.get_value(
    )
    if language is None:
        raise MoreInfoError(
            "Please specify a language with --language or set the default language using `lean config set default-language python/csharp`",
            "https://www.quantconnect.com/docs/v2/lean-cli/tutorials/project-management"
        )

    full_path = Path.cwd() / name

    path_validator = container.path_validator()
    if not path_validator.is_path_valid(full_path):
        raise MoreInfoError(
            f"'{name}' is not a valid path",
            "https://www.quantconnect.com/docs/v2/lean-cli/user-guides/troubleshooting#02-Common-errors"
        )

    if full_path.exists():
        raise RuntimeError(
            f"A project named '{name}' already exists, please choose a different name"
        )
    else:
        project_manager = container.project_manager()
        project_manager.create_new_project(
            full_path,
            QCLanguage.Python if language == "python" else QCLanguage.CSharp)

    # Convert the project name into a valid class name by removing all non-alphanumeric characters
    class_name = re.sub(f"[^a-zA-Z0-9]", "",
                        "".join(map(_capitalize, name.split(" "))))

    if language == "python":
        with (full_path / "main.py").open("w+", encoding="utf-8") as file:
            file.write(DEFAULT_PYTHON_MAIN.replace("$NAME$", class_name))
    else:
        with (full_path / "Main.cs").open("w+", encoding="utf-8") as file:
            file.write(DEFAULT_CSHARP_MAIN.replace("$NAME$", class_name))

    with (full_path / "research.ipynb").open("w+", encoding="utf-8") as file:
        file.write(DEFAULT_PYTHON_NOTEBOOK if language ==
                   "python" else DEFAULT_CSHARP_NOTEBOOK)

    with (full_path / "config.json").open("w+", encoding="utf-8") as file:
        file.write(DEFAULT_PYTHON_CONFIG if language ==
                   "python" else DEFAULT_CSHARP_CONFIG)

    logger = container.logger()
    logger.info(
        f"Successfully created {'Python' if language == 'python' else 'C#'} project '{name}'"
    )
예제 #18
0
def live(project: Path, environment: str, output: Optional[Path],
         image: Optional[str], update: bool) -> None:
    """Start live trading a project locally using Docker.

    \b
    If PROJECT is a directory, the algorithm in the main.py or Main.cs file inside it will be executed.
    If PROJECT is a file, the algorithm in the specified file will be executed.

    \b
    ENVIRONMENT must be the name of an environment in the Lean configuration file with live-mode set to true.

    By default the official LEAN engine image is used.
    You can override this using the --image option.
    Alternatively you can set the default engine image for all commands using `lean config set engine-image <image>`.
    """
    project_manager = container.project_manager()
    algorithm_file = project_manager.find_algorithm_file(Path(project))

    if output is None:
        output = algorithm_file.parent / "live" / datetime.now().strftime(
            "%Y-%m-%d_%H-%M-%S")

    lean_config_manager = container.lean_config_manager()
    lean_config = lean_config_manager.get_complete_lean_config(
        environment, algorithm_file, None)

    if "environments" not in lean_config or environment not in lean_config[
            "environments"]:
        lean_config_path = lean_config_manager.get_lean_config_path()
        raise MoreInfoError(
            f"{lean_config_path} does not contain an environment named '{environment}'",
            "https://www.quantconnect.com/docs/v2/lean-cli/tutorials/live-trading/local-live-trading"
        )

    if not lean_config["environments"][environment]["live-mode"]:
        raise MoreInfoError(
            f"The '{environment}' is not a live trading environment (live-mode is set to false)",
            "https://www.quantconnect.com/docs/v2/lean-cli/tutorials/live-trading/local-live-trading"
        )

    _raise_for_missing_properties(lean_config, environment,
                                  lean_config_manager.get_lean_config_path())
    _start_iqconnect_if_necessary(lean_config, environment)

    cli_config_manager = container.cli_config_manager()
    engine_image = cli_config_manager.get_engine_image(image)

    docker_manager = container.docker_manager()

    if update or not docker_manager.supports_dotnet_5(engine_image):
        docker_manager.pull_image(engine_image)

    lean_runner = container.lean_runner()
    lean_runner.run_lean(environment, algorithm_file, output, engine_image,
                         None)

    if str(engine_image) == DEFAULT_ENGINE_IMAGE and not update:
        update_manager = container.update_manager()
        update_manager.warn_if_docker_image_outdated(engine_image)
예제 #19
0
def build(root: Path, tag: str) -> None:
    """Build Docker images of your own version of LEAN and the Alpha Streams SDK.

    \b
    ROOT must point to a directory containing the LEAN repository and the Alpha Streams SDK repository:
    https://github.com/QuantConnect/Lean & https://github.com/QuantConnect/AlphaStreams

    When ROOT is not given, the current directory is used as root directory.

    \b
    This command performs the following actions:
    1. The lean-cli/foundation:latest image is built from Lean/DockerfileLeanFoundation(ARM).
    2. LEAN is compiled in a Docker container using the lean-cli/foundation:latest image.
    3. The Alpha Streams SDK is compiled in a Docker container using the lean-cli/foundation:latest image.
    4. The lean-cli/engine:latest image is built from Lean/Dockerfile using lean-cli/foundation:latest as base image.
    5. The lean-cli/research:latest image is built from Lean/DockerfileJupyter using lean-cli/engine:latest as base image.
    6. The default engine image is set to lean-cli/engine:latest.
    7. The default research image is set to lean-cli/research:latest.
    """
    lean_dir = root / "Lean"
    if not lean_dir.is_dir():
        raise RuntimeError(
            f"Please clone https://github.com/QuantConnect/Lean to '{lean_dir}'"
        )

    alpha_streams_dir = root / "AlphaStreams"
    if not lean_dir.is_dir():
        raise RuntimeError(
            f"Please clone https://github.com/QuantConnect/AlphaStreams to '{alpha_streams_dir}'"
        )

    (root / "DataLibraries").mkdir(exist_ok=True)

    if platform.machine() in ["arm64", "aarch64"]:
        foundation_dockerfile = lean_dir / "DockerfileLeanFoundationARM"
    else:
        foundation_dockerfile = lean_dir / "DockerfileLeanFoundation"

    custom_foundation_image = DockerImage(name="lean-cli/foundation", tag=tag)
    custom_engine_image = DockerImage(name="lean-cli/engine", tag=tag)
    custom_research_image = DockerImage(name="lean-cli/research", tag=tag)

    _build_image(root, foundation_dockerfile, None, custom_foundation_image)
    _compile_csharp(root, lean_dir, custom_foundation_image)
    _compile_csharp(root, alpha_streams_dir, custom_foundation_image)
    _build_image(root, lean_dir / "Dockerfile", custom_foundation_image,
                 custom_engine_image)
    _build_image(root, lean_dir / "DockerfileJupyter", custom_engine_image,
                 custom_research_image)

    logger = container.logger()
    cli_config_manager = container.cli_config_manager()

    logger.info(f"Setting default engine image to '{custom_engine_image}'")
    cli_config_manager.engine_image.set_value(str(custom_engine_image))

    logger.info(f"Setting default research image to '{custom_research_image}'")
    cli_config_manager.research_image.set_value(str(custom_research_image))
예제 #20
0
def test_report_runs_custom_image_when_set_in_config() -> None:
    docker_manager = mock.Mock()
    docker_manager.run_image.side_effect = run_image
    container.docker_manager.override(providers.Object(docker_manager))

    container.cli_config_manager().engine_image.set_value("custom/lean:123")

    result = CliRunner().invoke(lean, [
        "report", "--backtest-results",
        "Python Project/backtests/2020-01-01_00-00-00/results.json"
    ])

    assert result.exit_code == 0

    docker_manager.run_image.assert_called_once()
    args, kwargs = docker_manager.run_image.call_args

    assert args[0] == DockerImage(name="custom/lean", tag="123")
예제 #21
0
def create_project(name: str, language: str) -> None:
    """Create a new project containing starter code.

    If NAME is a path containing subdirectories those will be created automatically.

    The default language can be set using `lean config set default-language python/csharp`.
    """
    cli_config_manager = container.cli_config_manager()

    language = language if language is not None else cli_config_manager.default_language.get_value()
    if language is None:
        raise MoreInfoError(
            "Please specify a language with --language or set the default language using `lean config set default-language python/csharp`",
            "https://www.lean.io/docs/lean-cli/projects/project-management")

    full_path = Path.cwd() / name

    if not container.path_manager().is_path_valid(full_path):
        raise MoreInfoError(f"'{name}' is not a valid path",
                            "https://www.lean.io/docs/lean-cli/key-concepts/troubleshooting#02-Common-Errors")

    is_library_project = False
    try:
        library_dir = container.lean_config_manager().get_cli_root_directory() / "Library"
        is_library_project = library_dir in full_path.parents
    except:
        # get_cli_root_directory() raises an error if there is no such directory
        pass

    if is_library_project and language == "python" and not full_path.name.isidentifier():
        raise RuntimeError(
            f"'{full_path.name}' is not a valid Python identifier, which is required for Python library projects to be importable")

    if full_path.exists():
        raise RuntimeError(f"A project named '{name}' already exists, please choose a different name")
    else:
        project_manager = container.project_manager()
        project_manager.create_new_project(full_path, QCLanguage.Python if language == "python" else QCLanguage.CSharp)

    # Convert the project name into a valid class name by removing all non-alphanumeric characters
    class_name = re.sub(f"[^a-zA-Z0-9]", "", "".join(map(_capitalize, full_path.name.split(" "))))

    if language == "python":
        main_name = "main.py"
        main_content = DEFAULT_PYTHON_MAIN if not is_library_project else LIBRARY_PYTHON_MAIN
    else:
        main_name = "Main.cs"
        main_content = DEFAULT_CSHARP_MAIN if not is_library_project else LIBRARY_CSHARP_MAIN

    with (full_path / main_name).open("w+", encoding="utf-8") as file:
        file.write(main_content.replace("$CLASS_NAME$", class_name).replace("$PROJECT_NAME$", full_path.name))

    with (full_path / "research.ipynb").open("w+", encoding="utf-8") as file:
        file.write(DEFAULT_PYTHON_NOTEBOOK if language == "python" else DEFAULT_CSHARP_NOTEBOOK)

    logger = container.logger()
    logger.info(f"Successfully created {'Python' if language == 'python' else 'C#'} project '{name}'")
예제 #22
0
def test_data_generate_runs_custom_image_when_set_in_config() -> None:
    create_fake_lean_cli_directory()

    docker_manager = mock.Mock()
    container.docker_manager.override(providers.Object(docker_manager))

    container.cli_config_manager().engine_image.set_value("custom/lean:123")

    result = CliRunner().invoke(
        lean,
        ["data", "generate", "--start", "20200101", "--symbol-count", "1"])

    assert result.exit_code == 0

    docker_manager.run_image.assert_called_once()
    args, kwargs = docker_manager.run_image.call_args

    assert args[0] == DockerImage(name="custom/lean", tag="123")
예제 #23
0
def test_config_list_lists_all_options(size) -> None:
    size.return_value = ConsoleDimensions(1000, 1000)

    result = CliRunner().invoke(lean, ["config", "list"])

    assert result.exit_code == 0

    for option in container.cli_config_manager().all_options:
        assert option.key in result.output
        assert option.description in result.output
예제 #24
0
def test_optimize_runs_custom_image_when_set_in_config() -> None:
    create_fake_lean_cli_directory()

    docker_manager = mock.Mock()
    container.docker_manager.override(providers.Object(docker_manager))

    container.cli_config_manager().engine_image.set_value("custom/lean:123")

    Storage(str(Path.cwd() / "Python Project" / "config.json")).set(
        "parameters", {"param1": "1"})

    result = CliRunner().invoke(lean, ["optimize", "Python Project"])

    assert result.exit_code == 0

    docker_manager.run_image.assert_called_once()
    args, kwargs = docker_manager.run_image.call_args

    assert args[0] == DockerImage(name="custom/lean", tag="123")
예제 #25
0
def test_backtest_passes_custom_image_to_lean_runner_when_set_in_config(
) -> None:
    create_fake_lean_cli_directory()

    docker_manager = mock.Mock()
    container.docker_manager.override(providers.Object(docker_manager))

    lean_runner = mock.Mock()
    container.lean_runner.override(providers.Object(lean_runner))

    container.cli_config_manager().engine_image.set_value("custom/lean:123")

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

    assert result.exit_code == 0

    lean_runner.run_lean.assert_called_once_with(
        "backtesting",
        Path("Python Project/main.py").resolve(), mock.ANY,
        DockerImage(name="custom/lean", tag="123"), None)
예제 #26
0
def set(key: str, value: str) -> None:
    """Set a configurable option.

    Run `lean config list` to show all available options.
    """
    cli_config_manager = container.cli_config_manager()

    option = cli_config_manager.get_option_by_key(key)
    option.set_value(value)

    click.echo(f"Successfully updated the value of '{key}' to '{option.get_value()}'")
예제 #27
0
def unset(key: str) -> None:
    """Unset a configurable option.

    Run `lean config list` to show all available options.
    """
    cli_config_manager = container.cli_config_manager()

    option = cli_config_manager.get_option_by_key(key)
    option.unset()

    logger = container.logger()
    logger.info(f"Successfully unset '{key}'")
예제 #28
0
def backtest(project: Path, output: Optional[Path], debug: Optional[str],
             image: Optional[str], update: bool) -> None:
    """Backtest a project locally using Docker.

    \b
    If PROJECT is a directory, the algorithm in the main.py or Main.cs file inside it will be executed.
    If PROJECT is a file, the algorithm in the specified file will be executed.

    \b
    Go to the following url to learn how to debug backtests locally using the Lean CLI:
    https://www.quantconnect.com/docs/v2/lean-cli/tutorials/backtesting/debugging-local-backtests

    By default the official LEAN engine image is used.
    You can override this using the --image option.
    Alternatively you can set the default engine image for all commands using `lean config set engine-image <image>`.
    """
    project_manager = container.project_manager()
    algorithm_file = project_manager.find_algorithm_file(Path(project))

    if output is None:
        output = algorithm_file.parent / "backtests" / datetime.now().strftime(
            "%Y-%m-%d_%H-%M-%S")

    debugging_method = None
    if debug == "pycharm":
        debugging_method = DebuggingMethod.PyCharm
        _migrate_dotnet_5_python_pycharm(algorithm_file.parent)
    elif debug == "ptvsd":
        debugging_method = DebuggingMethod.PTVSD
        _migrate_dotnet_5_python_vscode(algorithm_file.parent)
    elif debug == "vsdbg":
        debugging_method = DebuggingMethod.VSDBG
        _migrate_dotnet_5_csharp_vscode(algorithm_file.parent)
    elif debug == "rider":
        debugging_method = DebuggingMethod.Rider
        _migrate_dotnet_5_csharp_rider(algorithm_file.parent)

    cli_config_manager = container.cli_config_manager()
    engine_image = cli_config_manager.get_engine_image(image)

    docker_manager = container.docker_manager()

    if update or not docker_manager.supports_dotnet_5(engine_image):
        docker_manager.pull_image(engine_image)

    lean_runner = container.lean_runner()
    lean_runner.run_lean("backtesting", algorithm_file, output, engine_image,
                         debugging_method)

    if str(engine_image) == DEFAULT_ENGINE_IMAGE and not update:
        update_manager = container.update_manager()
        update_manager.warn_if_docker_image_outdated(engine_image)
예제 #29
0
def list() -> None:
    """List the configurable options and their current values."""
    table = Table(box=box.SQUARE)
    table.add_column("Key")
    table.add_column("Value")
    table.add_column("Location")
    table.add_column("Description")

    for option in container.cli_config_manager().all_options:
        value = option.get_value(default="<not set>")

        # Mask values of sensitive options
        if value != "<not set>" and option.is_sensitive:
            value = "*" * 12 + value[-3:] if len(value) >= 5 else "*" * 15

        table.add_row(option.key, value, str(option.location),
                      option.description)

    logger = container.logger()
    logger.info(table)
예제 #30
0
파일: get.py 프로젝트: valmac/lean-cli
def get(key: str) -> None:
    """Get the current value of a configurable option.

    Sensitive options like credentials cannot be retrieved this way for security reasons.
    Please open ~/.lean/credentials if you want to see your currently stored credentials.

    Run `lean config list` to show all available options.
    """
    cli_config_manager = container.cli_config_manager()

    option = cli_config_manager.get_option_by_key(key)
    if option.is_sensitive:
        raise RuntimeError(
            "Sensitive options like credentials cannot be retrieved using `lean config get` for security reasons"
        )

    value = option.get_value()
    if value is None:
        raise RuntimeError(
            f"The option with key '{key}' doesn't have a value set")

    click.echo(value)