HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
File: C:/Users/fred/anaconda3/Lib/site-packages/anaconda_navigator/api/cloud/login_management.py
# -*- coding: utf8 -*-

"""Utilities to manage login workflows and states."""

from __future__ import annotations

__all__ = ['LoginManager', 'LOGIN_MANAGER']

import typing

from qtpy import QtCore

from anaconda_navigator import config
from anaconda_navigator.utils import singletons
from anaconda_navigator.utils import telemetry
from anaconda_navigator.utils import workers
from anaconda_navigator.utils.logs import logger
from . import api


class LoginManager(QtCore.QObject):
    """Manage .cloud login flow."""

    sig_login_started = QtCore.Signal()
    sig_login_succeeded = QtCore.Signal(workers.TaskResult)
    sig_login_canceled = QtCore.Signal(workers.TaskResult)
    sig_login_failed = QtCore.Signal(workers.TaskResult)
    sig_login_done = QtCore.Signal(workers.TaskResult)

    sig_logout = QtCore.Signal()

    sig_state_changed = QtCore.Signal(bool)

    sig_throttle_started = QtCore.Signal()
    sig_throttle_finished = QtCore.Signal()

    def __init__(self, parent: QtCore.QObject | None = None) -> None:
        """Initialize new instance of a :class:`~CloudLoginManager`."""
        super().__init__(parent=parent)

        self.__pending: bool = False
        self.__worker: workers.TaskWorker | None = None

        self.__throttle: bool = False

        self.__throttle_timer: typing.Final[QtCore.QTimer] = QtCore.QTimer(self)
        self.__throttle_timer.setInterval(5_000)  # 5 seconds
        self.__throttle_timer.setSingleShot(True)
        self.__throttle_timer.timeout.connect(self.__stop_throttle)

    # login

    def login(self, origin: str = 'unknown') -> None:
        """Start a login process."""
        if self.__throttle:
            logger.debug('cloud login throttled')
            return
        logger.debug('cloud login initiated')

        telemetry.ANALYTICS.instance.event('cloud-login-requested', {'origin': origin})
        if self.__worker is not None:
            self.__pending = True
            self.__worker.cancel()
        else:
            self.__login()

    def __login(self) -> None:
        """Initialize a worker to process login workflow."""
        self.__start_throttle()

        self.__worker = typing.cast(workers.TaskWorker, api.CloudAPI().login.worker())
        self.__worker.signals.sig_start.connect(self.sig_login_started)
        self.__worker.signals.sig_succeeded.connect(self.sig_login_succeeded)
        self.__worker.signals.sig_canceled.connect(self.sig_login_canceled)
        self.__worker.signals.sig_failed.connect(self.sig_login_failed)
        self.__worker.signals.sig_done.connect(self.__login_done)
        self.__worker.start()

    def __login_done(self, result: workers.TaskResult) -> None:
        """Process result of a login processing."""
        self.__stop_throttle()
        self.__worker = None
        self.sig_login_done.emit(result)

        if result.status == workers.TaskStatus.SUCCEEDED:
            logger.debug('cloud login succeeded')
            telemetry.ANALYTICS.instance.event(
                'login_changed', {
                    'action': telemetry.AccountAction.LOGIN,
                    'edition': config.AnacondaBrand.CLOUD,
                }, user_properties={'cloud-edition': config.AnacondaBrand.CLOUD})
            self.sig_state_changed.emit(True)
        elif result.status == workers.TaskStatus.CANCELED:
            if self.__pending:
                self.__pending = False
                self.__login()

            logger.debug('cloud login canceled')
            telemetry.ANALYTICS.instance.event('cancel-cloud-login')
        elif result.status == workers.TaskStatus.FAILED:
            logger.debug('cloud login failed')
            telemetry.ANALYTICS.instance.event('cloud-login-failed')
        else:  # just in case we add new status and forget to add it here
            logger.debug('cloud login done')  # type: ignore

    # logout

    def logout(self) -> None:
        """Log out from .cloud account."""
        if not api.CloudAPI().token:
            logger.debug('cloud logout ignored')
            return
        logger.debug('cloud logout initiated')

        if self.__worker is not None:
            self.__worker.cancel()

        api.CloudAPI().logout()
        telemetry.ANALYTICS.instance.event(
            'login_changed', {
                'action': telemetry.AccountAction.LOGOUT,
                'edition': config.AnacondaBrand.CLOUD,
            },
            user_properties={'cloud-edition': '-'})
        self.sig_logout.emit()
        self.sig_state_changed.emit(False)
        logger.debug('cloud logout succeeded')

    # throttle

    def __start_throttle(self) -> None:
        """
        Start login process throttling.

        It doesn't allow to spam logins by giving each login attempt 5 seconds to start.
        """
        if self.__throttle:
            return
        logger.debug('cloud login throttling started')
        self.__throttle = True
        self.__throttle_timer.start()
        self.sig_throttle_started.emit()

    def __stop_throttle(self) -> None:
        """Finish login throttling."""
        if not self.__throttle:
            return
        logger.debug('cloud login throttling finished')

        self.__throttle_timer.stop()
        self.__throttle = False
        self.sig_throttle_finished.emit()


LOGIN_MANAGER: typing.Final[singletons.Singleton[LoginManager]] = singletons.SingleInstanceOf(LoginManager)