Skip to content

Commit

Permalink
Feature: Login process in Initialization of lean folder (#486)
Browse files Browse the repository at this point in the history
* feat: add login process in init command

* fix: Import Tuple from the typing module to ensure type hinting is compatible across different Python versions.

* test:feat: add authenticated procees in mock of init tests
  • Loading branch information
Romazes authored Aug 15, 2024
1 parent 8590aad commit 5848527
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 16 deletions.
8 changes: 7 additions & 1 deletion lean/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
62 changes: 47 additions & 15 deletions lean/commands/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,80 @@
# 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")
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 = 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)
3 changes: 3 additions & 0 deletions tests/commands/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 5848527

Please sign in to comment.