Skip to content

Commit 9bdad1e

Browse files
authored
Merge pull request #91 from SentienceAPI/sdk_async2
async browser and methods, share utility for loading textensions
2 parents 605f544 + 92d22b8 commit 9bdad1e

File tree

8 files changed

+1705
-34
lines changed

8 files changed

+1705
-34
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,28 @@ for match in result.results:
487487

488488
---
489489

490+
## 🔄 Async API
491+
492+
For asyncio contexts (FastAPI, async frameworks):
493+
494+
```python
495+
from sentience.async_api import AsyncSentienceBrowser, snapshot_async, click_async, find
496+
497+
async def main():
498+
async with AsyncSentienceBrowser() as browser:
499+
await browser.goto("https://example.com")
500+
snap = await snapshot_async(browser)
501+
button = find(snap, "role=button")
502+
if button:
503+
await click_async(browser, button.id)
504+
505+
asyncio.run(main())
506+
```
507+
508+
**See example:** `examples/async_api_demo.py`
509+
510+
---
511+
490512
## 📋 Reference
491513

492514
<details>

examples/async_api_demo.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""
2+
Example: Using Async API for asyncio contexts
3+
4+
This example demonstrates how to use the Sentience SDK's async API
5+
when working with asyncio, FastAPI, or other async frameworks.
6+
7+
To run this example:
8+
python -m examples.async_api_demo
9+
10+
Or if sentience is installed:
11+
python examples/async_api_demo.py
12+
"""
13+
14+
import asyncio
15+
import os
16+
17+
# Import async API functions
18+
from sentience.async_api import (
19+
AsyncSentienceBrowser,
20+
click_async,
21+
find,
22+
press_async,
23+
snapshot_async,
24+
type_text_async,
25+
)
26+
from sentience.models import SnapshotOptions, Viewport
27+
28+
29+
async def basic_async_example():
30+
"""Basic async browser usage with context manager"""
31+
api_key = os.environ.get("SENTIENCE_API_KEY")
32+
33+
# Use async context manager
34+
async with AsyncSentienceBrowser(api_key=api_key, headless=False) as browser:
35+
# Navigate to a page
36+
await browser.goto("https://example.com")
37+
38+
# Take a snapshot (async)
39+
snap = await snapshot_async(browser)
40+
print(f"✅ Found {len(snap.elements)} elements on the page")
41+
42+
# Find an element
43+
link = find(snap, "role=link")
44+
if link:
45+
print(f"Found link: {link.text} (id: {link.id})")
46+
47+
# Click it (async)
48+
result = await click_async(browser, link.id)
49+
print(f"Click result: success={result.success}, outcome={result.outcome}")
50+
51+
52+
async def custom_viewport_example():
53+
"""Example using custom viewport with Viewport class"""
54+
# Use Viewport class for type safety
55+
custom_viewport = Viewport(width=1920, height=1080)
56+
57+
async with AsyncSentienceBrowser(viewport=custom_viewport, headless=False) as browser:
58+
await browser.goto("https://example.com")
59+
60+
# Verify viewport size
61+
viewport_size = await browser.page.evaluate(
62+
"() => ({ width: window.innerWidth, height: window.innerHeight })"
63+
)
64+
print(f"✅ Viewport: {viewport_size['width']}x{viewport_size['height']}")
65+
66+
67+
async def snapshot_with_options_example():
68+
"""Example using SnapshotOptions with async API"""
69+
async with AsyncSentienceBrowser(headless=False) as browser:
70+
await browser.goto("https://example.com")
71+
72+
# Take snapshot with options
73+
options = SnapshotOptions(
74+
limit=10,
75+
screenshot=False,
76+
show_overlay=False,
77+
)
78+
snap = await snapshot_async(browser, options)
79+
print(f"✅ Snapshot with limit=10: {len(snap.elements)} elements")
80+
81+
82+
async def actions_example():
83+
"""Example of all async actions"""
84+
async with AsyncSentienceBrowser(headless=False) as browser:
85+
await browser.goto("https://example.com")
86+
87+
# Take snapshot
88+
snap = await snapshot_async(browser)
89+
90+
# Find a textbox if available
91+
textbox = find(snap, "role=textbox")
92+
if textbox:
93+
# Type text (async)
94+
result = await type_text_async(browser, textbox.id, "Hello, World!")
95+
print(f"✅ Typed text: success={result.success}")
96+
97+
# Press a key (async)
98+
result = await press_async(browser, "Enter")
99+
print(f"✅ Pressed Enter: success={result.success}")
100+
101+
102+
async def from_existing_context_example():
103+
"""Example using from_existing() with existing Playwright context"""
104+
from playwright.async_api import async_playwright
105+
106+
async with async_playwright() as p:
107+
# Create your own Playwright context
108+
context = await p.chromium.launch_persistent_context("", headless=True)
109+
110+
try:
111+
# Create SentienceBrowser from existing context
112+
browser = await AsyncSentienceBrowser.from_existing(context)
113+
await browser.goto("https://example.com")
114+
115+
# Use Sentience SDK functions
116+
snap = await snapshot_async(browser)
117+
print(f"✅ Using existing context: {len(snap.elements)} elements")
118+
finally:
119+
await context.close()
120+
121+
122+
async def from_existing_page_example():
123+
"""Example using from_page() with existing Playwright page"""
124+
from playwright.async_api import async_playwright
125+
126+
async with async_playwright() as p:
127+
browser_instance = await p.chromium.launch(headless=True)
128+
context = await browser_instance.new_context()
129+
page = await context.new_page()
130+
131+
try:
132+
# Create SentienceBrowser from existing page
133+
sentience_browser = await AsyncSentienceBrowser.from_page(page)
134+
await sentience_browser.goto("https://example.com")
135+
136+
# Use Sentience SDK functions
137+
snap = await snapshot_async(sentience_browser)
138+
print(f"✅ Using existing page: {len(snap.elements)} elements")
139+
finally:
140+
await context.close()
141+
await browser_instance.close()
142+
143+
144+
async def multiple_browsers_example():
145+
"""Example running multiple browsers concurrently"""
146+
147+
async def process_site(url: str):
148+
async with AsyncSentienceBrowser(headless=True) as browser:
149+
await browser.goto(url)
150+
snap = await snapshot_async(browser)
151+
return {"url": url, "elements": len(snap.elements)}
152+
153+
# Process multiple sites concurrently
154+
urls = [
155+
"https://example.com",
156+
"https://httpbin.org/html",
157+
]
158+
159+
results = await asyncio.gather(*[process_site(url) for url in urls])
160+
for result in results:
161+
print(f"✅ {result['url']}: {result['elements']} elements")
162+
163+
164+
async def main():
165+
"""Run all examples"""
166+
print("=== Basic Async Example ===")
167+
await basic_async_example()
168+
169+
print("\n=== Custom Viewport Example ===")
170+
await custom_viewport_example()
171+
172+
print("\n=== Snapshot with Options Example ===")
173+
await snapshot_with_options_example()
174+
175+
print("\n=== Actions Example ===")
176+
await actions_example()
177+
178+
print("\n=== From Existing Context Example ===")
179+
await from_existing_context_example()
180+
181+
print("\n=== From Existing Page Example ===")
182+
await from_existing_page_example()
183+
184+
print("\n=== Multiple Browsers Concurrent Example ===")
185+
await multiple_browsers_example()
186+
187+
print("\n✅ All async examples completed!")
188+
189+
190+
if __name__ == "__main__":
191+
# Run the async main function
192+
asyncio.run(main())

screenshot.png

141 Bytes
Loading

sentience/_extension_loader.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
Shared extension loading logic for sync and async implementations
3+
"""
4+
5+
from pathlib import Path
6+
7+
8+
def find_extension_path() -> Path:
9+
"""
10+
Find Sentience extension directory (shared logic for sync and async).
11+
12+
Checks multiple locations:
13+
1. sentience/extension/ (installed package)
14+
2. ../sentience-chrome (development/monorepo)
15+
16+
Returns:
17+
Path to extension directory
18+
19+
Raises:
20+
FileNotFoundError: If extension not found in any location
21+
"""
22+
# 1. Try relative to this file (installed package structure)
23+
# sentience/_extension_loader.py -> sentience/extension/
24+
package_ext_path = Path(__file__).parent / "extension"
25+
26+
# 2. Try development root (if running from source repo)
27+
# sentience/_extension_loader.py -> ../sentience-chrome
28+
dev_ext_path = Path(__file__).parent.parent.parent / "sentience-chrome"
29+
30+
if package_ext_path.exists() and (package_ext_path / "manifest.json").exists():
31+
return package_ext_path
32+
elif dev_ext_path.exists() and (dev_ext_path / "manifest.json").exists():
33+
return dev_ext_path
34+
else:
35+
raise FileNotFoundError(
36+
f"Extension not found. Checked:\n"
37+
f"1. {package_ext_path}\n"
38+
f"2. {dev_ext_path}\n"
39+
"Make sure the extension is built and 'sentience/extension' directory exists."
40+
)

0 commit comments

Comments
 (0)