okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
448d7dc
main
1"""Bounded frame capture and reference construction. Page owns retention and lifetime.""" 2 3from __future__import annotations 4 5from dataclasses import dataclass 6from typing import Any 7 8from .errors import ProtocolError 9from .transport import CDPSession ,JSON 10from .observation import Node ,attributes_from_snapshot ,ax_properties ,walk_ax 11 12 13@dataclass 14class FrameSession : 15cdp :CDPSession 16target_id :str 17root_frame :str = "" 18generation :int = 0 19ready :bool = False 20error :str | None = None 21 22 23@dataclass( frozen = True ) 24class Reference : 25session_id :str 26frame_id :str 27generation :int 28backend_id :int 29role :str 30name :str 31attributes :dict [str ,str ] 32 33 34@dataclass( frozen = True ) 35class CaptureLimits : 36snapshot_id :str 37max_nodes :int 38max_text :int 39max_depth :int 40 41 42@dataclass 43class CapturedFrames : 44rows :list [Node ] 45references :dict [str ,Reference ] 46warnings :list [JSON ] 47 48 49async def capture_frames ( 50frames :list [dict [str ,Any ]], 51sessions :dict [str ,FrameSession ], 52limits :CaptureLimits , 53)-> CapturedFrames : 54warnings :list [JSON ]= [] 55dom_by_session = {} 56refs :dict [str ,Reference ]= {} 57rows :list [Node ]= [] 58for frame in frames : 59if len (rows )>= limits .max_nodes : 60warnings .append ( 61 {"kind" :"frame_omitted_by_node_limit" ,"frame_id" :frame ["id" ]} 62 ) 63continue 64sid = frame ["session_id" ] 65state = sessions [sid ] 66generation = state .generation 67try : 68if sid not in dom_by_session : 69dom_by_session [sid ]= attributes_from_snapshot ( 70await state .cdp .send ( 71"DOMSnapshot.captureSnapshot" , 72 {"computedStyles" : []}, 73 ) 74 ) 75raw = ( 76await state .cdp .send ( 77"Accessibility.getFullAXTree" , 78 { 79"frameId" :frame ["id" ], 80"depth" :limits .max_depth , 81 }, 82 ) 83 )["nodes" ] 84except ProtocolError as exc : 85warnings .append ( 86 { 87"kind" :"frame_unavailable" , 88"frame_id" :frame ["id" ], 89"error" :exc .to_dict (), 90 } 91 ) 92continue 93attributes = dom_by_session [sid ] 94nodes = walk_ax (raw ) 95raw_ids = {n ["nodeId" ]for n in raw } 96if any (c not in raw_ids for n in raw for c in n .get ("childIds" , [])): 97warnings .append ({"kind" :"depth_or_tree_omission" ,"frame_id" :frame ["id" ]}) 98ids :dict [str ,str ]= {} 99for ax ,parent ,depth in nodes : 100if len (rows )>= limits .max_nodes : 101warnings .append ({"kind" :"node_limit" ,"frame_id" :frame ["id" ]}) 102break 103row_id = f" { limits . snapshot_id } : { len ( rows ) + 1 } " 104ids [ax ["nodeId" ]]= row_id 105role = str (ax .get ("role" , {}).get ("value" ,"" )) 106name = str (ax .get ("name" , {}).get ("value" ,"" )) 107backend = ax .get ("backendDOMNodeId" ) 108attrs = attributes .get (backend , {}) 109actionable = bool ( 110backend 111and backend in attributes 112and role not in {"StaticText" ,"RootWebArea" ,"InlineTextBox" } 113 ) 114ref = row_id if actionable else None 115row :Node = { 116"id" :row_id , 117"ref" :ref , 118"role" :"text" if role == "StaticText" else role , 119"name" :name [:limits .max_text ], 120"frame_id" :frame ["id" ], 121"parent" :ids .get (parent ), 122"depth" :depth , 123 } 124if len (name )> limits .max_text : 125row ["name_truncated" ]= True 126warnings .append ({"kind" :"text_truncated" ,"node" :row_id }) 127props = ax_properties (ax ) 128for key in ( 129"disabled" , 130"checked" , 131"selected" , 132"expanded" , 133"required" , 134"readonly" , 135"focused" , 136"level" , 137 ): 138if key in props : 139row [key ]= props [key ] 140if attrs : 141row ["attributes" ]= dict (attrs ) 142if "href" in attrs : 143row ["href" ]= attrs ["href" ] 144if "value" in ax : 145value = ax ["value" ].get ("value" ) 146row ["value" ]= ( 147"[redacted]" if attrs .get ("type" )== "password" else value 148 ) 149if ( 150isinstance (row ["value" ],str ) 151and len (row ["value" ])> limits .max_text 152 ): 153row ["value" ]= row ["value" ][:limits .max_text ] 154warnings .append ({"kind" :"value_truncated" ,"node" :row_id }) 155if ref and backend is not None : 156refs [ref ]= Reference ( 157sid ,frame ["id" ],generation ,backend ,role ,name ,dict (attrs ) 158 ) 159rows .append (row ) 160if state .generation != generation : 161warnings .append ( 162 {"kind" :"document_changed_during_capture" ,"frame_id" :frame ["id" ]} 163 ) 164return CapturedFrames (rows ,refs ,warnings )