browserfabric 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ """
2
+ browserfabric - Python SDK for BrowserFabric API
3
+
4
+ Simple Usage:
5
+ import browserfabric
6
+
7
+ # Super simple interface
8
+ mcp = browserfabric.MCP("browseruse")
9
+ session = await mcp.create_session()
10
+ await session.navigate("https://example.com")
11
+ await session.screenshot("page.png")
12
+
13
+ # Or even simpler with context manager
14
+ async with browserfabric.browser() as browser:
15
+ await browser.navigate("https://example.com")
16
+ await browser.click("#button")
17
+ await browser.screenshot("result.png")
18
+
19
+ # One-liner screenshot
20
+ await browserfabric.screenshot("https://example.com", "example.png")
21
+ """
22
+
23
+ __version__ = "1.0.0"
24
+ __author__ = "BrowserFabric Contributors"
25
+ __email__ = "info@example.com"
26
+
27
+ # Simple client interface
28
+ from .client import MCP, MCPError, browser, ensure_server_running, screenshot, test_form
29
+
30
+ __all__ = [
31
+ # Simple client interface
32
+ "MCP",
33
+ "browser",
34
+ "screenshot",
35
+ "test_form",
36
+ "ensure_server_running",
37
+ "MCPError",
38
+ ]
@@ -0,0 +1,646 @@
1
+ """
2
+ Super simple client interface for BrowserFabric.
3
+
4
+ Usage:
5
+ import browserfabric
6
+
7
+ # Simple usage
8
+ mcp = browserfabric.MCP("browseruse")
9
+ session = await mcp.create_session()
10
+ await session.navigate("https://example.com")
11
+ await session.screenshot("page.png")
12
+
13
+ # With persistent context
14
+ session = await mcp.create_session(persist=True)
15
+ await session.navigate("https://twitter.com")
16
+ # ... user logs in ...
17
+ ctx = await session.save_context("twitter-login")
18
+ await session.close()
19
+
20
+ # Restore context later
21
+ session = await mcp.create_session(context_id=ctx["context_id"])
22
+ # Already logged in!
23
+
24
+ # Or even simpler
25
+ async with browserfabric.browser() as browser:
26
+ await browser.navigate("https://example.com")
27
+ await browser.click("#button")
28
+ await browser.screenshot("result.png")
29
+ """
30
+
31
+ import asyncio
32
+ import base64
33
+ import os
34
+ from contextlib import asynccontextmanager
35
+ from pathlib import Path
36
+ from typing import Any, Dict, List, Optional
37
+
38
+ import httpx
39
+
40
+
41
+ class MCPError(Exception):
42
+ """Base exception for MCP operations."""
43
+
44
+ pass
45
+
46
+
47
+ class BrowserSession:
48
+ """Represents a browser session with convenient methods."""
49
+
50
+ def __init__(self, client: "MCPClient", session_id: str, ws_url: Optional[str] = None,
51
+ cdp_port: Optional[int] = None):
52
+ self.client = client
53
+ self.session_id = session_id
54
+ self.ws_url = ws_url
55
+ self.cdp_port = cdp_port
56
+ self._closed = False
57
+
58
+ async def navigate(self, url: str, wait_until: str = "domcontentloaded") -> Dict[str, Any]:
59
+ """Navigate to a URL. wait_until can be: domcontentloaded, load, networkidle, commit."""
60
+ if self._closed:
61
+ raise MCPError("Session is closed")
62
+
63
+ result = await self.client._call_tool(
64
+ "navigate", {"url": url, "wait_until": wait_until, "session_id": self.session_id}
65
+ )
66
+
67
+ if not result.get("success"):
68
+ raise MCPError(f"Navigation failed: {result.get('error')}")
69
+
70
+ return result["result"]
71
+
72
+ async def click(self, selector: str, by: str = "css") -> Dict[str, Any]:
73
+ """Click an element."""
74
+ if self._closed:
75
+ raise MCPError("Session is closed")
76
+
77
+ result = await self.client._call_tool(
78
+ "click_element",
79
+ {"selector": selector, "by": by, "session_id": self.session_id},
80
+ )
81
+
82
+ if not result.get("success"):
83
+ raise MCPError(f"Click failed: {result.get('error')}")
84
+
85
+ return result["result"]
86
+
87
+ async def type(self, selector: str, text: str, by: str = "css") -> Dict[str, Any]:
88
+ """Type text into an element."""
89
+ if self._closed:
90
+ raise MCPError("Session is closed")
91
+
92
+ result = await self.client._call_tool(
93
+ "type_text",
94
+ {
95
+ "selector": selector,
96
+ "text": text,
97
+ "by": by,
98
+ "session_id": self.session_id,
99
+ },
100
+ )
101
+
102
+ if not result.get("success"):
103
+ raise MCPError(f"Type failed: {result.get('error')}")
104
+
105
+ return result["result"]
106
+
107
+ async def find(self, selector: str, by: str = "css") -> List[Dict[str, Any]]:
108
+ """Find elements on the page."""
109
+ if self._closed:
110
+ raise MCPError("Session is closed")
111
+
112
+ result = await self.client._call_tool(
113
+ "find_elements",
114
+ {"selector": selector, "by": by, "session_id": self.session_id},
115
+ )
116
+
117
+ if not result.get("success"):
118
+ raise MCPError(f"Find failed: {result.get('error')}")
119
+
120
+ return result["result"].get("elements", [])
121
+
122
+ async def screenshot(
123
+ self, filename: Optional[str] = None, save_dir: str = "screenshots", full_page: bool = False
124
+ ) -> str:
125
+ """Take a screenshot and save it. Set full_page=True for full scrollable page."""
126
+ if self._closed:
127
+ raise MCPError("Session is closed")
128
+
129
+ result = await self.client._call_tool(
130
+ "take_screenshot", {"full_page": full_page, "session_id": self.session_id}
131
+ )
132
+
133
+ if not result.get("success"):
134
+ raise MCPError(f"Screenshot failed: {result.get('error')}")
135
+
136
+ screenshot_b64 = result["result"]["screenshot"]
137
+ screenshot_data = base64.b64decode(screenshot_b64)
138
+
139
+ if not filename:
140
+ from datetime import datetime
141
+
142
+ filename = f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
143
+
144
+ save_path = Path(save_dir)
145
+ save_path.mkdir(exist_ok=True)
146
+
147
+ filepath = save_path / filename
148
+ with open(filepath, "wb") as f:
149
+ f.write(screenshot_data)
150
+
151
+ return str(filepath)
152
+
153
+ async def page_info(self) -> Dict[str, Any]:
154
+ """Get current page information."""
155
+ if self._closed:
156
+ raise MCPError("Session is closed")
157
+
158
+ result = await self.client._call_tool(
159
+ "get_page_info", {"session_id": self.session_id}
160
+ )
161
+
162
+ if not result.get("success"):
163
+ raise MCPError(f"Get page info failed: {result.get('error')}")
164
+
165
+ return result["result"]
166
+
167
+ async def observe(self) -> Dict[str, Any]:
168
+ """Get simplified text-based DOM tree of important visible elements with interaction paths."""
169
+ if self._closed:
170
+ raise MCPError("Session is closed")
171
+
172
+ result = await self.client._call_tool(
173
+ "observe", {"session_id": self.session_id}
174
+ )
175
+
176
+ if not result.get("success"):
177
+ raise MCPError(f"Observe failed: {result.get('error')}")
178
+
179
+ return result["result"]
180
+
181
+ async def snapshot(self) -> str:
182
+ """Get the accessibility tree of the page."""
183
+ if self._closed:
184
+ raise MCPError("Session is closed")
185
+
186
+ result = await self.client._call_tool(
187
+ "snapshot", {"session_id": self.session_id}
188
+ )
189
+
190
+ if not result.get("success"):
191
+ raise MCPError(f"Snapshot failed: {result.get('error')}")
192
+
193
+ return result["result"].get("snapshot", "")
194
+
195
+ async def session_info(self) -> Dict[str, Any]:
196
+ """Get session metadata including CDP WebSocket URL."""
197
+ if self._closed:
198
+ raise MCPError("Session is closed")
199
+
200
+ result = await self.client._call_tool(
201
+ "get_session_info", {"session_id": self.session_id}
202
+ )
203
+
204
+ if not result.get("success"):
205
+ raise MCPError(f"Get session info failed: {result.get('error')}")
206
+
207
+ return result["result"]
208
+
209
+ async def wait_for(
210
+ self, selector: str, state: str = "visible", timeout: int = 30, by: str = "css"
211
+ ) -> Dict[str, Any]:
212
+ """Wait for an element to reach a given state (visible, hidden, attached, detached)."""
213
+ if self._closed:
214
+ raise MCPError("Session is closed")
215
+ result = await self.client._call_tool(
216
+ "wait_for",
217
+ {"selector": selector, "state": state, "timeout": timeout, "by": by, "session_id": self.session_id},
218
+ )
219
+ if not result.get("success"):
220
+ raise MCPError(f"Wait failed: {result.get('error')}")
221
+ return result["result"]
222
+
223
+ async def scroll(self, direction: str = "down", amount: int = 500) -> Dict[str, Any]:
224
+ """Scroll the page. Directions: down, up, top, bottom."""
225
+ if self._closed:
226
+ raise MCPError("Session is closed")
227
+ result = await self.client._call_tool(
228
+ "scroll",
229
+ {"direction": direction, "amount": amount, "session_id": self.session_id},
230
+ )
231
+ if not result.get("success"):
232
+ raise MCPError(f"Scroll failed: {result.get('error')}")
233
+ return result["result"]
234
+
235
+ async def press_key(self, key: str) -> Dict[str, Any]:
236
+ """Press a keyboard key (Enter, Tab, Escape, ArrowDown, etc.)."""
237
+ if self._closed:
238
+ raise MCPError("Session is closed")
239
+ result = await self.client._call_tool(
240
+ "press_key", {"key": key, "session_id": self.session_id},
241
+ )
242
+ if not result.get("success"):
243
+ raise MCPError(f"Press key failed: {result.get('error')}")
244
+ return result["result"]
245
+
246
+ async def select_option(self, selector: str, value: str, by: str = "css") -> Dict[str, Any]:
247
+ """Select an option from a dropdown/select element."""
248
+ if self._closed:
249
+ raise MCPError("Session is closed")
250
+ result = await self.client._call_tool(
251
+ "select_option",
252
+ {"selector": selector, "value": value, "by": by, "session_id": self.session_id},
253
+ )
254
+ if not result.get("success"):
255
+ raise MCPError(f"Select failed: {result.get('error')}")
256
+ return result["result"]
257
+
258
+ async def hover(self, selector: str, by: str = "css") -> Dict[str, Any]:
259
+ """Hover over an element."""
260
+ if self._closed:
261
+ raise MCPError("Session is closed")
262
+ result = await self.client._call_tool(
263
+ "hover",
264
+ {"selector": selector, "by": by, "session_id": self.session_id},
265
+ )
266
+ if not result.get("success"):
267
+ raise MCPError(f"Hover failed: {result.get('error')}")
268
+ return result["result"]
269
+
270
+ async def evaluate_js(self, expression: str) -> Any:
271
+ """Execute JavaScript in the page and return the result."""
272
+ if self._closed:
273
+ raise MCPError("Session is closed")
274
+ result = await self.client._call_tool(
275
+ "evaluate_js",
276
+ {"expression": expression, "session_id": self.session_id},
277
+ )
278
+ if not result.get("success"):
279
+ raise MCPError(f"JS eval failed: {result.get('error')}")
280
+ return result["result"].get("result")
281
+
282
+ async def go_back(self) -> Dict[str, Any]:
283
+ """Navigate back in browser history."""
284
+ if self._closed:
285
+ raise MCPError("Session is closed")
286
+ result = await self.client._call_tool(
287
+ "go_back", {"session_id": self.session_id},
288
+ )
289
+ if not result.get("success"):
290
+ raise MCPError(f"Go back failed: {result.get('error')}")
291
+ return result["result"]
292
+
293
+ async def go_forward(self) -> Dict[str, Any]:
294
+ """Navigate forward in browser history."""
295
+ if self._closed:
296
+ raise MCPError("Session is closed")
297
+ result = await self.client._call_tool(
298
+ "go_forward", {"session_id": self.session_id},
299
+ )
300
+ if not result.get("success"):
301
+ raise MCPError(f"Go forward failed: {result.get('error')}")
302
+ return result["result"]
303
+
304
+ async def screenshot_element(self, selector: str, by: str = "css") -> str:
305
+ """Take a screenshot of a specific element. Returns base64 PNG."""
306
+ if self._closed:
307
+ raise MCPError("Session is closed")
308
+ result = await self.client._call_tool(
309
+ "screenshot_element",
310
+ {"selector": selector, "by": by, "session_id": self.session_id},
311
+ )
312
+ if not result.get("success"):
313
+ raise MCPError(f"Element screenshot failed: {result.get('error')}")
314
+ return result["result"]["screenshot"]
315
+
316
+ async def page_summary(self) -> Dict[str, Any]:
317
+ """Get a quick summary of the current page state."""
318
+ if self._closed:
319
+ raise MCPError("Session is closed")
320
+ result = await self.client._call_tool(
321
+ "page_summary", {"session_id": self.session_id},
322
+ )
323
+ if not result.get("success"):
324
+ raise MCPError(f"Page summary failed: {result.get('error')}")
325
+ return result["result"]
326
+
327
+ async def save_context(self, name: str) -> Dict[str, Any]:
328
+ """Save browser context (cookies, localStorage) for later restoration.
329
+
330
+ Args:
331
+ name: Human-readable name for the context (e.g., "twitter-login")
332
+
333
+ Returns:
334
+ Dict with context_id, name, cookies_count, status
335
+ """
336
+ if self._closed:
337
+ raise MCPError("Session is closed")
338
+
339
+ result = await self.client._call_tool(
340
+ "save_context", {"context_name": name, "session_id": self.session_id}
341
+ )
342
+
343
+ if not result.get("success"):
344
+ raise MCPError(f"Save context failed: {result.get('error')}")
345
+
346
+ return result["result"]
347
+
348
+ async def close(self):
349
+ """Close the browser session."""
350
+ if self._closed:
351
+ return
352
+
353
+ try:
354
+ result = await self.client._call_tool(
355
+ "close_session", {"session_id": self.session_id}
356
+ )
357
+
358
+ if not result.get("success"):
359
+ print(f"Warning: Failed to close session: {result.get('error')}")
360
+ finally:
361
+ self._closed = True
362
+
363
+ async def __aenter__(self):
364
+ return self
365
+
366
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
367
+ await self.close()
368
+
369
+
370
+ class MCPClient:
371
+ """Simple client for BrowserFabric services."""
372
+
373
+ def __init__(
374
+ self,
375
+ service_name: str,
376
+ api_key: Optional[str] = None,
377
+ base_url: str = "http://localhost:9000",
378
+ max_retries: int = 3,
379
+ timeout: float = 60.0,
380
+ ):
381
+ self.service_name = service_name
382
+ self.base_url = base_url.rstrip("/")
383
+ self.max_retries = max_retries
384
+ self.timeout = timeout
385
+
386
+ # API key support
387
+ self.headers = {}
388
+ key = api_key or os.getenv("BROWSERFABRIC_API_KEY")
389
+ if key:
390
+ self.headers["Authorization"] = f"Bearer {key}"
391
+
392
+ async def _call_tool(
393
+ self, tool_name: str, arguments: Dict[str, Any]
394
+ ) -> Dict[str, Any]:
395
+ """Call a tool on the service with automatic retry on transient errors."""
396
+ session_id = (
397
+ arguments.pop("session_id", None) if "session_id" in arguments else None
398
+ )
399
+
400
+ request_data = {"tool_name": tool_name, "arguments": arguments}
401
+ if session_id:
402
+ request_data["session_id"] = session_id
403
+
404
+ last_error = None
405
+ for attempt in range(self.max_retries):
406
+ try:
407
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
408
+ response = await client.post(
409
+ f"{self.base_url}/api/v1/services/{self.service_name}/call",
410
+ headers=self.headers,
411
+ json=request_data,
412
+ )
413
+
414
+ # Don't retry client errors (4xx) except 429 (rate limit)
415
+ if response.status_code == 429:
416
+ retry_after = int(response.headers.get("Retry-After", 5))
417
+ await asyncio.sleep(retry_after)
418
+ continue
419
+
420
+ if response.status_code != 200:
421
+ raise MCPError(f"HTTP {response.status_code}: {response.text}")
422
+
423
+ return response.json()
424
+
425
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.WriteTimeout) as e:
426
+ last_error = e
427
+ if attempt < self.max_retries - 1:
428
+ wait = min(2 ** attempt, 10) # 1s, 2s, 4s... max 10s
429
+ await asyncio.sleep(wait)
430
+ continue
431
+ raise MCPError(f"Connection failed after {self.max_retries} attempts: {e}")
432
+
433
+ raise MCPError(f"Failed after {self.max_retries} attempts: {last_error}")
434
+
435
+ async def create_session(
436
+ self,
437
+ headless: bool = True,
438
+ timeout: int = 30,
439
+ context_id: Optional[str] = None,
440
+ persist: bool = False,
441
+ ) -> BrowserSession:
442
+ """Create a new browser session.
443
+
444
+ Args:
445
+ headless: Run browser in headless mode
446
+ timeout: Default timeout in seconds
447
+ context_id: Optional context ID to restore cookies/localStorage from
448
+ persist: If True, auto-save cookies/localStorage when session closes
449
+
450
+ Returns:
451
+ BrowserSession with ws_url for CDP connections
452
+ """
453
+ args = {"headless": headless, "timeout": timeout, "persist": persist}
454
+ if context_id:
455
+ args["context_id"] = context_id
456
+
457
+ result = await self._call_tool("create_session", args)
458
+
459
+ if not result.get("success"):
460
+ raise MCPError(f"Failed to create session: {result.get('error')}")
461
+
462
+ data = result["result"]
463
+ return BrowserSession(
464
+ self,
465
+ data["session_id"],
466
+ ws_url=data.get("ws_url"),
467
+ cdp_port=data.get("cdp_port"),
468
+ )
469
+
470
+ async def list_sessions(self) -> List[Dict[str, Any]]:
471
+ """List all active browser sessions for the current user."""
472
+ result = await self._call_tool("list_sessions", {})
473
+ if not result.get("success"):
474
+ raise MCPError(f"Failed to list sessions: {result.get('error')}")
475
+ return result["result"].get("sessions", [])
476
+
477
+ async def list_contexts(self) -> List[Dict[str, Any]]:
478
+ """List all saved persistent contexts."""
479
+ result = await self._call_tool("list_contexts", {})
480
+ if not result.get("success"):
481
+ raise MCPError(f"Failed to list contexts: {result.get('error')}")
482
+ return result["result"].get("contexts", [])
483
+
484
+ async def delete_context(self, context_id: str) -> Dict[str, Any]:
485
+ """Delete a saved persistent context."""
486
+ result = await self._call_tool("delete_context", {"context_id": context_id})
487
+ if not result.get("success"):
488
+ raise MCPError(f"Failed to delete context: {result.get('error')}")
489
+ return result["result"]
490
+
491
+ async def health_check(self) -> Dict[str, Any]:
492
+ """Check if the service is healthy."""
493
+ async with httpx.AsyncClient(timeout=30.0) as client:
494
+ response = await client.get(f"{self.base_url}/health")
495
+
496
+ if response.status_code != 200:
497
+ raise MCPError(f"Health check failed: HTTP {response.status_code}")
498
+
499
+ return response.json()
500
+
501
+ async def is_healthy(self) -> bool:
502
+ """Quick health check. Returns True if server is reachable, False otherwise."""
503
+ try:
504
+ await self.health_check()
505
+ return True
506
+ except Exception:
507
+ return False
508
+
509
+ async def wait_for_ready(self, timeout: int = 30, interval: float = 1.0) -> bool:
510
+ """Wait for the server to become ready. Returns True if ready within timeout."""
511
+ import time
512
+ start = time.monotonic()
513
+ while time.monotonic() - start < timeout:
514
+ if await self.is_healthy():
515
+ return True
516
+ await asyncio.sleep(interval)
517
+ return False
518
+
519
+ async def list_tools(self) -> List[Dict[str, Any]]:
520
+ """List available tools for this service."""
521
+ async with httpx.AsyncClient(timeout=30.0) as client:
522
+ response = await client.get(
523
+ f"{self.base_url}/api/v1/services/{self.service_name}/tools",
524
+ headers=self.headers,
525
+ )
526
+
527
+ if response.status_code != 200:
528
+ raise MCPError(f"Failed to list tools: HTTP {response.status_code}")
529
+
530
+ result = response.json()
531
+ return result.get("tools", [])
532
+
533
+
534
+ class MCP:
535
+ """Main MCP interface - super simple to use!"""
536
+
537
+ def __init__(
538
+ self,
539
+ service_name: str = "browseruse",
540
+ api_key: Optional[str] = None,
541
+ base_url: str = "http://localhost:9000",
542
+ ):
543
+ self.client = MCPClient(service_name, api_key, base_url)
544
+
545
+ async def create_session(
546
+ self,
547
+ headless: bool = True,
548
+ timeout: int = 30,
549
+ context_id: Optional[str] = None,
550
+ persist: bool = False,
551
+ ) -> BrowserSession:
552
+ """Create a new browser session."""
553
+ return await self.client.create_session(headless, timeout, context_id, persist)
554
+
555
+ async def list_contexts(self) -> List[Dict[str, Any]]:
556
+ """List all saved persistent contexts."""
557
+ return await self.client.list_contexts()
558
+
559
+ async def delete_context(self, context_id: str) -> Dict[str, Any]:
560
+ """Delete a saved persistent context."""
561
+ return await self.client.delete_context(context_id)
562
+
563
+ async def health_check(self) -> Dict[str, Any]:
564
+ """Check service health."""
565
+ return await self.client.health_check()
566
+
567
+ async def list_tools(self) -> List[Dict[str, Any]]:
568
+ """List available tools."""
569
+ return await self.client.list_tools()
570
+
571
+ async def quick_screenshot(self, url: str, filename: Optional[str] = None) -> str:
572
+ """Quick screenshot of any URL."""
573
+ async with await self.create_session() as session:
574
+ await session.navigate(url)
575
+ await asyncio.sleep(2)
576
+ return await session.screenshot(filename)
577
+
578
+ async def quick_navigate(self, url: str) -> BrowserSession:
579
+ """Quick navigation - returns session for further use."""
580
+ session = await self.create_session()
581
+ await session.navigate(url)
582
+ return session
583
+
584
+
585
+ @asynccontextmanager
586
+ async def browser(
587
+ api_key: Optional[str] = None,
588
+ base_url: str = "http://localhost:9000",
589
+ headless: bool = True,
590
+ context_id: Optional[str] = None,
591
+ persist: bool = False,
592
+ ):
593
+ """
594
+ Super convenient context manager for browser automation.
595
+
596
+ Usage:
597
+ async with browserfabric.browser() as browser:
598
+ await browser.navigate("https://example.com")
599
+ await browser.click("#button")
600
+ await browser.screenshot("result.png")
601
+
602
+ # With persistent context
603
+ async with browserfabric.browser(persist=True) as browser:
604
+ await browser.navigate("https://twitter.com")
605
+ # ... cookies auto-saved on close
606
+ """
607
+ mcp = MCP("browseruse", api_key, base_url)
608
+ session = await mcp.create_session(
609
+ headless=headless, context_id=context_id, persist=persist
610
+ )
611
+ try:
612
+ yield session
613
+ finally:
614
+ await session.close()
615
+
616
+
617
+ async def screenshot(
618
+ url: str, filename: Optional[str] = None, api_key: Optional[str] = None
619
+ ) -> str:
620
+ """Take a quick screenshot of any URL."""
621
+ mcp = MCP("browseruse", api_key)
622
+ return await mcp.quick_screenshot(url, filename)
623
+
624
+
625
+ async def test_form(
626
+ url: str, form_data: Dict[str, str], api_key: Optional[str] = None
627
+ ) -> str:
628
+ """Quick form testing."""
629
+ async with browser(api_key) as session:
630
+ await session.navigate(url)
631
+
632
+ for selector, value in form_data.items():
633
+ await session.type(selector, value)
634
+
635
+ return await session.screenshot("form_test.png")
636
+
637
+
638
+ async def ensure_server_running():
639
+ """Ensure BrowserFabric server is running."""
640
+ try:
641
+ mcp = MCP("browseruse")
642
+ await mcp.health_check()
643
+ return True
644
+ except Exception:
645
+ print("BrowserFabric server not running. Start it with: browserfabric serve")
646
+ return False
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: browserfabric
3
+ Version: 1.0.0
4
+ Summary: BrowserFabric Python SDK - Client library for BrowserFabric browser automation API
5
+ Author-email: Raphael Shu <raphael@uaca.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/acenta-ai/remote_browser
8
+ Project-URL: Documentation, https://browserfabric.com/docs
9
+ Project-URL: Repository, https://github.com/acenta-ai/remote_browser
10
+ Project-URL: Issues, https://github.com/acenta-ai/remote_browser/issues
11
+ Keywords: browser,automation,api,web,crawling,sdk,client,playwright,cdp,headless
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: httpx>=0.25.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
28
+ Requires-Dist: black>=23.9.0; extra == "dev"
29
+ Requires-Dist: isort>=5.12.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.6.0; extra == "dev"
31
+
32
+ # BrowserFabric
33
+
34
+ Python SDK for **BrowserFabric** — cloud browser automation API with persistent sessions and CDP access.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install browserfabric
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ import browserfabric
46
+ import asyncio
47
+
48
+ async def main():
49
+ async with browserfabric.browser() as session:
50
+ await session.navigate("https://example.com")
51
+ await session.click("a")
52
+ await session.screenshot("result.png")
53
+
54
+ asyncio.run(main())
55
+ ```
56
+
57
+ ## Authentication
58
+
59
+ Set your API key as an environment variable or pass it directly:
60
+
61
+ ```python
62
+ # Via environment variable
63
+ export BROWSERFABRIC_API_KEY=bf_your_key_here
64
+
65
+ # Or pass directly
66
+ async with browserfabric.browser(api_key="bf_...") as session:
67
+ ...
68
+ ```
69
+
70
+ ## Persistent Sessions
71
+
72
+ Save and restore browser state (cookies, localStorage) across sessions:
73
+
74
+ ```python
75
+ # Save state on close
76
+ async with browserfabric.browser(persist=True) as session:
77
+ await session.navigate("https://app.example.com/login")
78
+ await session.type("#email", "user@example.com")
79
+ await session.type("#password", "secret")
80
+ await session.click("#submit")
81
+ ctx = await session.save_context("my-app-login")
82
+
83
+ # Restore later
84
+ async with browserfabric.browser(context_id=ctx["context_id"]) as session:
85
+ # Already logged in
86
+ await session.navigate("https://app.example.com/dashboard")
87
+ ```
88
+
89
+ ## CDP WebSocket
90
+
91
+ Connect with Playwright directly for full browser control:
92
+
93
+ ```python
94
+ from playwright.async_api import async_playwright
95
+
96
+ # Get the CDP WebSocket URL from session info
97
+ info = await session.session_info()
98
+ ws_url = info["ws_url"]
99
+
100
+ # Connect with Playwright
101
+ pw = await async_playwright().start()
102
+ browser = await pw.chromium.connect_over_cdp(ws_url)
103
+ page = browser.contexts[0].pages[0]
104
+ await page.goto("https://example.com")
105
+ ```
106
+
107
+ ## Available Methods
108
+
109
+ | Method | Description |
110
+ |--------|-------------|
111
+ | `navigate(url)` | Go to a URL |
112
+ | `click(selector)` | Click an element |
113
+ | `type(selector, text)` | Type into an input |
114
+ | `find(selector)` | Find elements |
115
+ | `screenshot(filename)` | Take a screenshot |
116
+ | `observe()` | Get interactive DOM elements |
117
+ | `snapshot()` | Get accessibility tree |
118
+ | `page_info()` | Get page URL and title |
119
+ | `session_info()` | Get session metadata + CDP URL |
120
+ | `save_context(name)` | Save browser state |
121
+ | `close()` | Close the session |
122
+
123
+ ## Self-Hosted
124
+
125
+ Point to your own BrowserFabric instance:
126
+
127
+ ```python
128
+ async with browserfabric.browser(base_url="http://your-server:9000") as session:
129
+ ...
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,6 @@
1
+ browserfabric/__init__.py,sha256=_atiyYccmWOmm5kQDvE3qD_oMv9MQWLNkKrKrxmPP04,1000
2
+ browserfabric/client.py,sha256=MOEmcZTyXCDWtbw3zUjehRDUWgZJk5VvvvxTDzOs6g0,22803
3
+ browserfabric-1.0.0.dist-info/METADATA,sha256=2Yz87b6NVbsECvhuOVfx3y3MOIcjPYAZFwh-vbGD_NM,3936
4
+ browserfabric-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ browserfabric-1.0.0.dist-info/top_level.txt,sha256=_GY9asze9UyTLWQHgHr_kmbyl0p6HdK_DG9O1XantGY,14
6
+ browserfabric-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ browserfabric