Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Login process in Initialization of lean folder #486

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading