API reference¶
Generated from the apogee-ai-browser source with mkdocstrings. Every symbol below is exported from apogee_ai_browser, so it is part of the supported public surface.
Application · DTOs¶
BenchDTO
dataclass
¶
CrawlDTO
dataclass
¶
CrawlDTO(seed_url: str, max_depth: int = 2, max_pages: int = 10, allowed_domains: tuple[str, ...] = (), browser: str = 'stub')
ExtractDTO
dataclass
¶
NavigateDTO
dataclass
¶
ScreenshotDTO
dataclass
¶
Application · Use cases¶
BenchNavigationUseCase
¶
Synthetic crawl bench against StubBrowser fixtures.
execute
async
¶
Source code in apogee_ai_browser/application/use_cases/bench_navigation_use_case.py
async def execute(self, pages: int) -> dict[str, float]:
if pages <= 0:
raise ValueError("pages must be positive")
fixtures = _synthetic_fixtures(pages)
browser = StubBrowser(fixtures=fixtures)
policy = NavigationPolicy(
allowed_domains=("example.test",),
max_depth=pages + 1,
max_pages=pages,
)
crawl = CrawlUseCase(browser, policy)
start = time.perf_counter()
crawled = await crawl.execute("https://example.test/page-0000")
elapsed = (time.perf_counter() - start) * 1000.0
return {
"pages_target": float(pages),
"pages_crawled": float(len(crawled)),
"elapsed_ms": elapsed,
"pages_per_second": (len(crawled) / elapsed * 1000.0) if elapsed > 0 else 0.0,
}
CrawlUseCase
¶
CrawlUseCase(browser, policy: NavigationPolicy, rate_limiter=None, robots_checker=None)
Bounded BFS crawl. Respects allowlist + max_depth + max_pages.
rate_limiter and robots_checker are optional. When supplied, every URL is checked before fetching.
Source code in apogee_ai_browser/application/use_cases/crawl_use_case.py
execute
async
¶
execute(seed_url: str) -> list[Page]
Source code in apogee_ai_browser/application/use_cases/crawl_use_case.py
async def execute(self, seed_url: str) -> list[Page]:
if not self._policy.is_allowed(seed_url):
raise PolicyViolationException(seed_url, "seed not in allowlist")
await self._browser.open()
visited: set[str] = set()
queue: deque[tuple[str, int]] = deque([(seed_url, 0)])
pages: list[Page] = []
while queue and len(pages) < self._policy.max_pages:
url, depth = queue.popleft()
if url in visited or depth > self._policy.max_depth:
continue
visited.add(url)
if not self._policy.is_allowed(url):
continue
if self._robots is not None and not await self._robots.can_fetch(
url, self._policy.user_agent
):
continue
host = self._policy.host_of(url)
if self._rate_limiter is not None and not await self._rate_limiter.acquire(host):
raise RateLimitedException(host)
try:
page = await self._browser.navigate(url)
except NavigationError:
continue
pages.append(page)
if depth >= self._policy.max_depth:
continue
for href in _LINK_RE.findall(page.html or ""):
target = urljoin(url, href)
if target not in visited:
queue.append((target, depth + 1))
return pages
ExtractTextUseCase
¶
Source code in apogee_ai_browser/application/use_cases/extract_text_use_case.py
execute
async
¶
execute(url: str, selector: ElementSelector | None = None) -> str
Source code in apogee_ai_browser/application/use_cases/extract_text_use_case.py
NavigateUseCase
¶
Source code in apogee_ai_browser/application/use_cases/navigate_use_case.py
execute
async
¶
execute(url: str) -> Page
Source code in apogee_ai_browser/application/use_cases/navigate_use_case.py
async def execute(self, url: str) -> Page:
if self._policy is not None and not await self._policy.is_allowed(url):
from ...domain.exceptions.browser_exceptions import (
PolicyViolationException,
)
raise PolicyViolationException(url, "domain not allowed")
await self._browser.open()
return await self._browser.navigate(url)
ScreenshotUseCase
¶
Source code in apogee_ai_browser/application/use_cases/screenshot_use_case.py
execute
async
¶
Domain¶
BrowserAction
dataclass
¶
BrowserAction(kind: ActionKind, target: str | None = None, value: str | None = None)
BrowserSession
dataclass
¶
BrowserSession(id: str, user_agent: str = 'Apogee-AI-Browser/0.1', visited: set[str] = set(), cookies: dict[str, str] = dict(), current_url: str | None = None)
Element
dataclass
¶
Element(selector: ElementSelector, text: str = '', attributes: dict[str, str] = dict(), tag: str = '')
attributes
class-attribute
instance-attribute
¶
ElementSelector
dataclass
¶
ElementSelector(kind: SelectorKind, value: str)
NavigationPolicy
dataclass
¶
NavigationPolicy(allowed_domains: tuple[str, ...] = (), blocked_domains: tuple[str, ...] = (), max_depth: int = 3, max_pages: int = 50, respect_robots: bool = True, user_agent: str = 'Apogee-AI-Browser/0.1', request_timeout_s: float = 10.0, requests_per_minute_per_host: int = 30, metadata: dict[str, str] = dict())
requests_per_minute_per_host
class-attribute
instance-attribute
¶
metadata
class-attribute
instance-attribute
¶
host_of
¶
is_allowed
¶
Source code in apogee_ai_browser/domain/value_objects/navigation_policy.py
def is_allowed(self, url: str) -> bool:
host = self.host_of(url)
if not host:
return False
if any(host == bad or host.endswith("." + bad) for bad in self.blocked_domains):
return False
if not self.allowed_domains:
return True
return any(
host == allow or host.endswith("." + allow)
for allow in self.allowed_domains
)
NavigationStep
dataclass
¶
NavigationStep(kind: ActionKind, url: str, depth: int = 0, parent_url: str | None = None, timestamp_s: float = 0.0)
Page
dataclass
¶
Page(url: str, title: str = '', text: str = '', html: str = '', status: int = 200, screenshot_path: str | None = None, headers: dict[str, str] = dict(), metadata: dict[str, str] = dict())
Domain · Enums¶
ActionKind
¶
Bases: str, Enum
BrowserKind
¶
SelectorKind
¶
Domain · Exceptions¶
BrowserError
¶
Bases: Exception
Base for apogee-ai-browser errors.
ElementNotFoundException
¶
NavigationError
¶
PolicyViolationException
¶
Bases: BrowserError
Source code in apogee_ai_browser/domain/exceptions/browser_exceptions.py
RateLimitedException
¶
Domain · Protocols (ports)¶
IBrowser
¶
Bases: Protocol
open
async
¶
find
async
¶
find(selector: ElementSelector) -> Element
click
async
¶
click(selector: ElementSelector) -> None
type_text
async
¶
type_text(selector: ElementSelector, value: str) -> None
screenshot
async
¶
close
async
¶
Infrastructure¶
AllowAllRobotsChecker
¶
Default checker — bypasses robots.txt entirely (CI / tests).
can_fetch
async
¶
DomainAllowlistPolicy
¶
DomainAllowlistPolicy(policy: NavigationPolicy)
Wraps NavigationPolicy.is_allowed in an async-friendly check.
Source code in apogee_ai_browser/infrastructure/policies/domain_allowlist.py
is_allowed
async
¶
HttpFetchBrowser
¶
Static HTTP fetch via urllib. No JS, no cookies persistence.
Always available — no extra dependency required.
Source code in apogee_ai_browser/infrastructure/browsers/http_fetch_browser.py
open
async
¶
navigate
async
¶
navigate(url: str) -> Page
Source code in apogee_ai_browser/infrastructure/browsers/http_fetch_browser.py
async def navigate(self, url: str) -> Page:
status, headers, html = await asyncio.to_thread(self._fetch_sync, url)
page = Page(
url=url,
title=_extract_title(html),
text=_extract_text(html),
html=html,
status=status,
headers={k.lower(): v for k, v in headers.items()},
metadata={"links": ",".join(_LINK_RE.findall(html))},
)
self._current = page
return page
find
async
¶
find(selector: ElementSelector) -> Element
Source code in apogee_ai_browser/infrastructure/browsers/http_fetch_browser.py
click
async
¶
click(selector: ElementSelector) -> None
type_text
async
¶
type_text(selector: ElementSelector, value: str) -> None
screenshot
async
¶
close
async
¶
InMemorySessionStore
¶
Tracks browser sessions by id for the lifetime of a process.
Source code in apogee_ai_browser/infrastructure/sessions/in_memory_session_store.py
create
¶
create(session_id: str, user_agent: str = 'Apogee-AI-Browser/0.1') -> BrowserSession
Source code in apogee_ai_browser/infrastructure/sessions/in_memory_session_store.py
get
¶
get(session_id: str) -> BrowserSession | None
drop
¶
count
¶
PerHostRateLimiter
¶
Token bucket per host. Refills linearly at requests_per_minute / 60.
Source code in apogee_ai_browser/infrastructure/policies/per_host_rate_limiter.py
def __init__(self, requests_per_minute: int = 30, burst: int = 1) -> None:
if requests_per_minute <= 0:
raise ValueError("requests_per_minute must be positive")
if burst <= 0:
raise ValueError("burst must be positive")
self._rpm = requests_per_minute
self._capacity = float(requests_per_minute * burst)
self._buckets: dict[str, _Bucket] = {}
acquire
async
¶
PlaywrightBrowser
¶
PlaywrightBrowser(headless: bool = True, engine: str = 'chromium', user_agent: str = 'Apogee-AI-Browser/0.1')
Lazy Playwright adapter — install via extras=playwright.
Supports full JS rendering, click, type, screenshot.
Source code in apogee_ai_browser/infrastructure/browsers/playwright_browser.py
open
async
¶
Source code in apogee_ai_browser/infrastructure/browsers/playwright_browser.py
async def open(self) -> None:
if self._page is not None:
return
try:
from playwright.async_api import async_playwright # type: ignore
except ImportError as exc: # pragma: no cover
raise BrowserError(
"install with `pip install apogee-ai-browser[playwright]` "
"and run `playwright install chromium`"
) from exc
self._playwright = await async_playwright().start()
engine = getattr(self._playwright, self._engine)
self._browser = await engine.launch(headless=self._headless)
self._context = await self._browser.new_context(user_agent=self._user_agent)
self._page = await self._context.new_page()
navigate
async
¶
navigate(url: str) -> Page
Source code in apogee_ai_browser/infrastructure/browsers/playwright_browser.py
async def navigate(self, url: str) -> Page:
await self.open()
try:
response = await self._page.goto(url) # type: ignore[union-attr]
except Exception as exc: # pragma: no cover - depends on Playwright
raise NavigationError(url, str(exc)) from exc
title = await self._page.title() # type: ignore[union-attr]
text = await self._page.evaluate("() => document.body.innerText || ''") # type: ignore[union-attr]
html = await self._page.content() # type: ignore[union-attr]
return Page(
url=url,
title=title or "",
text=text or "",
html=html or "",
status=response.status if response else 0,
)
find
async
¶
find(selector: ElementSelector) -> Element
Source code in apogee_ai_browser/infrastructure/browsers/playwright_browser.py
click
async
¶
click(selector: ElementSelector) -> None
type_text
async
¶
type_text(selector: ElementSelector, value: str) -> None
screenshot
async
¶
close
async
¶
Source code in apogee_ai_browser/infrastructure/browsers/playwright_browser.py
RobotsTxtChecker
¶
Caches robots.txt per host. Defaults to allow on fetch failure.
Source code in apogee_ai_browser/infrastructure/policies/robots_checker.py
can_fetch
async
¶
Source code in apogee_ai_browser/infrastructure/policies/robots_checker.py
async def can_fetch(self, url: str, user_agent: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https") or not parsed.hostname:
return False
host_key = f"{parsed.scheme}://{parsed.hostname}"
if host_key not in self._cache:
self._cache[host_key] = await asyncio.to_thread(
self._fetch_sync, f"{host_key}/robots.txt"
)
return self._cache[host_key].can_fetch(user_agent, url)
StubBrowser
¶
Deterministic in-memory browser for tests / CI. No network.
Source code in apogee_ai_browser/infrastructure/browsers/stub_browser.py
add_fixture
¶
open
async
¶
navigate
async
¶
navigate(url: str) -> Page
Source code in apogee_ai_browser/infrastructure/browsers/stub_browser.py
async def navigate(self, url: str) -> Page:
if not self._open:
await self.open()
if url not in self._fixtures:
raise NavigationError(url, "no fixture registered")
html = self._fixtures[url]
page = Page(
url=url,
title=_extract_title(html),
text=_extract_text(html),
html=html,
status=200,
headers={"content-type": "text/html"},
metadata={"links": ",".join(_LINK_RE.findall(html))},
)
self._current = page
return page
find
async
¶
find(selector: ElementSelector) -> Element
Source code in apogee_ai_browser/infrastructure/browsers/stub_browser.py
click
async
¶
click(selector: ElementSelector) -> None
type_text
async
¶
type_text(selector: ElementSelector, value: str) -> None