Skip to content

Commit 089b8a1

Browse files
refactoring
1 parent 22be055 commit 089b8a1

File tree

7 files changed

+861
-1
lines changed

7 files changed

+861
-1
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Browser Connector for Playwright CDP connections."""
2+
3+
from __future__ import annotations
4+
import logging
5+
from typing import Any, Optional
6+
7+
from .base import BrowserConfig, BrowserConnectionResult, ConnectionStatus
8+
9+
log = logging.getLogger("nlp2cmd.browser_manager.connector")
10+
11+
12+
class BrowserConnector:
13+
"""Connect to browsers via Playwright CDP protocol."""
14+
15+
def __init__(self, config: Optional[BrowserConfig] = None) -> None:
16+
self.config = config or BrowserConfig()
17+
18+
def connect(
19+
self,
20+
port: int,
21+
verbose: bool = False,
22+
console: Optional[Any] = None,
23+
) -> BrowserConnectionResult:
24+
"""Connect to browser on specified CDP port.
25+
26+
Args:
27+
port: CDP port to connect to
28+
verbose: Whether to log detailed output
29+
console: Optional Rich console for formatted output
30+
31+
Returns:
32+
BrowserConnectionResult with connection details
33+
"""
34+
result = BrowserConnectionResult(cdp_port=port)
35+
36+
try:
37+
from playwright.sync_api import sync_playwright
38+
except ImportError:
39+
result.status = ConnectionStatus.PLAYWRIGHT_MISSING
40+
result.error = "Playwright not installed"
41+
if console and verbose:
42+
console.print("[red] ✗ Playwright not installed[/red]")
43+
return result
44+
45+
if console and verbose:
46+
console.print(f"[cyan] → Connecting via Playwright to port {port}...[/cyan]")
47+
48+
try:
49+
with sync_playwright() as p:
50+
# Try Chrome/Chromium first
51+
browser, browser_type = self._try_connect_chrome(p, port, verbose, console)
52+
53+
if not browser:
54+
# Try Firefox as fallback
55+
browser, browser_type = self._try_connect_firefox(p, port, verbose, console)
56+
57+
if not browser:
58+
result.status = ConnectionStatus.CONNECTION_FAILED
59+
result.error = "Failed to connect to any browser type"
60+
return result
61+
62+
result.browser = browser
63+
result.browser_type = browser_type
64+
result.success = True
65+
result.status = ConnectionStatus.SUCCESS
66+
67+
# Create context and page
68+
if console and verbose:
69+
console.print(f"[dim] Creating browser context...[/dim]")
70+
71+
try:
72+
context = browser.new_context()
73+
page = context.new_page()
74+
75+
result.context = context
76+
result.page = page
77+
78+
if console and verbose:
79+
console.print(f"[green] ✓ Browser context created[/green]")
80+
except Exception as e:
81+
result.status = ConnectionStatus.CONTEXT_FAILED
82+
result.error = f"Failed to create context: {e}"
83+
if console and verbose:
84+
console.print(f"[red] ✗ Failed to create browser context: {e}[/red]")
85+
return result
86+
87+
return result
88+
89+
except Exception as e:
90+
result.status = ConnectionStatus.ERROR
91+
result.error = str(e)
92+
if console and verbose:
93+
console.print(f"[red] ✗ CDP connection error: {e}[/red]")
94+
return result
95+
96+
def _try_connect_chrome(
97+
self,
98+
playwright: Any,
99+
port: int,
100+
verbose: bool,
101+
console: Optional[Any],
102+
) -> tuple[Optional[Any], str]:
103+
"""Try to connect to Chrome/Chromium browser."""
104+
try:
105+
browser = playwright.chromium.connect_over_cdp(f"http://localhost:{port}")
106+
if console and verbose:
107+
console.print(f"[green] ✓ Connected to Chrome/Chromium via CDP[/green]")
108+
return browser, "chromium"
109+
except Exception as chrome_err:
110+
if console and verbose:
111+
console.print(f"[dim] Chromium CDP failed: {str(chrome_err)[:50]}...[/dim]")
112+
return None, ""
113+
114+
def _try_connect_firefox(
115+
self,
116+
playwright: Any,
117+
port: int,
118+
verbose: bool,
119+
console: Optional[Any],
120+
) -> tuple[Optional[Any], str]:
121+
"""Try to connect to Firefox browser."""
122+
try:
123+
browser = playwright.firefox.connect_over_cdp(f"http://localhost:{port}")
124+
if console and verbose:
125+
console.print(f"[green] ✓ Connected to Firefox via CDP[/green]")
126+
return browser, "firefox"
127+
except Exception as firefox_err:
128+
if console and verbose:
129+
console.print(f"[red] ✗ CDP connection failed for both browsers[/red]")
130+
return None, ""

src/nlp2cmd/browser_manager/cdp_detector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44
import socket
55
import logging
6-
from typing import Optional
6+
from typing import Any, Optional
77

88
from .base import BrowserConfig
99

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Existing Browser Manager - orchestrator for connecting to existing browsers."""
2+
3+
from __future__ import annotations
4+
import logging
5+
from typing import Any, Optional
6+
7+
from .base import BrowserConfig, BrowserConnectionResult, ConnectionStatus
8+
from .cdp_detector import CdpDetector
9+
from .browser_connector import BrowserConnector
10+
from .token_navigator import TokenNavigator, NavigationStatus
11+
12+
log = logging.getLogger("nlp2cmd.browser_manager.existing")
13+
14+
15+
class ExistingBrowserManager:
16+
"""Orchestrator for connecting to existing browser via CDP.
17+
18+
Coordinates CDP detection, browser connection, and navigation.
19+
"""
20+
21+
def __init__(self, config: Optional[BrowserConfig] = None) -> None:
22+
self.config = config or BrowserConfig()
23+
self.cdp_detector = CdpDetector(self.config)
24+
self.browser_connector = BrowserConnector(self.config)
25+
self.token_navigator = TokenNavigator(self.config)
26+
27+
def connect_and_navigate(
28+
self,
29+
verbose: bool = True,
30+
console: Optional[Any] = None,
31+
) -> BrowserConnectionResult:
32+
"""Find existing browser, connect, and navigate to token page.
33+
34+
Args:
35+
verbose: Whether to log detailed output
36+
console: Optional Rich console for formatted output
37+
38+
Returns:
39+
BrowserConnectionResult with connection details and page
40+
"""
41+
# Stage 1: Find CDP port
42+
port = self.cdp_detector.find_cdp_port(verbose=verbose, console=console)
43+
44+
if not port:
45+
result = BrowserConnectionResult()
46+
result.status = ConnectionStatus.NO_CDP
47+
result.error = "No existing browser with CDP found"
48+
return result
49+
50+
# Stage 2: Connect to browser
51+
result = self.browser_connector.connect(port, verbose=verbose, console=console)
52+
53+
if not result.success:
54+
return result
55+
56+
if not result.page:
57+
result.status = ConnectionStatus.CONTEXT_FAILED
58+
result.error = "Browser connected but page creation failed"
59+
return result
60+
61+
# Stage 3: Navigate to tokens page
62+
nav_status, actual_url = self.token_navigator.navigate(
63+
result.page, verbose=verbose, console=console
64+
)
65+
66+
result.actual_url = actual_url
67+
68+
if nav_status == NavigationStatus.FAILED:
69+
result.success = False
70+
result.error = f"Navigation failed, last URL: {actual_url}"
71+
elif nav_status == NavigationStatus.WRONG_PAGE:
72+
# Still proceed - user can navigate manually
73+
log.debug("Navigated to unexpected URL: %s", actual_url)
74+
75+
return result
76+
77+
def get_token_interactive(
78+
self,
79+
result: BrowserConnectionResult,
80+
verbose: bool = True,
81+
console: Optional[Any] = None,
82+
) -> Optional[str]:
83+
"""Get token from user via interactive prompt.
84+
85+
Args:
86+
result: BrowserConnectionResult with connected page
87+
verbose: Whether to log detailed output
88+
console: Optional Rich console for formatted output
89+
90+
Returns:
91+
Token string if entered, None otherwise
92+
"""
93+
if not result.page:
94+
return None
95+
96+
if console and verbose:
97+
console.print(f"[dim] [Token Step 1/4] Navigated to: {result.actual_url}[/dim]")
98+
console.print(f"[cyan] [Token Step 2/4] Showing instructions:[/cyan]")
99+
console.print(" 1. Login to Hugging Face if needed")
100+
console.print(" 2. Click 'New token' button")
101+
console.print(" 3. Set name: 'nlp2cmd'")
102+
console.print(" 4. Select 'Read' role")
103+
console.print(" 5. Click 'Generate token'")
104+
console.print(" 6. Copy the token and paste it here")
105+
else:
106+
print("\n📋 Instructions:")
107+
print(" 1. Login to Hugging Face if needed")
108+
print(" 2. Click 'New token' button")
109+
print(" 3. Set name: 'nlp2cmd'")
110+
print(" 4. Select 'Read' role")
111+
print(" 5. Click 'Generate token'")
112+
print(" 6. Copy the token and paste it here")
113+
114+
if console and verbose:
115+
console.print(f"[cyan] [Token Step 3/4] Waiting for user input...[/cyan]")
116+
console.print(f"[bold yellow] ⚠️ CHECK YOUR TERMINAL - waiting for token input![/bold yellow]")
117+
118+
try:
119+
# Print visible separator
120+
print("\n" + "="*60)
121+
print("🔐 ENTER YOUR HF_TOKEN BELOW 🔐")
122+
print("="*60)
123+
124+
token = input("🔑 Paste HF_TOKEN here: ").strip()
125+
126+
print("="*60)
127+
128+
if console and verbose:
129+
console.print(f"[dim] Input received: {'Yes' if token else 'No'}[/dim]")
130+
131+
if token:
132+
if console and verbose:
133+
console.print(f"[cyan] [Token Step 4/4] Closing browser page...[/cyan]")
134+
135+
try:
136+
result.close()
137+
if console and verbose:
138+
console.print(f"[green] ✓ Browser connection closed[/green]")
139+
except Exception as e:
140+
if console and verbose:
141+
console.print(f"[dim] Note: Could not close cleanly: {e}[/dim]")
142+
143+
return token
144+
else:
145+
if console and verbose:
146+
console.print(f"[yellow] ⚠ No token entered[/yellow]")
147+
148+
except EOFError:
149+
if console and verbose:
150+
console.print(f"[red] ✗ EOFError (no input available)[/red]")
151+
except KeyboardInterrupt:
152+
if console and verbose:
153+
console.print(f"[yellow] ⚠ User cancelled (KeyboardInterrupt)[/yellow]")
154+
except Exception as e:
155+
if console and verbose:
156+
console.print(f"[red] ✗ Error getting input: {e}[/red]")
157+
158+
# Cleanup on failure
159+
result.close()
160+
return None
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Token Navigator for navigating to HF token pages."""
2+
3+
from __future__ import annotations
4+
from enum import Enum
5+
import logging
6+
from typing import Any, Optional
7+
8+
from .base import BrowserConfig
9+
10+
log = logging.getLogger("nlp2cmd.browser_manager.navigator")
11+
12+
13+
class NavigationStatus(Enum):
14+
"""Status of navigation attempt."""
15+
SUCCESS = "success"
16+
LOGIN_REQUIRED = "login_required"
17+
WRONG_PAGE = "wrong_page"
18+
FAILED = "failed"
19+
20+
21+
class TokenNavigator:
22+
"""Navigate to HuggingFace token settings page."""
23+
24+
def __init__(self, config: Optional[BrowserConfig] = None) -> None:
25+
self.config = config or BrowserConfig()
26+
27+
def navigate(
28+
self,
29+
page: Any,
30+
verbose: bool = False,
31+
console: Optional[Any] = None,
32+
) -> tuple[NavigationStatus, str]:
33+
"""Navigate to HF tokens page and verify.
34+
35+
Args:
36+
page: Playwright page object
37+
verbose: Whether to log detailed output
38+
console: Optional Rich console for formatted output
39+
40+
Returns:
41+
Tuple of (NavigationStatus, actual_url)
42+
"""
43+
if console and verbose:
44+
console.print(f"[cyan] → Navigating to huggingface.co...[/cyan]")
45+
46+
actual_url = ""
47+
48+
try:
49+
page.goto(self.config.target_url, timeout=self.config.timeout_ms)
50+
actual_url = page.url
51+
52+
# Verify we reached the expected page
53+
if self.config.target_pattern in actual_url:
54+
if console and verbose:
55+
console.print(f"[green] ✓ Page loaded at correct URL[/green]")
56+
return NavigationStatus.SUCCESS, actual_url
57+
58+
elif self.config.login_pattern in actual_url:
59+
# This is expected if not logged in
60+
if console and verbose:
61+
console.print(f"[yellow] ⚠ Page loaded but requires login first[/yellow]")
62+
console.print(f"[dim] URL: {actual_url}[/dim]")
63+
return NavigationStatus.LOGIN_REQUIRED, actual_url
64+
65+
elif self.config.domain_pattern in actual_url:
66+
# On HF domain but different path
67+
if console and verbose:
68+
console.print(f"[yellow] ⚠ Page loaded on HF domain but different path[/yellow]")
69+
console.print(f"[dim] URL: {actual_url}[/dim]")
70+
return NavigationStatus.SUCCESS, actual_url
71+
72+
else:
73+
# Unexpected URL
74+
if console and verbose:
75+
console.print(f"[red] ✗ Page loaded but unexpected URL[/red]")
76+
console.print(f"[dim] Expected: {self.config.target_pattern}[/dim]")
77+
console.print(f"[dim] Actual: {actual_url}[/dim]")
78+
return NavigationStatus.WRONG_PAGE, actual_url
79+
80+
except Exception as e:
81+
if console and verbose:
82+
console.print(f"[red] ✗ Navigation failed: {e}[/red]")
83+
if actual_url:
84+
console.print(f"[dim] Last URL: {actual_url}[/dim]")
85+
log.debug("Navigation failed: %s", e)
86+
return NavigationStatus.FAILED, actual_url
87+
88+
def is_valid_destination(self, url: str) -> bool:
89+
"""Check if URL is a valid navigation destination.
90+
91+
Args:
92+
url: URL to check
93+
94+
Returns:
95+
True if URL is on HF domain
96+
"""
97+
return self.config.domain_pattern in url

0 commit comments

Comments
 (0)