okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petcut idle daemon memory: lean vm defaults, sweep gc, no per-tool copies of catalogs, encoded replay events, lazy browser plugin, 10 min kernel idlec90606d

main
3.4 KiB140 linesraw
1"""Data-first CDP browser plugin. Use top-level await inside Albedo cells.
2
3    b = await browser.spawn()
4    page = await b.new_page()
5    await page.goto("https://example.com")
6    snapshot = await page.observe()
7    print(browser.render(snapshot))
8
9No run() entry point: the plugin is a regular importable module. Only the
10errors load with the kernel; the rest loads on first use, so a kernel that
11never drives a browser does not hold it.
12"""
13
14from __future__ import annotations
15
16import importlib
17import sys
18from typing import TYPE_CHECKING
19
20from . import hooks
21from .errors import (
22    ActionOutcomeUnknown,
23    BrowserError,
24    CommandTimeout,
25    ConnectionLost,
26    EventOverflow,
27    IncompleteObservation,
28    JavaScriptError,
29    LaunchError,
30    NotActionable,
31    ProtocolError,
32    SelectionError,
33    StaleReference,
34    UnsupportedOperation,
35    WaitTimeout,
36    TargetGone,
37    RecoveryError,
38)
39
40if TYPE_CHECKING:
41    from albedo_api import PythonApi
42
43    from .launch import spawn
44    from .observation import Node, Observation, find, one, render
45    from .page import Page
46    from .session import Browser, EventScope, connect
47    from .transport import CDPSession, Subscription
48
49# Where each name that loads on first use lives.
50_LAZY = {
51    "spawn": "launch",
52    "cleanup_all": "launch",
53    "connect": "session",
54    "Browser": "session",
55    "EventScope": "session",
56    "Page": "page",
57    "Observation": "observation",
58    "Node": "observation",
59    "find": "observation",
60    "one": "observation",
61    "render": "observation",
62    "CDPSession": "transport",
63    "Subscription": "transport",
64}
65
66__all__ = [
67    "connect",
68    "spawn",
69    "Browser",
70    "Page",
71    "Observation",
72    "Node",
73    "find",
74    "one",
75    "render",
76    "CDPSession",
77    "EventScope",
78    "Subscription",
79    "BrowserError",
80    "ActionOutcomeUnknown",
81    "CommandTimeout",
82    "ConnectionLost",
83    "EventOverflow",
84    "IncompleteObservation",
85    "JavaScriptError",
86    "LaunchError",
87    "NotActionable",
88    "ProtocolError",
89    "SelectionError",
90    "StaleReference",
91    "UnsupportedOperation",
92    "WaitTimeout",
93    "TargetGone",
94    "RecoveryError",
95]
96
97
98def __getattr__(name: str) -> object:
99    if name not in _LAZY:
100        raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
101    return getattr(importlib.import_module(f".{_LAZY[name]}", __name__), name)
102
103
104class BrowserApi:
105    """Browser CDP control for Chrome and Chromium."""
106
107    _METHODS = ("spawn", "connect", "render", "find", "one")
108
109    def __getattr__(self, name: str) -> object:
110        if name not in self._METHODS:
111            raise AttributeError(name)
112        return __getattr__(name)
113
114    def __dir__(self) -> list[str]:
115        return [*super().__dir__(), *self._METHODS]
116
117    def __repr__(self) -> str:
118        return "<browser api: spawn(), connect(), render(), find(), one()>"
119
120
121browser = BrowserApi()
122
123# Register module alias so `import browser` works alongside the `browser` binding
124sys.modules["browser"] = sys.modules[__name__]
125
126
127async def _cleanup() -> None:
128    """End the browsers this kernel launched, if it ever launched one."""
129    launch = sys.modules.get(f"{__name__}.launch")
130    if launch is not None:
131        await launch.cleanup_all()
132
133
134def setup(api: PythonApi) -> dict[str, object]:
135    hooks.attach_image = api.attach_image
136    api.on_shutdown(_cleanup)
137    return {
138        "browser": browser,
139        "BrowserError": BrowserError,
140    }