diff --git a/lean/commands/init.py b/lean/commands/init.py index 45a7ade8..89dd4a76 100644 --- a/lean/commands/init.py +++ b/lean/commands/init.py @@ -17,6 +17,7 @@ from click import command, option, Choice, confirm, prompt from lean.click import LeanCommand +from lean.commands.login import get_credentials, validate_credentials, get_lean_config_credentials from lean.constants import DEFAULT_DATA_DIRECTORY_NAME, DEFAULT_LEAN_CONFIG_FILE_NAME from lean.container import container from lean.models.errors import MoreInfoError @@ -128,8 +129,13 @@ def init(organization: Optional[str], language: Optional[str]) -> None: from shutil import copytree from zipfile import ZipFile - # Select and set organization + # Retrieve current credentials from the Lean CLI configuration + # If credentials are not available, prompt the user to provide them + current_user_id, current_api_token = get_lean_config_credentials() + user_id, api_token = get_credentials(current_user_id, current_api_token, False) + validate_credentials(user_id, api_token) + # Select and set organization if organization is not None: organization_id, organization_name = _get_organization_id(organization) else: diff --git a/lean/commands/login.py b/lean/commands/login.py index 834318bb..f46d24a3 100644 --- a/lean/commands/login.py +++ b/lean/commands/login.py @@ -11,29 +11,38 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import Optional, Tuple from click import command, option, prompt from lean.click import LeanCommand from lean.container import container from lean.models.errors import MoreInfoError -from lean.components.api.api_client import APIClient -@command(cls=LeanCommand) -@option("--user-id", "-u", type=str, help="QuantConnect user id") -@option("--api-token", "-t", type=str, help="QuantConnect API token") -@option("--show-secrets", is_flag=True, show_default=True, default=False, help="Show secrets as they are input") -def login(user_id: Optional[str], api_token: Optional[str], show_secrets: bool) -> None: - """Log in with a QuantConnect account. +def get_lean_config_credentials() -> Tuple[str, str]: + """Retrieve the QuantConnect credentials from the Lean CLI configuration. - If user id or API token is not provided an interactive prompt will show. + This function accesses the Lean CLI configuration manager to obtain the + stored user ID and API token. The credentials are retrieved from the + configuration settings managed by the Lean CLI. - Credentials are stored in ~/.lean/credentials and are removed upon running `lean logout`. + Returns: + tuple[str, str]: A tuple containing the user ID and API token as strings. """ + cli_config_manager = container.cli_config_manager + + user_id = cli_config_manager.user_id.get_value() + api_token = cli_config_manager.api_token.get_value() + + return user_id, api_token + + +def get_credentials(user_id: Optional[str], api_token: Optional[str], show_secrets: bool) -> Tuple[str, str]: + """Fetch user credentials, prompting the user if necessary.""" logger = container.logger credentials_storage = container.credentials_storage + current_user_id, current_api_token = get_lean_config_credentials() 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") @@ -41,18 +50,41 @@ def login(user_id: Optional[str], api_token: Optional[str], show_secrets: bool) logger.info(f"Both will be saved in {credentials_storage.file}") if user_id is None: - user_id = prompt("User id") + user_id = prompt("User id", current_user_id) if api_token is None: - api_token = logger.prompt_password("API token", hide_input=not show_secrets) + api_token = logger.prompt_password("API token", current_api_token, hide_input=not show_secrets) + + return user_id, api_token + +def validate_credentials(user_id: str, api_token: str) -> None: + """Validate the user credentials by attempting to authenticate with the QuantConnect API.""" container.api_client.set_user_token(user_id=user_id, api_token=api_token) + if not container.api_client.is_authenticated(): - raise MoreInfoError("Credentials are invalid. Please ensure your computer clock is correct, or try using another terminal, or enter API token manually instead of copy-pasting.", - "https://www.lean.io/docs/v2/lean-cli") + raise MoreInfoError( + "Credentials are invalid. Please ensure your computer clock is correct, or try using another terminal, or enter API token manually instead of copy-pasting.", + "https://www.lean.io/docs/v2/lean-cli" + ) 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") + container.logger.info("Successfully logged in") + +@command(cls=LeanCommand) +@option("--user-id", "-u", type=str, help="QuantConnect user id") +@option("--api-token", "-t", type=str, help="QuantConnect API token") +@option("--show-secrets", is_flag=True, show_default=True, default=False, help="Show secrets as they are input") +def login(user_id: Optional[str], api_token: Optional[str], show_secrets: bool) -> 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`. + """ + + user_id, api_token = get_credentials(user_id, api_token, show_secrets) + validate_credentials(user_id, api_token) diff --git a/tests/commands/test_init.py b/tests/commands/test_init.py index f39c8ce0..248a8e9e 100644 --- a/tests/commands/test_init.py +++ b/tests/commands/test_init.py @@ -80,6 +80,9 @@ def mock_get_organization(org_id: str) -> QCMinimalOrganization: # container.api_client is already a mock from set_unauthenticated() api_client = container.api_client + container.cli_config_manager.user_id.set_value("123") + container.cli_config_manager.api_token.set_value("456") + api_client.is_authenticated.return_value = True api_client.organizations.get_all.side_effect = _get_all_organizations api_client.organizations.get.side_effect = mock_get_organization