Skip to content

Commit e632393

Browse files
Den 204 use dendrite exceptions in sdk (#32)
* update ask-page exception handling and api call * add new exceptions and rename old * refactor return None instead of exception * update download exception * fix missing none check for page click and fill * make screenshot optional for exception * change default retry count to 2 * update default kwarg for page click and fill * update default get_element retry count to 2 from 3 * rewrite Exception to DendriteException Co-authored-by: Paul Sanders <[email protected]>
1 parent 6de2f41 commit e632393

15 files changed

+188
-176
lines changed

dendrite_sdk/__init__.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
from ._core.dendrite_element import DendriteElement
44
from ._core.dendrite_page import DendritePage
55
from ._core.models.response import DendriteElementsResponse
6-
from ._exceptions.dendrite_exception import DendriteException
7-
from ._exceptions.incorrect_outcome_exception import IncorrectOutcomeException
6+
87

98
logger.disable("dendrite_python_sdk")
109

@@ -13,6 +12,4 @@
1312
"DendriteElement",
1413
"DendritePage",
1514
"DendriteElementsResponse",
16-
"DendriteException",
17-
"IncorrectOutcomeException",
1815
]

dendrite_sdk/_api/browser_api_client.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from loguru import logger
44
from dendrite_sdk._core.models.authentication import AuthSession
5+
from dendrite_sdk._api.response.get_element_response import GetElementResponse
56
from dendrite_sdk._api.dto.ask_page_dto import AskPageDTO
67
from dendrite_sdk._api.dto.authenticate_dto import AuthenticateDTO
78
from dendrite_sdk._api.dto.get_elements_dto import GetElementsDTO
@@ -33,11 +34,13 @@ async def upload_auth_session(self, dto: UploadAuthSessionDTO):
3334
"actions/upload-auth-session", data=dto.dict(), method="POST"
3435
)
3536

36-
async def get_interactions_selector(self, dto: GetElementsDTO) -> dict:
37+
async def get_interactions_selector(
38+
self, dto: GetElementsDTO
39+
) -> GetElementResponse:
3740
res = await self.send_request(
3841
"actions/get-interaction-selector", data=dto.dict(), method="POST"
3942
)
40-
return res.json()
43+
return GetElementResponse(**res.json())
4144

4245
async def make_interaction(self, dto: MakeInteractionDTO) -> InteractionResponse:
4346
res = await self.send_request(
@@ -67,7 +70,9 @@ async def ask_page(self, dto: AskPageDTO) -> AskPageResponse:
6770
)
6871
res_dict = res.json()
6972
return AskPageResponse(
70-
description=res_dict["description"], return_data=res_dict["return_data"]
73+
status=res_dict["status"],
74+
description=res_dict["description"],
75+
return_data=res_dict["return_data"],
7176
)
7277

