okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
c90606d
main
1"""Data-first CDP browser plugin. Use top-level await inside Albedo cells. 2 3b = await browser.spawn() 4page = await b.new_page() 5await page.goto("https://example.com") 6snapshot = await page.observe() 7print(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 ( 22ActionOutcomeUnknown , 23BrowserError , 24CommandTimeout , 25ConnectionLost , 26EventOverflow , 27IncompleteObservation , 28JavaScriptError , 29LaunchError , 30NotActionable , 31ProtocolError , 32SelectionError , 33StaleReference , 34UnsupportedOperation , 35WaitTimeout , 36TargetGone , 37RecoveryError , 38) 39 40if TYPE_CHECKING : 41from albedo_api import PythonApi 42 43from .launch import spawn 44from .observation import Node ,Observation ,find ,one ,render 45from .page import Page 46from .session import Browser ,EventScope ,connect 47from .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 : 99if name not in _LAZY : 100raise AttributeError (f"module { __name__ !r } has no attribute { name !r } " ) 101return 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 109def __getattr__ (self ,name :str )-> object : 110if name not in self ._METHODS : 111raise AttributeError (name ) 112return __getattr__ (name ) 113 114def __dir__ (self )-> list [str ]: 115return [* super ().__dir__ (),* self ._METHODS ] 116 117def __repr__ (self )-> str : 118return "<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.""" 129launch = sys .modules .get (f" { __name__ } .launch" ) 130if launch is not None : 131await launch .cleanup_all () 132 133 134def setup (api :PythonApi )-> dict [str ,object ]: 135hooks .attach_image = api .attach_image 136api .on_shutdown (_cleanup ) 137return { 138"browser" :browser , 139"BrowserError" :BrowserError , 140 }