Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,35 @@ def _on_login_connect(self):
"""
self.connect()

def login(self, connect_after_login=True, timeout=120):
def _complete_login(self, connect_after_login=True):
user = self.pieces_client.os_api.sign_into_os()
self.user_profile = user
if connect_after_login and user:
self._on_login_connect()
return user
Comment thread
anthony-maio marked this conversation as resolved.
Outdated

def login(self, connect_after_login=True, timeout=120, async_req=False):
"""
Logs the user into the OS and optionally connects to the cloud.

Args:
connect_after_login: A flag indicating if the user should connect to the cloud after login (default is True).
timeout: The maximum time to wait for the login process (default is 120 seconds).
async_req: Start the login flow in the background without waiting for it to finish.
"""
result = {}

def target():
result['user'] = self.pieces_client.os_api.sign_into_os()
result['user'] = self._complete_login(connect_after_login)
Comment thread
anthony-maio marked this conversation as resolved.
Outdated

thread = Thread(target=target)
thread = Thread(target=target, daemon=True)
thread.start()
if async_req:
return thread

Comment thread
anthony-maio marked this conversation as resolved.
Outdated
thread.join(timeout)
Comment thread
anthony-maio marked this conversation as resolved.

if connect_after_login:
self.user_profile = result.get('user')
self._on_login_connect()
return result.get('user')

def logout(self):
"""
Expand Down
13 changes: 11 additions & 2 deletions src/pieces/command_interface/auth_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,19 @@ def execute(self, **kwargs) -> int:
)
if status == AllocationStatusEnum.DISCONNECTED:
Settings.logger.print("Connecting to the Pieces Cloud...")
Settings.pieces_client.user.connect()
if Settings.run_in_loop:
Settings.pieces_client.user.connect(async_req=True)
else:
Comment thread
anthony-maio marked this conversation as resolved.
Settings.pieces_client.user.connect()
Comment thread
anthony-maio marked this conversation as resolved.
return 0
try:
Settings.pieces_client.user.login(True)
if Settings.run_in_loop:
Settings.pieces_client.user.login(True, async_req=True)
Settings.logger.print(
"Sign-in opened in your browser. You can keep using `pieces run` while it completes."
)
else:
Settings.pieces_client.user.login(True)
except Exception as e:
Settings.logger.error(f"Sign in failed: {e}")
return 0
Expand Down
48 changes: 48 additions & 0 deletions tests/auth_commands_test.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import pytest
import threading
from unittest.mock import Mock, patch
from pieces.command_interface.auth_commands import LoginCommand, LogoutCommand
from pieces.settings import Settings
from pieces._vendor.pieces_os_client.models.allocation_status_enum import (
AllocationStatusEnum,
)
from pieces._vendor.pieces_os_client.models.user_profile import UserProfile
from pieces._vendor.pieces_os_client.wrapper.basic_identifier.user import BasicUser


class TestLoginCommand:
Expand Down Expand Up @@ -107,6 +109,27 @@ def test_execute_not_logged_in(
assert result == 0
mock_user.login.assert_called_once_with(True)

@patch.object(Settings, "run_in_loop", True)
@patch.object(Settings, "logger")
@patch.object(Settings, "pieces_client")
def test_execute_not_logged_in_in_run_loop_uses_async_login(
self, mock_pieces_client, mock_logger, login_command
):
"""Test login in run mode uses the non-blocking login path."""
mock_user = Mock()
mock_user.user_profile = None
mock_user.login = Mock()

mock_pieces_client.user = mock_user
mock_pieces_client.user_api.user_snapshot.return_value.user = None

result = login_command.execute()

assert result == 0
mock_user.login.assert_called_once_with(True, async_req=True)
mock_logger.print.assert_called_once()
assert "browser" in mock_logger.print.call_args[0][0].lower()

@patch.object(Settings, "logger")
@patch.object(Settings, "pieces_client")
def test_execute_login_exception(
Expand Down Expand Up @@ -317,6 +340,31 @@ def test_execute_multiple_logout_calls(
mock_logger.error.assert_not_called()


class TestBasicUserLogin:
def test_login_async_returns_background_thread_without_waiting(self):
pieces_client = Mock()
started = threading.Event()
release = threading.Event()

def delayed_login():
started.set()
release.wait(timeout=5)
return "user-profile"

pieces_client.os_api.sign_into_os.side_effect = delayed_login

user = BasicUser(pieces_client)

thread = user.login(connect_after_login=False, async_req=True)

assert started.wait(timeout=1)
assert thread.is_alive()

release.set()
thread.join(timeout=1)
assert not thread.is_alive()


class TestLoginLogoutIntegration:
"""Integration tests for login and logout commands working together."""

Expand Down
Loading