7378
async def try_run_cached(
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from typing import Generic, TypeVar
1+
from typing import Generic, Literal, TypeVar
22
from pydantic import BaseModel
33

44

55
T = TypeVar("T")
66

77

88
class AskPageResponse(BaseModel, Generic[T]):
9+
status: Literal["success", "error"]
910
return_data: T
1011
description: str
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from typing import List, Optional
2+
3+
from pydantic import BaseModel
4+
5+
6+
class GetElementResponse(BaseModel):
7+
selectors: Optional[List[str]] = None
8+
message: str = ""

dendrite_sdk/_common/constants.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"--font-render-hinting=none",
6565
"--disable-logging",
6666
"--enable-surface-synchronization",
67-
#"--run-all-compositor-stages-before-draw",
67+
# "--run-all-compositor-stages-before-draw",
6868
"--disable-threaded-animation",
6969
"--disable-threaded-scrolling",
7070
"--disable-checker-imaging",

dendrite_sdk/_common/event_sync.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
from typing import Generic, Optional, TypeVar
33

4+
from dendrite_sdk._exceptions.dendrite_exception import DendriteException
5+
46

57
T = TypeVar("T")
68

@@ -65,7 +67,7 @@ async def get_data(self, timeout: float = 30000) -> T:
6567
try:
6668
await asyncio.wait_for(self.event.wait(), timeout * 0.001)
6769
if not self.data:
68-
raise Exception(f"No {self.data.__class__.__name__} was found.")
70+
raise DendriteException(f"No {self.data.__class__.__name__} was found.")
6971
data = self.data
7072

7173
self.data = None

dendrite_sdk/_core/_base_browser.py

+24-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from abc import ABC, abstractmethod
22
import sys
3-
from typing import Any, Generic, List, Optional, TypeVar, Union
3+
from typing import Any, List, Optional, Union
44
from uuid import uuid4
55
import os
66
from loguru import logger
@@ -21,13 +21,17 @@
2121

2222
from dendrite_sdk._core.dendrite_page import DendritePage
2323
from dendrite_sdk._common.constants import STEALTH_ARGS
24-
from dendrite_sdk._core.models.download_interface import DownloadInterface
2524
from dendrite_sdk._core.models.authentication import (
2625
AuthSession,
2726
)
2827
from dendrite_sdk._core.models.llm_config import LLMConfig
2928
from dendrite_sdk._api.browser_api_client import BrowserAPIClient
30-
from dendrite_sdk._exceptions.dendrite_exception import BrowserNotLaunchedError
29+
from dendrite_sdk._exceptions.dendrite_exception import (
30+
BrowserNotLaunchedError,
31+
DendriteException,
32+
IncorrectOutcomeError,
33+
MissingApiKeyError,
34+
)
3135

3236

3337
class BaseDendriteBrowser(ABC):
@@ -81,17 +85,23 @@ def __init__(
8185
if not dendrite_api_key or dendrite_api_key == "":
8286
dendrite_api_key = os.environ.get("DENDRITE_API_KEY", "")
8387
if not dendrite_api_key or dendrite_api_key == "":
84-
raise Exception("Dendrite API key is required to use DendriteBrowser")
88+
raise MissingApiKeyError(
89+
"Dendrite API key is required to use DendriteBrowser"
90+
)
8591

8692
if not anthropic_api_key or anthropic_api_key == "":
8793
anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY", "")
8894
if anthropic_api_key == "":
89-
raise Exception("Anthropic API key is required to use DendriteBrowser")
95+
raise MissingApiKeyError(
96+
"Anthropic API key is required to use DendriteBrowser"
97+
)
9098

9199
if not openai_api_key or openai_api_key == "":
92100
openai_api_key = os.environ.get("OPENAI_API_KEY", "")
93101
if not openai_api_key or openai_api_key == "":
94-
raise Exception("OpenAI API key is required to use DendriteBrowser")
102+
raise MissingApiKeyError(
103+
"OpenAI API key is required to use DendriteBrowser"
104+
)
95105

96106
self._id = uuid4().hex
97107
self._auth_data: Optional[AuthSession] = None
@@ -188,8 +198,8 @@ async def goto(
188198
try:
189199
prompt = f"We are checking if we have arrived on the expected type of page. If it is apparent that we have arrived on the wrong page, output an error. Here is the description: '{expected_page}'"
190200
await active_page.ask(prompt, bool)
191-
except Exception as e:
192-
raise Exception(f"Incorrect navigation, reason: {e}")
201+
except DendriteException as e:
202+
raise IncorrectOutcomeError(f"Incorrect navigation, reason: {e}")
193203

194204
return active_page
195205

@@ -265,7 +275,7 @@ async def add_cookies(self, cookies):
265275
Exception: If the browser context is not initialized.
266276
"""
267277
if not self.browser_context:
268-
raise Exception("Browser context not initialized")
278+
raise DendriteException("Browser context not initialized")
269279

270280
await self.browser_context.add_cookies(cookies)
271281

@@ -292,9 +302,11 @@ async def close(self):
292302
)
293303
await self._browser_api_client.upload_auth_session(dto)
294304
await self.browser_context.close()
295-
296-
if self._playwright:
297-
await self._playwright.stop()
305+
try:
306+
if self._playwright:
307+
await self._playwright.stop()
308+
except AttributeError:
309+
pass
298310

299311
def _is_launched(self):
300312
"""

dendrite_sdk/_core/_utils.py

-32
Original file line numberDiff line numberDiff line change
@@ -79,35 +79,3 @@ def get_frame_context(page: Page, iframe_path: str) -> Union[FrameLocator, Page]
7979
for iframe_id in iframe_path_list:
8080
frame_context = frame_context.frame_locator(f"[tf623_id='{iframe_id}']")
8181
return frame_context
82-
83-
84-
async def get_all_elements_from_selector(
85-
page: "DendritePage", selector: str
86-
) -> List[DendriteElement]:
87-
dendrite_elements: List[DendriteElement] = []
88-
soup = await page._get_soup()
89-
elements = soup.select(selector)
90-
91-
for element in elements:
92-
frame = page._get_context(element)
93-
d_id = element.get("d-id", "")
94-
locator = frame.locator(f"xpath=//*[@d-id='{d_id}']")
95-
96-
if not d_id:
97-
continue
98-
99-
if isinstance(d_id, list):
100-
d_id = d_id[0]
101-
102-
dendrite_elements.append(
103-
DendriteElement(
104-
d_id,
105-
locator,
106-
page.dendrite_browser,
107-
)
108-
)
109-
110-
if len(dendrite_elements) == 0:
111-
raise Exception(f"No elements found for selector '{selector}'")
112-
113-
return dendrite_elements

dendrite_sdk/_core/dendrite_element.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
from loguru import logger
99
from playwright.async_api import Locator
1010

11+
from dendrite_sdk._exceptions.dendrite_exception import IncorrectOutcomeError
12+
1113
if TYPE_CHECKING:
1214
from dendrite_sdk._core._base_browser import BaseDendriteBrowser
1315
from dendrite_sdk._core.models.page_diff_information import PageDiffInformation
1416
from dendrite_sdk._core._type_spec import Interaction
1517
from dendrite_sdk._api.response.interaction_response import InteractionResponse
1618
from dendrite_sdk._api.dto.make_interaction_dto import MakeInteractionDTO
17-
from dendrite_sdk._exceptions.incorrect_outcome_exception import (
18-
IncorrectOutcomeException,
19-
)
2019

2120

2221
def perform_action(interaction_type: Interaction):
@@ -81,7 +80,7 @@ async def wrapper(
8180
res = await self._browser_api_client.make_interaction(dto)
8281

8382
if res.status == "failed":
84-
raise IncorrectOutcomeException(
83+
raise IncorrectOutcomeError(
8584
message=res.message,
8685
screenshot_base64=page_delta_information.page_after.screenshot_base64,
8786
)

dendrite_sdk/_core/dendrite_page.py

+43-27
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737

3838

3939
from dendrite_sdk._core._managers.screenshot_manager import ScreenshotManager
40-
from dendrite_sdk._exceptions.dendrite_exception import DendriteException
40+
from dendrite_sdk._exceptions.dendrite_exception import (
41+
DendriteException,
42+
PageConditionNotMet,
43+
)
4144

4245

4346
from dendrite_sdk._core._utils import (
@@ -247,7 +250,7 @@ async def _generate_dendrite_ids(self):
247250
)
248251
tries += 1
249252

250-
raise Exception("Failed to add d-ids to DOM.")
253+
raise DendriteException("Failed to add d-ids to DOM.")
251254

252255
async def scroll_through_entire_page(self) -> None:
253256
"""
@@ -289,30 +292,31 @@ async def wait_for(
289292
) # HACK: Wait for page to load slightly when running first time
290293
while num_attempts < max_retries:
291294
num_attempts += 1
295+
start_time = time.time()
296+
297+
page_information = await self._get_page_information()
298+
prompt = f"Prompt: '{prompt}'\n\nReturn a boolean that determines if the requested information or thing is available on the page."
292299
try:
293-
start_time = time.time()
294-
page_information = await self._get_page_information()
295-
prompt = f"Prompt: '{prompt}'\n\nReturn a boolean that determines if the requested information or thing is available on the page."
296300
res = await self.ask(prompt, bool)
297-
elapsed_time = (
298-
time.time() - start_time
299-
) * 1000 # Convert to milliseconds
300-
301-
if res:
302-
return res
303-
304-
if elapsed_time >= timeout:
305-
# If the response took longer than the timeout, continue immediately
306-
continue
307-
else:
308-
# Otherwise, wait for the remaining time
309-
await asyncio.sleep((timeout - elapsed_time) * 0.001)
310-
except Exception as e:
311-
logger.debug(f"Waited for page, but got this exception: {e}")
301+
except DendriteException as e:
302+
logger.debug(
303+
f"Attempt {num_attempts}/{max_retries} failed: {e.message}"
304+
)
305+
306+
elapsed_time = (time.time() - start_time) * 1000 # Convert to milliseconds
307+
308+
if res:
309+
return res
310+
311+
if elapsed_time >= timeout:
312+
# If the response took longer than the timeout, continue immediately
312313
continue
314+
else:
315+
# Otherwise, wait for the remaining time
316+
await asyncio.sleep((timeout - elapsed_time) * 0.001)
313317

314318
page_information = await self._get_page_information()
315-
raise DendriteException(
319+
raise PageConditionNotMet(
316320
message=f"Retried {max_retries} times but failed to wait for the requested condition.",
317321
screenshot_base64=page_information.screenshot_base64,
318322
)
@@ -326,7 +330,7 @@ async def click(
326330
timeout: int = 2000,
327331
force: bool = False,
328332
*args,
329-
kwargs,
333+
kwargs={},
330334
) -> InteractionResponse:
331335
"""
332336
Clicks an element on the page based on the provided prompt.
@@ -356,6 +360,13 @@ async def click(
356360
max_retries=max_retries,
357361
timeout=timeout,
358362
)
363+
364+
if not element:
365+
raise DendriteException(
366+
message=f"No element found with the prompt: {prompt}",
367+
screenshot_base64="",
368+
)
369+
359370
return await element.click(
360371
expected_outcome=expected_outcome,
361372
timeout=timeout,
@@ -373,7 +384,7 @@ async def fill(
373384
max_retries: int = 3,
374385
timeout: int = 2000,
375386
*args,
376-
kwargs,
387+
kwargs={},
377388
) -> InteractionResponse:
378389
"""
379390
Fills an element on the page with the provided value based on the given prompt.
@@ -404,6 +415,12 @@ async def fill(
404415
timeout=timeout,
405416
)
406417

418+
if not element:
419+
raise DendriteException(
420+
message=f"No element found with the prompt: {prompt}",
421+
screenshot_base64="",
422+
)
423+
407424
return await element.fill(
408425
value,
409426
expected_outcome=expected_outcome,
@@ -473,7 +490,9 @@ async def _expand_iframes(self, page_source: BeautifulSoup):
473490
"""
474491
await expand_iframes(self.playwright_page, page_source)
475492

476-
async def _get_all_elements_from_selector(self, selector: str):
493+
async def _get_all_elements_from_selector(
494+
self, selector: str
495+
) -> List[DendriteElement]:
477496
dendrite_elements: List[DendriteElement] = []
478497
soup = await self._get_soup()
479498
elements = soup.select(selector)
@@ -497,9 +516,6 @@ async def _get_all_elements_from_selector(self, selector: str):
497516
)
498517
)
499518

500-
if len(dendrite_elements) == 0:
501-
raise Exception(f"No elements found for selector '{selector}'")
502-
503519
return dendrite_elements
504520

505521
async def _dump_html(self, path: str) -> None:

0 commit comments

Comments
 (0)