diff --git a/cdp_use/cdp/__init__.py b/cdp_use/cdp/__init__.py index 7386383..ad4f07d 100644 --- a/cdp_use/cdp/__init__.py +++ b/cdp_use/cdp/__init__.py @@ -13,34 +13,41 @@ from . import accessibility from . import animation from . import audits -from . import extensions from . import autofill from . import backgroundservice +from . import bluetoothemulation from . import browser from . import css from . import cachestorage from . import cast from . import dom from . import domdebugger -from . import eventbreakpoints from . import domsnapshot from . import domstorage +from . import deviceaccess from . import deviceorientation from . import emulation +from . import eventbreakpoints +from . import extensions +from . import fedcm +from . import fetch +from . import filesystem from . import headlessexperimental from . import io -from . import filesystem from . import indexeddb from . import input from . import inspector from . import layertree from . import log +from . import media from . import memory from . import network from . import overlay +from . import pwa from . import page from . import performance from . import performancetimeline +from . import preload from . import security from . import serviceworker from . import storage @@ -48,15 +55,9 @@ from . import target from . import tethering from . import tracing -from . import fetch from . import webaudio from . import webauthn -from . import media -from . import deviceaccess -from . import preload -from . import fedcm -from . import pwa -from . import bluetoothemulation +from . import browseruse from .library import CDPLibrary from .registry import EventRegistry @@ -72,34 +73,41 @@ "accessibility", "animation", "audits", - "extensions", "autofill", "backgroundservice", + "bluetoothemulation", "browser", "css", "cachestorage", "cast", "dom", "domdebugger", - "eventbreakpoints", "domsnapshot", "domstorage", + "deviceaccess", "deviceorientation", "emulation", + "eventbreakpoints", + "extensions", + "fedcm", + "fetch", + "filesystem", "headlessexperimental", "io", - "filesystem", "indexeddb", "input", "inspector", "layertree", "log", + "media", "memory", "network", "overlay", + "pwa", "page", "performance", "performancetimeline", + "preload", "security", "serviceworker", "storage", @@ -107,15 +115,9 @@ "target", "tethering", "tracing", - "fetch", "webaudio", "webauthn", - "media", - "deviceaccess", - "preload", - "fedcm", - "pwa", - "bluetoothemulation", + "browseruse", "CDPLibrary", "EventRegistry", "CDPRegistrationLibrary", diff --git a/cdp_use/cdp/audits/types.py b/cdp_use/cdp/audits/types.py index a5f792d..2ff95e3 100644 --- a/cdp_use/cdp/audits/types.py +++ b/cdp_use/cdp/audits/types.py @@ -236,6 +236,10 @@ class CorsIssueDetails(TypedDict): +UnencodedDigestError = Literal["MalformedDictionary", "UnknownAlgorithm", "IncorrectDigestType", "IncorrectDigestLength"] + + + class AttributionReportingIssueDetails(TypedDict): """Details for issues around \"Attribution Reporting API\" usage. Explainer: https://github.com/WICG/attribution-reporting-api""" @@ -281,6 +285,12 @@ class SRIMessageSignatureIssueDetails(TypedDict): +class UnencodedDigestIssueDetails(TypedDict): + error: "UnencodedDigestError" + request: "AffectedRequest" + + + GenericIssueErrorType = Literal["FormLabelForNameError", "FormDuplicateIdForInputError", "FormInputWithNoLabelError", "FormAutocompleteAttributeEmptyError", "FormEmptyIdAndNameAttributesForInputError", "FormAriaLabelledByToNonExistingId", "FormInputAssignedAutocompleteValueToIdOrNameAttributeError", "FormLabelHasNeitherForNorNestedInput", "FormLabelForMatchesNonExistingIdError", "FormInputHasWrongButWellIntendedAutocompleteValueError", "ResponseWasBlockedByORB"] @@ -438,7 +448,7 @@ class PropertyRuleIssueDetails(TypedDict): -UserReidentificationIssueType = Literal["BlockedFrameNavigation", "BlockedSubresource"] +UserReidentificationIssueType = Literal["BlockedFrameNavigation", "BlockedSubresource", "NoisedCanvasReadback"] @@ -449,10 +459,12 @@ class UserReidentificationIssueDetails(TypedDict): type: "UserReidentificationIssueType" request: "NotRequired[AffectedRequest]" """Applies to BlockedFrameNavigation and BlockedSubresource issue types.""" + sourceCodeLocation: "NotRequired[SourceCodeLocation]" + """Applies to NoisedCanvasReadback issue type.""" -InspectorIssueCode = Literal["CookieIssue", "MixedContentIssue", "BlockedByResponseIssue", "HeavyAdIssue", "ContentSecurityPolicyIssue", "SharedArrayBufferIssue", "LowTextContrastIssue", "CorsIssue", "AttributionReportingIssue", "QuirksModeIssue", "PartitioningBlobURLIssue", "NavigatorUserAgentIssue", "GenericIssue", "DeprecationIssue", "ClientHintIssue", "FederatedAuthRequestIssue", "BounceTrackingIssue", "CookieDeprecationMetadataIssue", "StylesheetLoadingIssue", "FederatedAuthUserInfoRequestIssue", "PropertyRuleIssue", "SharedDictionaryIssue", "ElementAccessibilityIssue", "SRIMessageSignatureIssue", "UserReidentificationIssue"] +InspectorIssueCode = Literal["CookieIssue", "MixedContentIssue", "BlockedByResponseIssue", "HeavyAdIssue", "ContentSecurityPolicyIssue", "SharedArrayBufferIssue", "LowTextContrastIssue", "CorsIssue", "AttributionReportingIssue", "QuirksModeIssue", "PartitioningBlobURLIssue", "NavigatorUserAgentIssue", "GenericIssue", "DeprecationIssue", "ClientHintIssue", "FederatedAuthRequestIssue", "BounceTrackingIssue", "CookieDeprecationMetadataIssue", "StylesheetLoadingIssue", "FederatedAuthUserInfoRequestIssue", "PropertyRuleIssue", "SharedDictionaryIssue", "ElementAccessibilityIssue", "SRIMessageSignatureIssue", "UnencodedDigestIssue", "UserReidentificationIssue"] """A unique identifier for the type of issue. Each type may use one of the optional fields in InspectorIssueDetails to convey more specific information about the kind of issue.""" @@ -488,6 +500,7 @@ class InspectorIssueDetails(TypedDict, total=False): sharedDictionaryIssueDetails: "SharedDictionaryIssueDetails" elementAccessibilityIssueDetails: "ElementAccessibilityIssueDetails" sriMessageSignatureIssueDetails: "SRIMessageSignatureIssueDetails" + unencodedDigestIssueDetails: "UnencodedDigestIssueDetails" userReidentificationIssueDetails: "UserReidentificationIssueDetails" diff --git a/cdp_use/cdp/browseruse/__init__.py b/cdp_use/cdp/browseruse/__init__.py new file mode 100644 index 0000000..442b436 --- /dev/null +++ b/cdp_use/cdp/browseruse/__init__.py @@ -0,0 +1,9 @@ +# This file is auto-generated by the CDP protocol generator. +# Do not edit this file manually as your changes will be overwritten. +# Generated from Chrome DevTools Protocol specifications. + +"""CDP BrowserUse Domain""" + +from .types import * +from .commands import * +from .events import * diff --git a/cdp_use/cdp/browseruse/commands.py b/cdp_use/cdp/browseruse/commands.py new file mode 100644 index 0000000..858f792 --- /dev/null +++ b/cdp_use/cdp/browseruse/commands.py @@ -0,0 +1,8 @@ +# This file is auto-generated by the CDP protocol generator. +# Do not edit this file manually as your changes will be overwritten. +# Generated from Chrome DevTools Protocol specifications. + +"""CDP BrowserUse Domain Commands""" + + +# No commands defined for this domain \ No newline at end of file diff --git a/cdp_use/cdp/browseruse/events.py b/cdp_use/cdp/browseruse/events.py new file mode 100644 index 0000000..61001c0 --- /dev/null +++ b/cdp_use/cdp/browseruse/events.py @@ -0,0 +1,33 @@ +# This file is auto-generated by the CDP protocol generator. +# Do not edit this file manually as your changes will be overwritten. +# Generated from Chrome DevTools Protocol specifications. + +"""CDP BrowserUse Domain Events""" + +from typing_extensions import NotRequired, TypedDict + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..target.types import TargetID + +"""Captcha solving started.""" +class CaptchaSolverStartedEvent(TypedDict): + targetId: "TargetID" + vendor: "str" + url: "str" + startedAt: "int" + """Unix millis""" + eventTimeout: "NotRequired[float]" + + + +"""Captcha solving finished.""" +class CaptchaSolverFinishedEvent(TypedDict): + targetId: "TargetID" + vendor: "str" + url: "str" + durationMs: "int" + finishedAt: "int" + """Unix millis""" + eventTimeout: "NotRequired[float]" diff --git a/cdp_use/cdp/browseruse/library.py b/cdp_use/cdp/browseruse/library.py new file mode 100644 index 0000000..25baf7a --- /dev/null +++ b/cdp_use/cdp/browseruse/library.py @@ -0,0 +1,19 @@ +# This file is auto-generated by the CDP protocol generator. +# Do not edit this file manually as your changes will be overwritten. +# Generated from Chrome DevTools Protocol specifications. + +"""CDP BrowserUse Domain Library""" + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ...client import CDPClient + +class BrowserUseClient: + """Client for BrowserUse domain commands.""" + + def __init__(self, client: 'CDPClient'): + self._client = client + + diff --git a/cdp_use/cdp/browseruse/registration.py b/cdp_use/cdp/browseruse/registration.py new file mode 100644 index 0000000..f06fc65 --- /dev/null +++ b/cdp_use/cdp/browseruse/registration.py @@ -0,0 +1,51 @@ +# This file is auto-generated by the CDP protocol generator. +# Do not edit this file manually as your changes will be overwritten. +# Generated from Chrome DevTools Protocol specifications. + +"""CDP BrowserUse Domain Event Registration""" + +from typing import Callable, Optional + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..registry import EventRegistry + from .events import CaptchaSolverFinishedEvent, CaptchaSolverStartedEvent + +class BrowserUseRegistration: + """Event registration interface for BrowserUse domain.""" + + def __init__(self, registry: 'EventRegistry'): + self._registry = registry + self._domain = "BrowserUse" + + def captchaSolverStarted( + self, + callback: Callable[['CaptchaSolverStartedEvent', Optional[str]], None], + ) -> None: + """ + Register a callback for captchaSolverStarted events. + + Captcha solving started. + + Args: + callback: Function to call when event occurs. + Receives (event_data, session_id) as parameters. + """ + self._registry.register("BrowserUse.captchaSolverStarted", callback) + + def captchaSolverFinished( + self, + callback: Callable[['CaptchaSolverFinishedEvent', Optional[str]], None], + ) -> None: + """ + Register a callback for captchaSolverFinished events. + + Captcha solving finished. + + Args: + callback: Function to call when event occurs. + Receives (event_data, session_id) as parameters. + """ + self._registry.register("BrowserUse.captchaSolverFinished", callback) + diff --git a/cdp_use/cdp/browseruse/types.py b/cdp_use/cdp/browseruse/types.py new file mode 100644 index 0000000..2b9cb24 --- /dev/null +++ b/cdp_use/cdp/browseruse/types.py @@ -0,0 +1,8 @@ +# This file is auto-generated by the CDP protocol generator. +# Do not edit this file manually as your changes will be overwritten. +# Generated from Chrome DevTools Protocol specifications. + +"""CDP BrowserUse Domain Types""" + + +# No types defined for this domain \ No newline at end of file diff --git a/cdp_use/cdp/css/commands.py b/cdp_use/cdp/css/commands.py index aa3e672..5e0f7cd 100644 --- a/cdp_use/cdp/css/commands.py +++ b/cdp_use/cdp/css/commands.py @@ -4,7 +4,7 @@ """CDP CSS Domain Commands""" -from typing import List +from typing import Any, Dict, List from typing_extensions import NotRequired, TypedDict from typing import TYPE_CHECKING @@ -29,6 +29,7 @@ from .types import CSSScope from .types import CSSStyle from .types import CSSSupports + from .types import ComputedStyleExtraFields from .types import InheritedAnimatedStyleEntry from .types import InheritedPseudoElementMatches from .types import InheritedStyleEntry @@ -134,13 +135,15 @@ class GetComputedStyleForNodeParameters(TypedDict): class GetComputedStyleForNodeReturns(TypedDict): computedStyle: "List[CSSComputedStyleProperty]" """Computed style for the specified DOM node.""" + extraFields: "ComputedStyleExtraFields" + """A list of non-standard \"extra fields\" which blink stores alongside each +computed style.""" class ResolveValuesParameters(TypedDict): values: "List[str]" - """Substitution functions (var()/env()/attr()) and cascade-dependent -keywords (revert/revert-layer) do not work.""" + """Cascade-dependent keywords (revert/revert-layer) do not work.""" nodeId: "NodeId" """Id of the node in whose context the expression is evaluated""" propertyName: "NotRequired[str]" @@ -231,6 +234,11 @@ class GetMatchedStylesForNodeReturns(TypedDict): +class GetEnvironmentVariablesReturns(TypedDict): + environmentVariables: "Dict[str, Any]" + + + class GetMediaQueriesReturns(TypedDict): medias: "List[CSSMedia]" diff --git a/cdp_use/cdp/css/library.py b/cdp_use/cdp/css/library.py index a0bf628..50b6387 100644 --- a/cdp_use/cdp/css/library.py +++ b/cdp_use/cdp/css/library.py @@ -24,6 +24,7 @@ from .commands import GetBackgroundColorsReturns from .commands import GetComputedStyleForNodeParameters from .commands import GetComputedStyleForNodeReturns + from .commands import GetEnvironmentVariablesReturns from .commands import GetInlineStylesForNodeParameters from .commands import GetInlineStylesForNodeReturns from .commands import GetLayersForNodeParameters @@ -252,6 +253,18 @@ async def getMatchedStylesForNode( session_id=session_id, )) + async def getEnvironmentVariables( + self, + params: None = None, + session_id: Optional[str] = None, + ) -> "GetEnvironmentVariablesReturns": + """Returns the values of the default UA-defined environment variables used in env()""" + return cast("GetEnvironmentVariablesReturns", await self._client.send_raw( + method="CSS.getEnvironmentVariables", + params=params, + session_id=session_id, + )) + async def getMediaQueries( self, params: None = None, diff --git a/cdp_use/cdp/css/types.py b/cdp_use/cdp/css/types.py index 8cfcd3b..17be237 100644 --- a/cdp_use/cdp/css/types.py +++ b/cdp_use/cdp/css/types.py @@ -263,6 +263,14 @@ class CSSComputedStyleProperty(TypedDict): +class ComputedStyleExtraFields(TypedDict): + isAppearanceBase: "bool" + """Returns whether or not this node is being rendered with base appearance, +which happens when it has its appearance property set to base/base-select +or it is in the subtree of an element being rendered with base appearance.""" + + + class CSSStyle(TypedDict): """CSS style representation.""" @@ -371,6 +379,8 @@ class CSSContainerQuery(TypedDict): """Optional logical axes queried for the container.""" queriesScrollState: "NotRequired[bool]" """true if the query contains scroll-state() queries.""" + queriesAnchored: "NotRequired[bool]" + """true if the query contains anchored() queries.""" diff --git a/cdp_use/cdp/dom/commands.py b/cdp_use/cdp/dom/commands.py index c74b55c..439d60e 100644 --- a/cdp_use/cdp/dom/commands.py +++ b/cdp_use/cdp/dom/commands.py @@ -561,6 +561,7 @@ class GetContainerForNodeParameters(TypedDict): physicalAxes: "NotRequired[PhysicalAxes]" logicalAxes: "NotRequired[LogicalAxes]" queriesScrollState: "NotRequired[bool]" + queriesAnchored: "NotRequired[bool]" class GetContainerForNodeReturns(TypedDict): @@ -593,3 +594,17 @@ class GetAnchorElementParameters(TypedDict): class GetAnchorElementReturns(TypedDict): nodeId: "NodeId" """The anchor element of the given anchor query.""" + + + +class ForceShowPopoverParameters(TypedDict): + nodeId: "NodeId" + """Id of the popover HTMLElement""" + enable: "bool" + """If true, opens the popover and keeps it open. If false, closes the +popover if it was previously force-opened.""" + + +class ForceShowPopoverReturns(TypedDict): + nodeIds: "List[NodeId]" + """List of popovers that were closed in order to respect popover stacking order.""" diff --git a/cdp_use/cdp/dom/library.py b/cdp_use/cdp/dom/library.py index 93b479f..d3027ef 100644 --- a/cdp_use/cdp/dom/library.py +++ b/cdp_use/cdp/dom/library.py @@ -19,6 +19,8 @@ from .commands import DiscardSearchResultsParameters from .commands import EnableParameters from .commands import FocusParameters + from .commands import ForceShowPopoverParameters + from .commands import ForceShowPopoverReturns from .commands import GetAnchorElementParameters from .commands import GetAnchorElementReturns from .commands import GetAttributesParameters @@ -707,9 +709,9 @@ async def getContainerForNode( ) -> "GetContainerForNodeReturns": """Returns the query container of the given node based on container query conditions: containerName, physical and logical axes, and whether it queries -scroll-state. If no axes are provided and queriesScrollState is false, the -style container is returned, which is the direct parent or the closest -element with a matching container-name.""" +scroll-state or anchored elements. If no axes are provided and +queriesScrollState is false, the style container is returned, which is the +direct parent or the closest element with a matching container-name.""" return cast("GetContainerForNodeReturns", await self._client.send_raw( method="DOM.getContainerForNode", params=params, @@ -742,4 +744,17 @@ async def getAnchorElement( session_id=session_id, )) + async def forceShowPopover( + self, + params: "ForceShowPopoverParameters", + session_id: Optional[str] = None, + ) -> "ForceShowPopoverReturns": + """When enabling, this API force-opens the popover identified by nodeId +and keeps it open until disabled.""" + return cast("ForceShowPopoverReturns", await self._client.send_raw( + method="DOM.forceShowPopover", + params=params, + session_id=session_id, + )) + diff --git a/cdp_use/cdp/dom/types.py b/cdp_use/cdp/dom/types.py index 7901d8a..89c6530 100644 --- a/cdp_use/cdp/dom/types.py +++ b/cdp_use/cdp/dom/types.py @@ -35,7 +35,7 @@ class BackendNode(TypedDict): -PseudoType = Literal["first-line", "first-letter", "checkmark", "before", "after", "picker-icon", "marker", "backdrop", "column", "selection", "search-text", "target-text", "spelling-error", "grammar-error", "highlight", "first-line-inherited", "scroll-marker", "scroll-marker-group", "scroll-button", "scrollbar", "scrollbar-thumb", "scrollbar-button", "scrollbar-track", "scrollbar-track-piece", "scrollbar-corner", "resizer", "input-list-button", "view-transition", "view-transition-group", "view-transition-image-pair", "view-transition-group-children", "view-transition-old", "view-transition-new", "placeholder", "file-selector-button", "details-content", "picker", "permission-icon"] +PseudoType = Literal["first-line", "first-letter", "checkmark", "before", "after", "picker-icon", "interest-hint", "marker", "backdrop", "column", "selection", "search-text", "target-text", "spelling-error", "grammar-error", "highlight", "first-line-inherited", "scroll-marker", "scroll-marker-group", "scroll-button", "scrollbar", "scrollbar-thumb", "scrollbar-button", "scrollbar-track", "scrollbar-track-piece", "scrollbar-corner", "resizer", "input-list-button", "view-transition", "view-transition-group", "view-transition-image-pair", "view-transition-group-children", "view-transition-old", "view-transition-new", "placeholder", "file-selector-button", "details-content", "picker", "permission-icon"] """Pseudo element type.""" diff --git a/cdp_use/cdp/emulation/commands.py b/cdp_use/cdp/emulation/commands.py index 9dcb38d..b4623d4 100644 --- a/cdp_use/cdp/emulation/commands.py +++ b/cdp_use/cdp/emulation/commands.py @@ -21,12 +21,15 @@ from .types import PressureSource from .types import PressureState from .types import SafeAreaInsets + from .types import ScreenId + from .types import ScreenInfo from .types import ScreenOrientation from .types import SensorMetadata from .types import SensorReading from .types import SensorType from .types import UserAgentMetadata from .types import VirtualTimePolicy + from .types import WorkAreaInsets class CanEmulateReturns(TypedDict): result: "bool" @@ -396,3 +399,43 @@ class SetSmallViewportHeightDifferenceOverrideParameters(TypedDict): of size 100lvh.""" + + + +class GetScreenInfosReturns(TypedDict): + screenInfos: "List[ScreenInfo]" + + + +class AddScreenParameters(TypedDict): + left: "int" + """Offset of the left edge of the screen in pixels.""" + top: "int" + """Offset of the top edge of the screen in pixels.""" + width: "int" + """The width of the screen in pixels.""" + height: "int" + """The height of the screen in pixels.""" + workAreaInsets: "NotRequired[WorkAreaInsets]" + """Specifies the screen's work area. Default is entire screen.""" + devicePixelRatio: "NotRequired[float]" + """Specifies the screen's device pixel ratio. Default is 1.""" + rotation: "NotRequired[int]" + """Specifies the screen's rotation angle. Available values are 0, 90, 180 and 270. Default is 0.""" + colorDepth: "NotRequired[int]" + """Specifies the screen's color depth in bits. Default is 24.""" + label: "NotRequired[str]" + """Specifies the descriptive label for the screen. Default is none.""" + isInternal: "NotRequired[bool]" + """Indicates whether the screen is internal to the device or external, attached to the device. Default is false.""" + + +class AddScreenReturns(TypedDict): + screenInfo: "ScreenInfo" + + + +class RemoveScreenParameters(TypedDict): + screenId: "ScreenId" + + diff --git a/cdp_use/cdp/emulation/library.py b/cdp_use/cdp/emulation/library.py index 0acc278..81f8ea0 100644 --- a/cdp_use/cdp/emulation/library.py +++ b/cdp_use/cdp/emulation/library.py @@ -10,9 +10,13 @@ if TYPE_CHECKING: from ...client import CDPClient + from .commands import AddScreenParameters + from .commands import AddScreenReturns from .commands import CanEmulateReturns from .commands import GetOverriddenSensorInformationParameters from .commands import GetOverriddenSensorInformationReturns + from .commands import GetScreenInfosReturns + from .commands import RemoveScreenParameters from .commands import SetAutoDarkModeOverrideParameters from .commands import SetAutomationOverrideParameters from .commands import SetCPUThrottlingRateParameters @@ -597,4 +601,40 @@ async def setSmallViewportHeightDifferenceOverride( session_id=session_id, )) + async def getScreenInfos( + self, + params: None = None, + session_id: Optional[str] = None, + ) -> "GetScreenInfosReturns": + """Returns device's screen configuration.""" + return cast("GetScreenInfosReturns", await self._client.send_raw( + method="Emulation.getScreenInfos", + params=params, + session_id=session_id, + )) + + async def addScreen( + self, + params: "AddScreenParameters", + session_id: Optional[str] = None, + ) -> "AddScreenReturns": + """Add a new screen to the device. Only supported in headless mode.""" + return cast("AddScreenReturns", await self._client.send_raw( + method="Emulation.addScreen", + params=params, + session_id=session_id, + )) + + async def removeScreen( + self, + params: "RemoveScreenParameters", + session_id: Optional[str] = None, + ) -> "Dict[str, Any]": + """Remove screen from the device. Only supported in headless mode.""" + return cast("Dict[str, Any]", await self._client.send_raw( + method="Emulation.removeScreen", + params=params, + session_id=session_id, + )) + diff --git a/cdp_use/cdp/emulation/types.py b/cdp_use/cdp/emulation/types.py index 2dc77a4..e07061e 100644 --- a/cdp_use/cdp/emulation/types.py +++ b/cdp_use/cdp/emulation/types.py @@ -154,5 +154,60 @@ class PressureMetadata(TypedDict, total=False): +class WorkAreaInsets(TypedDict, total=False): + top: "int" + """Work area top inset in pixels. Default is 0;""" + left: "int" + """Work area left inset in pixels. Default is 0;""" + bottom: "int" + """Work area bottom inset in pixels. Default is 0;""" + right: "int" + """Work area right inset in pixels. Default is 0;""" + + + +ScreenId = str + + + +class ScreenInfo(TypedDict): + """Screen information similar to the one returned by window.getScreenDetails() method, +see https://w3c.github.io/window-management/#screendetailed.""" + + left: "int" + """Offset of the left edge of the screen.""" + top: "int" + """Offset of the top edge of the screen.""" + width: "int" + """Width of the screen.""" + height: "int" + """Height of the screen.""" + availLeft: "int" + """Offset of the left edge of the available screen area.""" + availTop: "int" + """Offset of the top edge of the available screen area.""" + availWidth: "int" + """Width of the available screen area.""" + availHeight: "int" + """Height of the available screen area.""" + devicePixelRatio: "float" + """Specifies the screen's device pixel ratio.""" + orientation: "ScreenOrientation" + """Specifies the screen's orientation.""" + colorDepth: "int" + """Specifies the screen's color depth in bits.""" + isExtended: "bool" + """Indicates whether the device has multiple screens.""" + isInternal: "bool" + """Indicates whether the screen is internal to the device or external, attached to the device.""" + isPrimary: "bool" + """Indicates whether the screen is set as the the operating system primary screen.""" + label: "str" + """Specifies the descriptive label for the screen.""" + id: "ScreenId" + """Specifies the unique identifier of the screen.""" + + + DisabledImageType = Literal["avif", "webp"] """Enum of image types that can be disabled.""" diff --git a/cdp_use/cdp/library.py b/cdp_use/cdp/library.py index 6a0a57d..ed14529 100644 --- a/cdp_use/cdp/library.py +++ b/cdp_use/cdp/library.py @@ -51,10 +51,6 @@ def __init__(self, client: 'CDPClient'): from .audits.library import AuditsClient self.Audits = AuditsClient(client) - # Extensions domain - from .extensions.library import ExtensionsClient - self.Extensions = ExtensionsClient(client) - # Autofill domain from .autofill.library import AutofillClient self.Autofill = AutofillClient(client) @@ -63,6 +59,10 @@ def __init__(self, client: 'CDPClient'): from .backgroundservice.library import BackgroundServiceClient self.BackgroundService = BackgroundServiceClient(client) + # BluetoothEmulation domain + from .bluetoothemulation.library import BluetoothEmulationClient + self.BluetoothEmulation = BluetoothEmulationClient(client) + # Browser domain from .browser.library import BrowserClient self.Browser = BrowserClient(client) @@ -87,10 +87,6 @@ def __init__(self, client: 'CDPClient'): from .domdebugger.library import DOMDebuggerClient self.DOMDebugger = DOMDebuggerClient(client) - # EventBreakpoints domain - from .eventbreakpoints.library import EventBreakpointsClient - self.EventBreakpoints = EventBreakpointsClient(client) - # DOMSnapshot domain from .domsnapshot.library import DOMSnapshotClient self.DOMSnapshot = DOMSnapshotClient(client) @@ -99,6 +95,10 @@ def __init__(self, client: 'CDPClient'): from .domstorage.library import DOMStorageClient self.DOMStorage = DOMStorageClient(client) + # DeviceAccess domain + from .deviceaccess.library import DeviceAccessClient + self.DeviceAccess = DeviceAccessClient(client) + # DeviceOrientation domain from .deviceorientation.library import DeviceOrientationClient self.DeviceOrientation = DeviceOrientationClient(client) @@ -107,6 +107,26 @@ def __init__(self, client: 'CDPClient'): from .emulation.library import EmulationClient self.Emulation = EmulationClient(client) + # EventBreakpoints domain + from .eventbreakpoints.library import EventBreakpointsClient + self.EventBreakpoints = EventBreakpointsClient(client) + + # Extensions domain + from .extensions.library import ExtensionsClient + self.Extensions = ExtensionsClient(client) + + # FedCm domain + from .fedcm.library import FedCmClient + self.FedCm = FedCmClient(client) + + # Fetch domain + from .fetch.library import FetchClient + self.Fetch = FetchClient(client) + + # FileSystem domain + from .filesystem.library import FileSystemClient + self.FileSystem = FileSystemClient(client) + # HeadlessExperimental domain from .headlessexperimental.library import HeadlessExperimentalClient self.HeadlessExperimental = HeadlessExperimentalClient(client) @@ -115,10 +135,6 @@ def __init__(self, client: 'CDPClient'): from .io.library import IOClient self.IO = IOClient(client) - # FileSystem domain - from .filesystem.library import FileSystemClient - self.FileSystem = FileSystemClient(client) - # IndexedDB domain from .indexeddb.library import IndexedDBClient self.IndexedDB = IndexedDBClient(client) @@ -139,6 +155,10 @@ def __init__(self, client: 'CDPClient'): from .log.library import LogClient self.Log = LogClient(client) + # Media domain + from .media.library import MediaClient + self.Media = MediaClient(client) + # Memory domain from .memory.library import MemoryClient self.Memory = MemoryClient(client) @@ -151,6 +171,10 @@ def __init__(self, client: 'CDPClient'): from .overlay.library import OverlayClient self.Overlay = OverlayClient(client) + # PWA domain + from .pwa.library import PWAClient + self.PWA = PWAClient(client) + # Page domain from .page.library import PageClient self.Page = PageClient(client) @@ -163,6 +187,10 @@ def __init__(self, client: 'CDPClient'): from .performancetimeline.library import PerformanceTimelineClient self.PerformanceTimeline = PerformanceTimelineClient(client) + # Preload domain + from .preload.library import PreloadClient + self.Preload = PreloadClient(client) + # Security domain from .security.library import SecurityClient self.Security = SecurityClient(client) @@ -191,10 +219,6 @@ def __init__(self, client: 'CDPClient'): from .tracing.library import TracingClient self.Tracing = TracingClient(client) - # Fetch domain - from .fetch.library import FetchClient - self.Fetch = FetchClient(client) - # WebAudio domain from .webaudio.library import WebAudioClient self.WebAudio = WebAudioClient(client) @@ -203,27 +227,7 @@ def __init__(self, client: 'CDPClient'): from .webauthn.library import WebAuthnClient self.WebAuthn = WebAuthnClient(client) - # Media domain - from .media.library import MediaClient - self.Media = MediaClient(client) - - # DeviceAccess domain - from .deviceaccess.library import DeviceAccessClient - self.DeviceAccess = DeviceAccessClient(client) - - # Preload domain - from .preload.library import PreloadClient - self.Preload = PreloadClient(client) - - # FedCm domain - from .fedcm.library import FedCmClient - self.FedCm = FedCmClient(client) - - # PWA domain - from .pwa.library import PWAClient - self.PWA = PWAClient(client) - - # BluetoothEmulation domain - from .bluetoothemulation.library import BluetoothEmulationClient - self.BluetoothEmulation = BluetoothEmulationClient(client) + # BrowserUse domain + from .browseruse.library import BrowserUseClient + self.BrowserUse = BrowserUseClient(client) diff --git a/cdp_use/cdp/media/events.py b/cdp_use/cdp/media/events.py index bc2bc81..1c893cb 100644 --- a/cdp_use/cdp/media/events.py +++ b/cdp_use/cdp/media/events.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + from .types import Player from .types import PlayerError from .types import PlayerEvent from .types import PlayerId @@ -47,7 +48,7 @@ class PlayerErrorsRaisedEvent(TypedDict): """Called whenever a player is created, or when a new agent joins and receives -a list of active players. If an agent is restored, it will receive the full -list of player ids and all events again.""" -class PlayersCreatedEvent(TypedDict): - players: "List[PlayerId]" +a list of active players. If an agent is restored, it will receive one +event for each active player.""" +class PlayerCreatedEvent(TypedDict): + player: "Player" diff --git a/cdp_use/cdp/media/registration.py b/cdp_use/cdp/media/registration.py index 28e0f75..93d94b9 100644 --- a/cdp_use/cdp/media/registration.py +++ b/cdp_use/cdp/media/registration.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from ..registry import EventRegistry - from .events import PlayerErrorsRaisedEvent, PlayerEventsAddedEvent, PlayerMessagesLoggedEvent, PlayerPropertiesChangedEvent, PlayersCreatedEvent + from .events import PlayerCreatedEvent, PlayerErrorsRaisedEvent, PlayerEventsAddedEvent, PlayerMessagesLoggedEvent, PlayerPropertiesChangedEvent class MediaRegistration: """Event registration interface for Media domain.""" @@ -81,20 +81,20 @@ def playerErrorsRaised( """ self._registry.register("Media.playerErrorsRaised", callback) - def playersCreated( + def playerCreated( self, - callback: Callable[['PlayersCreatedEvent', Optional[str]], None], + callback: Callable[['PlayerCreatedEvent', Optional[str]], None], ) -> None: """ - Register a callback for playersCreated events. + Register a callback for playerCreated events. Called whenever a player is created, or when a new agent joins and receives -a list of active players. If an agent is restored, it will receive the full -list of player ids and all events again. +a list of active players. If an agent is restored, it will receive one +event for each active player. Args: callback: Function to call when event occurs. Receives (event_data, session_id) as parameters. """ - self._registry.register("Media.playersCreated", callback) + self._registry.register("Media.playerCreated", callback) diff --git a/cdp_use/cdp/media/types.py b/cdp_use/cdp/media/types.py index 4209bbc..800468b 100644 --- a/cdp_use/cdp/media/types.py +++ b/cdp_use/cdp/media/types.py @@ -5,7 +5,12 @@ """CDP Media Domain Types""" from typing import Any, Dict, List -from typing_extensions import TypedDict +from typing_extensions import NotRequired, TypedDict + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..dom.types import BackendNodeId PlayerId = str """Players will get an ID that is unique within the agent context.""" @@ -73,3 +78,9 @@ class PlayerError(TypedDict): caused by an WindowsError""" data: "Dict[str, Any]" """Extra data attached to an error, such as an HRESULT, Video Codec, etc.""" + + + +class Player(TypedDict): + playerId: "PlayerId" + domNodeId: "NotRequired[BackendNodeId]" diff --git a/cdp_use/cdp/network/commands.py b/cdp_use/cdp/network/commands.py index ea76e70..81045ff 100644 --- a/cdp_use/cdp/network/commands.py +++ b/cdp_use/cdp/network/commands.py @@ -26,6 +26,7 @@ from .types import ErrorReason from .types import Headers from .types import InterceptionId + from .types import IpProxyStatus from .types import LoadNetworkResourceOptions from .types import LoadNetworkResourcePageResult from .types import RequestId @@ -33,6 +34,12 @@ from .types import SecurityIsolationStatus from .types import TimeSinceEpoch +class GetIPProtectionProxyStatusReturns(TypedDict): + status: "IpProxyStatus" + """Whether IP proxy is available""" + + + class SetAcceptedEncodingsParameters(TypedDict): encodings: "List[ContentEncoding]" """List of accepted content encodings.""" diff --git a/cdp_use/cdp/network/library.py b/cdp_use/cdp/network/library.py index dc61750..bb33a3a 100644 --- a/cdp_use/cdp/network/library.py +++ b/cdp_use/cdp/network/library.py @@ -23,6 +23,7 @@ from .commands import GetCertificateReturns from .commands import GetCookiesParameters from .commands import GetCookiesReturns + from .commands import GetIPProtectionProxyStatusReturns from .commands import GetRequestPostDataParameters from .commands import GetRequestPostDataReturns from .commands import GetResponseBodyForInterceptionParameters @@ -59,6 +60,19 @@ class NetworkClient: def __init__(self, client: 'CDPClient'): self._client = client + async def getIPProtectionProxyStatus( + self, + params: None = None, + session_id: Optional[str] = None, + ) -> "GetIPProtectionProxyStatusReturns": + """Returns enum representing if IP Proxy of requests is available +or reason it is not active.""" + return cast("GetIPProtectionProxyStatusReturns", await self._client.send_raw( + method="Network.getIPProtectionProxyStatus", + params=params, + session_id=session_id, + )) + async def setAcceptedEncodings( self, params: "SetAcceptedEncodingsParameters", diff --git a/cdp_use/cdp/network/types.py b/cdp_use/cdp/network/types.py index 62e06dd..eb9c87f 100644 --- a/cdp_use/cdp/network/types.py +++ b/cdp_use/cdp/network/types.py @@ -250,6 +250,12 @@ class SecurityDetails(TypedDict): +IpProxyStatus = Literal["Available", "FeatureNotEnabled", "MaskedDomainListNotEnabled", "MaskedDomainListNotPopulated", "AuthTokensUnavailable", "Unavailable", "BypassedByDevTools"] +"""Sets Controls for IP Proxy of requests. +Page reload is required before the new behavior will be observed.""" + + + CorsError = Literal["DisallowedByMode", "InvalidResponse", "WildcardOriginNotAllowed", "MissingAllowOriginHeader", "MultipleAllowOriginValues", "InvalidAllowOriginValue", "AllowOriginMismatch", "InvalidAllowCredentials", "CorsDisabledScheme", "PreflightInvalidStatus", "PreflightDisallowedRedirect", "PreflightWildcardOriginNotAllowed", "PreflightMissingAllowOriginHeader", "PreflightMultipleAllowOriginValues", "PreflightInvalidAllowOriginValue", "PreflightAllowOriginMismatch", "PreflightInvalidAllowCredentials", "PreflightMissingAllowExternal", "PreflightInvalidAllowExternal", "PreflightMissingAllowPrivateNetwork", "PreflightInvalidAllowPrivateNetwork", "InvalidAllowMethodsPreflightResponse", "InvalidAllowHeadersPreflightResponse", "MethodDisallowedByPreflightResponse", "HeaderDisallowedByPreflightResponse", "RedirectContainsCredentials", "InsecurePrivateNetwork", "InvalidPrivateNetworkAccess", "UnexpectedPrivateNetworkAccess", "NoCorsRedirectModeNotFollow", "PreflightMissingPrivateNetworkAccessId", "PreflightMissingPrivateNetworkAccessName", "PrivateNetworkAccessPermissionUnavailable", "PrivateNetworkAccessPermissionDenied", "LocalNetworkAccessPermissionDenied"] """The reason why request was blocked.""" @@ -367,6 +373,9 @@ class Response(TypedDict): """Security state of the request resource.""" securityDetails: "NotRequired[SecurityDetails]" """Security details for the request.""" + isIpProtectionUsed: "NotRequired[bool]" + """Indicates whether the request was sent through IP Protection proxies. If +set to true, the request used the IP Protection privacy feature.""" @@ -767,7 +776,7 @@ class DirectUDPMessage(TypedDict): -IPAddressSpace = Literal["Loopback", "Private", "Public", "Unknown"] +IPAddressSpace = Literal["Loopback", "Local", "Public", "Unknown"] diff --git a/cdp_use/cdp/overlay/library.py b/cdp_use/cdp/overlay/library.py index db8c7d1..b569f0e 100644 --- a/cdp_use/cdp/overlay/library.py +++ b/cdp_use/cdp/overlay/library.py @@ -163,7 +163,10 @@ async def highlightRect( params: "HighlightRectParameters", session_id: Optional[str] = None, ) -> "Dict[str, Any]": - """Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.""" + """Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport. +Issue: the method does not handle device pixel ratio (DPR) correctly. +The coordinates currently have to be adjusted by the client +if DPR is not 1 (see crbug.com/437807128).""" return cast("Dict[str, Any]", await self._client.send_raw( method="Overlay.highlightRect", params=params, diff --git a/cdp_use/cdp/page/commands.py b/cdp_use/cdp/page/commands.py index 968dcc0..a42be01 100644 --- a/cdp_use/cdp/page/commands.py +++ b/cdp_use/cdp/page/commands.py @@ -266,6 +266,8 @@ class NavigateReturns(TypedDict): as the previously committed loaderId would not change.""" errorText: "str" """User friendly error message, present if and only if navigation has failed.""" + isDownload: "bool" + """Whether the navigation resulted in a download.""" diff --git a/cdp_use/cdp/page/events.py b/cdp_use/cdp/page/events.py index 2f3f4c8..305b03c 100644 --- a/cdp_use/cdp/page/events.py +++ b/cdp_use/cdp/page/events.py @@ -305,8 +305,7 @@ class WindowOpenEvent(TypedDict): -"""Issued for every compilation cache generated. Is only available -if Page.setGenerateCompilationCache is enabled.""" +"""Issued for every compilation cache generated.""" class CompilationCacheProducedEvent(TypedDict): url: "str" data: "str" diff --git a/cdp_use/cdp/page/registration.py b/cdp_use/cdp/page/registration.py index e32f3f8..e43569e 100644 --- a/cdp_use/cdp/page/registration.py +++ b/cdp_use/cdp/page/registration.py @@ -471,8 +471,7 @@ def compilationCacheProduced( """ Register a callback for compilationCacheProduced events. - Issued for every compilation cache generated. Is only available -if Page.setGenerateCompilationCache is enabled. + Issued for every compilation cache generated. Args: callback: Function to call when event occurs. diff --git a/cdp_use/cdp/page/types.py b/cdp_use/cdp/page/types.py index 1b33c97..0b7a0f7 100644 --- a/cdp_use/cdp/page/types.py +++ b/cdp_use/cdp/page/types.py @@ -82,7 +82,7 @@ class AdScriptAncestry(TypedDict): -PermissionsPolicyFeature = Literal["accelerometer", "all-screens-capture", "ambient-light-sensor", "aria-notify", "attribution-reporting", "autoplay", "bluetooth", "browsing-topics", "camera", "captured-surface-control", "ch-dpr", "ch-device-memory", "ch-downlink", "ch-ect", "ch-prefers-color-scheme", "ch-prefers-reduced-motion", "ch-prefers-reduced-transparency", "ch-rtt", "ch-save-data", "ch-ua", "ch-ua-arch", "ch-ua-bitness", "ch-ua-high-entropy-values", "ch-ua-platform", "ch-ua-model", "ch-ua-mobile", "ch-ua-form-factors", "ch-ua-full-version", "ch-ua-full-version-list", "ch-ua-platform-version", "ch-ua-wow64", "ch-viewport-height", "ch-viewport-width", "ch-width", "clipboard-read", "clipboard-write", "compute-pressure", "controlled-frame", "cross-origin-isolated", "deferred-fetch", "deferred-fetch-minimal", "device-attributes", "digital-credentials-get", "direct-sockets", "direct-sockets-private", "display-capture", "document-domain", "encrypted-media", "execution-while-out-of-viewport", "execution-while-not-rendered", "fenced-unpartitioned-storage-read", "focus-without-user-activation", "fullscreen", "frobulate", "gamepad", "geolocation", "gyroscope", "hid", "identity-credentials-get", "idle-detection", "interest-cohort", "join-ad-interest-group", "keyboard-map", "language-detector", "language-model", "local-fonts", "local-network-access", "magnetometer", "media-playback-while-not-visible", "microphone", "midi", "on-device-speech-recognition", "otp-credentials", "payment", "picture-in-picture", "popins", "private-aggregation", "private-state-token-issuance", "private-state-token-redemption", "publickey-credentials-create", "publickey-credentials-get", "record-ad-auction-events", "rewriter", "run-ad-auction", "screen-wake-lock", "serial", "shared-autofill", "shared-storage", "shared-storage-select-url", "smart-card", "speaker-selection", "storage-access", "sub-apps", "summarizer", "sync-xhr", "translator", "unload", "usb", "usb-unrestricted", "vertical-scroll", "web-app-installation", "web-printing", "web-share", "window-management", "writer", "xr-spatial-tracking"] +PermissionsPolicyFeature = Literal["accelerometer", "all-screens-capture", "ambient-light-sensor", "aria-notify", "attribution-reporting", "autoplay", "bluetooth", "browsing-topics", "camera", "captured-surface-control", "ch-dpr", "ch-device-memory", "ch-downlink", "ch-ect", "ch-prefers-color-scheme", "ch-prefers-reduced-motion", "ch-prefers-reduced-transparency", "ch-rtt", "ch-save-data", "ch-ua", "ch-ua-arch", "ch-ua-bitness", "ch-ua-high-entropy-values", "ch-ua-platform", "ch-ua-model", "ch-ua-mobile", "ch-ua-form-factors", "ch-ua-full-version", "ch-ua-full-version-list", "ch-ua-platform-version", "ch-ua-wow64", "ch-viewport-height", "ch-viewport-width", "ch-width", "clipboard-read", "clipboard-write", "compute-pressure", "controlled-frame", "cross-origin-isolated", "deferred-fetch", "deferred-fetch-minimal", "device-attributes", "digital-credentials-create", "digital-credentials-get", "direct-sockets", "direct-sockets-private", "display-capture", "document-domain", "encrypted-media", "execution-while-out-of-viewport", "execution-while-not-rendered", "fenced-unpartitioned-storage-read", "focus-without-user-activation", "fullscreen", "frobulate", "gamepad", "geolocation", "gyroscope", "hid", "identity-credentials-get", "idle-detection", "interest-cohort", "join-ad-interest-group", "keyboard-map", "language-detector", "language-model", "local-fonts", "local-network-access", "magnetometer", "media-playback-while-not-visible", "microphone", "midi", "on-device-speech-recognition", "otp-credentials", "payment", "picture-in-picture", "popins", "private-aggregation", "private-state-token-issuance", "private-state-token-redemption", "publickey-credentials-create", "publickey-credentials-get", "record-ad-auction-events", "rewriter", "run-ad-auction", "screen-wake-lock", "serial", "shared-autofill", "shared-storage", "shared-storage-select-url", "smart-card", "speaker-selection", "storage-access", "sub-apps", "summarizer", "sync-xhr", "translator", "unload", "usb", "usb-unrestricted", "vertical-scroll", "web-app-installation", "web-printing", "web-share", "window-management", "writer", "xr-spatial-tracking"] """All Permissions Policy features. This enum should match the one defined in services/network/public/cpp/permissions_policy/permissions_policy_features.json5. LINT.IfChange(PermissionsPolicyFeature)""" diff --git a/cdp_use/cdp/preload/types.py b/cdp_use/cdp/preload/types.py index db0f467..fb250ce 100644 --- a/cdp_use/cdp/preload/types.py +++ b/cdp_use/cdp/preload/types.py @@ -55,7 +55,7 @@ class RuleSet(TypedDict): -RuleSetErrorType = Literal["SourceIsNotJsonObject", "InvalidRulesSkipped"] +RuleSetErrorType = Literal["SourceIsNotJsonObject", "InvalidRulesSkipped", "InvalidRulesetLevelTag"] @@ -111,7 +111,7 @@ class PreloadingAttemptSource(TypedDict): -PrerenderFinalStatus = Literal["Activated", "Destroyed", "LowEndDevice", "InvalidSchemeRedirect", "InvalidSchemeNavigation", "NavigationRequestBlockedByCsp", "MojoBinderPolicy", "RendererProcessCrashed", "RendererProcessKilled", "Download", "TriggerDestroyed", "NavigationNotCommitted", "NavigationBadHttpStatus", "ClientCertRequested", "NavigationRequestNetworkError", "CancelAllHostsForTesting", "DidFailLoad", "Stop", "SslCertificateError", "LoginAuthRequested", "UaChangeRequiresReload", "BlockedByClient", "AudioOutputDeviceRequested", "MixedContent", "TriggerBackgrounded", "MemoryLimitExceeded", "DataSaverEnabled", "TriggerUrlHasEffectiveUrl", "ActivatedBeforeStarted", "InactivePageRestriction", "StartFailed", "TimeoutBackgrounded", "CrossSiteRedirectInInitialNavigation", "CrossSiteNavigationInInitialNavigation", "SameSiteCrossOriginRedirectNotOptInInInitialNavigation", "SameSiteCrossOriginNavigationNotOptInInInitialNavigation", "ActivationNavigationParameterMismatch", "ActivatedInBackground", "EmbedderHostDisallowed", "ActivationNavigationDestroyedBeforeSuccess", "TabClosedByUserGesture", "TabClosedWithoutUserGesture", "PrimaryMainFrameRendererProcessCrashed", "PrimaryMainFrameRendererProcessKilled", "ActivationFramePolicyNotCompatible", "PreloadingDisabled", "BatterySaverEnabled", "ActivatedDuringMainFrameNavigation", "PreloadingUnsupportedByWebContents", "CrossSiteRedirectInMainFrameNavigation", "CrossSiteNavigationInMainFrameNavigation", "SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation", "SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation", "MemoryPressureOnTrigger", "MemoryPressureAfterTriggered", "PrerenderingDisabledByDevTools", "SpeculationRuleRemoved", "ActivatedWithAuxiliaryBrowsingContexts", "MaxNumOfRunningEagerPrerendersExceeded", "MaxNumOfRunningNonEagerPrerendersExceeded", "MaxNumOfRunningEmbedderPrerendersExceeded", "PrerenderingUrlHasEffectiveUrl", "RedirectedPrerenderingUrlHasEffectiveUrl", "ActivationUrlHasEffectiveUrl", "JavaScriptInterfaceAdded", "JavaScriptInterfaceRemoved", "AllPrerenderingCanceled", "WindowClosed", "SlowNetwork", "OtherPrerenderedPageActivated", "V8OptimizerDisabled", "PrerenderFailedDuringPrefetch", "BrowsingDataRemoved"] +PrerenderFinalStatus = Literal["Activated", "Destroyed", "LowEndDevice", "InvalidSchemeRedirect", "InvalidSchemeNavigation", "NavigationRequestBlockedByCsp", "MojoBinderPolicy", "RendererProcessCrashed", "RendererProcessKilled", "Download", "TriggerDestroyed", "NavigationNotCommitted", "NavigationBadHttpStatus", "ClientCertRequested", "NavigationRequestNetworkError", "CancelAllHostsForTesting", "DidFailLoad", "Stop", "SslCertificateError", "LoginAuthRequested", "UaChangeRequiresReload", "BlockedByClient", "AudioOutputDeviceRequested", "MixedContent", "TriggerBackgrounded", "MemoryLimitExceeded", "DataSaverEnabled", "TriggerUrlHasEffectiveUrl", "ActivatedBeforeStarted", "InactivePageRestriction", "StartFailed", "TimeoutBackgrounded", "CrossSiteRedirectInInitialNavigation", "CrossSiteNavigationInInitialNavigation", "SameSiteCrossOriginRedirectNotOptInInInitialNavigation", "SameSiteCrossOriginNavigationNotOptInInInitialNavigation", "ActivationNavigationParameterMismatch", "ActivatedInBackground", "EmbedderHostDisallowed", "ActivationNavigationDestroyedBeforeSuccess", "TabClosedByUserGesture", "TabClosedWithoutUserGesture", "PrimaryMainFrameRendererProcessCrashed", "PrimaryMainFrameRendererProcessKilled", "ActivationFramePolicyNotCompatible", "PreloadingDisabled", "BatterySaverEnabled", "ActivatedDuringMainFrameNavigation", "PreloadingUnsupportedByWebContents", "CrossSiteRedirectInMainFrameNavigation", "CrossSiteNavigationInMainFrameNavigation", "SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation", "SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation", "MemoryPressureOnTrigger", "MemoryPressureAfterTriggered", "PrerenderingDisabledByDevTools", "SpeculationRuleRemoved", "ActivatedWithAuxiliaryBrowsingContexts", "MaxNumOfRunningEagerPrerendersExceeded", "MaxNumOfRunningNonEagerPrerendersExceeded", "MaxNumOfRunningEmbedderPrerendersExceeded", "PrerenderingUrlHasEffectiveUrl", "RedirectedPrerenderingUrlHasEffectiveUrl", "ActivationUrlHasEffectiveUrl", "JavaScriptInterfaceAdded", "JavaScriptInterfaceRemoved", "AllPrerenderingCanceled", "WindowClosed", "SlowNetwork", "OtherPrerenderedPageActivated", "V8OptimizerDisabled", "PrerenderFailedDuringPrefetch", "BrowsingDataRemoved", "PrerenderHostReused"] """List of FinalStatus reasons for Prerender2.""" diff --git a/cdp_use/cdp/registration_library.py b/cdp_use/cdp/registration_library.py index be8cde6..1cf62fb 100644 --- a/cdp_use/cdp/registration_library.py +++ b/cdp_use/cdp/registration_library.py @@ -55,6 +55,10 @@ def __init__(self, registry: 'EventRegistry'): from .backgroundservice.registration import BackgroundServiceRegistration self.BackgroundService = BackgroundServiceRegistration(registry) + # BluetoothEmulation domain registration + from .bluetoothemulation.registration import BluetoothEmulationRegistration + self.BluetoothEmulation = BluetoothEmulationRegistration(registry) + # Browser domain registration from .browser.registration import BrowserRegistration self.Browser = BrowserRegistration(registry) @@ -75,10 +79,22 @@ def __init__(self, registry: 'EventRegistry'): from .domstorage.registration import DOMStorageRegistration self.DOMStorage = DOMStorageRegistration(registry) + # DeviceAccess domain registration + from .deviceaccess.registration import DeviceAccessRegistration + self.DeviceAccess = DeviceAccessRegistration(registry) + # Emulation domain registration from .emulation.registration import EmulationRegistration self.Emulation = EmulationRegistration(registry) + # FedCm domain registration + from .fedcm.registration import FedCmRegistration + self.FedCm = FedCmRegistration(registry) + + # Fetch domain registration + from .fetch.registration import FetchRegistration + self.Fetch = FetchRegistration(registry) + # Input domain registration from .input.registration import InputRegistration self.Input = InputRegistration(registry) @@ -95,6 +111,10 @@ def __init__(self, registry: 'EventRegistry'): from .log.registration import LogRegistration self.Log = LogRegistration(registry) + # Media domain registration + from .media.registration import MediaRegistration + self.Media = MediaRegistration(registry) + # Network domain registration from .network.registration import NetworkRegistration self.Network = NetworkRegistration(registry) @@ -115,6 +135,10 @@ def __init__(self, registry: 'EventRegistry'): from .performancetimeline.registration import PerformanceTimelineRegistration self.PerformanceTimeline = PerformanceTimelineRegistration(registry) + # Preload domain registration + from .preload.registration import PreloadRegistration + self.Preload = PreloadRegistration(registry) + # Security domain registration from .security.registration import SecurityRegistration self.Security = SecurityRegistration(registry) @@ -139,10 +163,6 @@ def __init__(self, registry: 'EventRegistry'): from .tracing.registration import TracingRegistration self.Tracing = TracingRegistration(registry) - # Fetch domain registration - from .fetch.registration import FetchRegistration - self.Fetch = FetchRegistration(registry) - # WebAudio domain registration from .webaudio.registration import WebAudioRegistration self.WebAudio = WebAudioRegistration(registry) @@ -151,23 +171,7 @@ def __init__(self, registry: 'EventRegistry'): from .webauthn.registration import WebAuthnRegistration self.WebAuthn = WebAuthnRegistration(registry) - # Media domain registration - from .media.registration import MediaRegistration - self.Media = MediaRegistration(registry) - - # DeviceAccess domain registration - from .deviceaccess.registration import DeviceAccessRegistration - self.DeviceAccess = DeviceAccessRegistration(registry) - - # Preload domain registration - from .preload.registration import PreloadRegistration - self.Preload = PreloadRegistration(registry) - - # FedCm domain registration - from .fedcm.registration import FedCmRegistration - self.FedCm = FedCmRegistration(registry) - - # BluetoothEmulation domain registration - from .bluetoothemulation.registration import BluetoothEmulationRegistration - self.BluetoothEmulation = BluetoothEmulationRegistration(registry) + # BrowserUse domain registration + from .browseruse.registration import BrowserUseRegistration + self.BrowserUse = BrowserUseRegistration(registry) diff --git a/cdp_use/cdp/registry.py b/cdp_use/cdp/registry.py index d52a2f9..baa7348 100644 --- a/cdp_use/cdp/registry.py +++ b/cdp_use/cdp/registry.py @@ -5,6 +5,7 @@ """CDP Event Registry""" import logging +import inspect from typing import Any, Callable, Dict, Optional logger = logging.getLogger(__name__) @@ -60,16 +61,10 @@ async def handle_event( """ if method in self._handlers: try: - import asyncio - import inspect handler = self._handlers[method] - - # Check if handler is async if inspect.iscoroutinefunction(handler): - # Await async handlers await handler(params, session_id) else: - # Call sync handlers directly handler(params, session_id) return True except Exception as e: diff --git a/cdp_use/cdp/target/commands.py b/cdp_use/cdp/target/commands.py index bc5671c..4ab7437 100644 --- a/cdp_use/cdp/target/commands.py +++ b/cdp_use/cdp/target/commands.py @@ -223,3 +223,14 @@ class SetRemoteLocationsParameters(TypedDict): """List of remote locations.""" + + + +class OpenDevToolsParameters(TypedDict): + targetId: "TargetID" + """This can be the page or tab target ID.""" + + +class OpenDevToolsReturns(TypedDict): + targetId: "TargetID" + """The targetId of DevTools page target.""" diff --git a/cdp_use/cdp/target/library.py b/cdp_use/cdp/target/library.py index 5b43cce..32fbaf7 100644 --- a/cdp_use/cdp/target/library.py +++ b/cdp_use/cdp/target/library.py @@ -29,6 +29,8 @@ from .commands import GetTargetInfoReturns from .commands import GetTargetsParameters from .commands import GetTargetsReturns + from .commands import OpenDevToolsParameters + from .commands import OpenDevToolsReturns from .commands import SendMessageToTargetParameters from .commands import SetAutoAttachParameters from .commands import SetDiscoverTargetsParameters @@ -268,4 +270,16 @@ async def setRemoteLocations( session_id=session_id, )) + async def openDevTools( + self, + params: "OpenDevToolsParameters", + session_id: Optional[str] = None, + ) -> "OpenDevToolsReturns": + """Opens a DevTools window for the target.""" + return cast("OpenDevToolsReturns", await self._client.send_raw( + method="Target.openDevTools", + params=params, + session_id=session_id, + )) + diff --git a/cdp_use/cdp/target/types.py b/cdp_use/cdp/target/types.py index 206a4d1..54fb5dd 100644 --- a/cdp_use/cdp/target/types.py +++ b/cdp_use/cdp/target/types.py @@ -37,6 +37,8 @@ class TargetInfo(TypedDict): """Whether the target has access to the originating window.""" openerFrameId: "NotRequired[FrameId]" """Frame id of originating window (is only set if target has an opener).""" + parentFrameId: "NotRequired[FrameId]" + """Id of the parent frame, only present for the \"iframe\" targets.""" browserContextId: "NotRequired[BrowserContextID]" subtype: "NotRequired[str]" """Provides additional details for specific target types. For example, for diff --git a/cdp_use/client.py b/cdp_use/client.py index 044c818..26aa60f 100644 --- a/cdp_use/client.py +++ b/cdp_use/client.py @@ -15,202 +15,214 @@ # Set up logging logger = logging.getLogger(__name__) + # Custom formatter for websocket messages class WebSocketLogFilter(logging.Filter): def __init__(self): super().__init__() self.ping_times = {} # Track ping send times by ping data self.ping_timeout_tasks = {} # Track timeout tasks - + def filter(self, record): # Only process websocket client messages - if record.name != 'websockets.client': + if record.name != "websockets.client": return True - + # Change the logger name - record.name = 'cdp_use.client' - + record.name = "cdp_use.client" + # Process the message msg = record.getMessage() - + # === SPECIAL CASES (suppress or consolidate) === - + # Handle PING/PONG sequences - if '> PING' in msg: - match = re.search(r'> PING ([a-f0-9 ]+) \[binary', msg) + if "> PING" in msg: + match = re.search(r"> PING ([a-f0-9 ]+) \[binary", msg) if match: ping_data = match.group(1) self.ping_times[ping_data] = time.time() - + # Schedule timeout warning async def check_timeout(): await asyncio.sleep(3) if ping_data in self.ping_times: del self.ping_times[ping_data] - timeout_logger = logging.getLogger('cdp_use.client') - timeout_logger.warning('⚠️ PING not answered by browser... (>3s and no PONG received)') - + timeout_logger = logging.getLogger("cdp_use.client") + timeout_logger.warning( + "⚠️ PING not answered by browser... (>3s and no PONG received)" + ) + try: loop = asyncio.get_event_loop() - self.ping_timeout_tasks[ping_data] = loop.create_task(check_timeout()) + self.ping_timeout_tasks[ping_data] = loop.create_task( + check_timeout() + ) except RuntimeError: pass - + return False # Suppress the PING message - - elif '< PONG' in msg: - match = re.search(r'< PONG ([a-f0-9 ]+) \[binary', msg) + + elif "< PONG" in msg: + match = re.search(r"< PONG ([a-f0-9 ]+) \[binary", msg) if match: pong_data = match.group(1) if pong_data in self.ping_times: elapsed = (time.time() - self.ping_times[pong_data]) * 1000 del self.ping_times[pong_data] - + if pong_data in self.ping_timeout_tasks: self.ping_timeout_tasks[pong_data].cancel() del self.ping_timeout_tasks[pong_data] - - record.msg = f'✔ PING ({elapsed:.1f}ms)' + + record.msg = f"✔ PING ({elapsed:.1f}ms)" record.args = () return True return False - + # Suppress keepalive and EOF messages - elif ('% sent keepalive ping' in msg or '% received keepalive pong' in msg or - '> EOF' in msg or '< EOF' in msg): + elif ( + "% sent keepalive ping" in msg + or "% received keepalive pong" in msg + or "> EOF" in msg + or "< EOF" in msg + ): return False - + # Connection state messages - elif '= connection is' in msg: - if 'CONNECTING' in msg: - record.msg = '🔗 Connecting...' - elif 'OPEN' in msg: - record.msg = '✅ Connected' - elif 'CLOSING' in msg or 'CLOSED' in msg: - record.msg = '🔌 Disconnected' + elif "= connection is" in msg: + if "CONNECTING" in msg: + record.msg = "🔗 Connecting..." + elif "OPEN" in msg: + record.msg = "✅ Connected" + elif "CLOSING" in msg or "CLOSED" in msg: + record.msg = "🔌 Disconnected" else: - msg = msg.replace('= ', '') + msg = msg.replace("= ", "") record.msg = msg record.args = () return True - - elif 'x half-closing TCP connection' in msg: - record.msg = '👋 Closing our half of the TCP connection' + + elif "x half-closing TCP connection" in msg: + record.msg = "👋 Closing our half of the TCP connection" record.args = () return True - + # === GENERIC PROCESSING === - + # Parse CDP messages - be flexible with regex matching - if 'TEXT' in msg: + if "TEXT" in msg: # Determine direction - is_outgoing = '>' in msg[:10] # Check start of message for direction - + is_outgoing = ">" in msg[:10] # Check start of message for direction + # Extract and handle size - size_match = re.search(r'\[(\d+) bytes\]', msg) + size_match = re.search(r"\[(\d+) bytes\]", msg) if size_match: size_bytes = int(size_match.group(1)) # Only show size if > 5kb - size_str = f' [{size_bytes // 1024}kb]' if size_bytes > 5120 else '' + size_str = f" [{size_bytes // 1024}kb]" if size_bytes > 5120 else "" # Remove size from message for cleaner parsing - msg_clean = msg[:msg.rfind('[')].strip() if '[' in msg else msg + msg_clean = msg[: msg.rfind("[")].strip() if "[" in msg else msg else: - size_str = '' + size_str = "" msg_clean = msg - + # Extract id (flexible) id_match = re.search(r'(?:"id":|id:)\s*(\d+)', msg_clean) msg_id = id_match.group(1) if id_match else None - + # Extract method (flexible) - method_match = re.search(r'(?:"method":|method:)\s*"?([A-Za-z.]+)', msg_clean) + method_match = re.search( + r'(?:"method":|method:)\s*"?([A-Za-z.]+)', msg_clean + ) method = method_match.group(1) if method_match else None - + # Remove quotes from entire message for cleaner output - msg_clean = msg_clean.replace('"', '') - + msg_clean = msg_clean.replace('"', "") + # Build formatted message based on what we found if is_outgoing and msg_id and method: # Outgoing request - params_match = re.search(r'(?:params:)\s*({[^}]*})', msg_clean) - params_str = params_match.group(1) if params_match else '' - - if params_str == '{}' or not params_str: - record.msg = f'🌎 ← #{msg_id}: {method}(){size_str}' + params_match = re.search(r"(?:params:)\s*({[^}]*})", msg_clean) + params_str = params_match.group(1) if params_match else "" + + if params_str == "{}" or not params_str: + record.msg = f"🌎 ← #{msg_id}: {method}(){size_str}" else: - record.msg = f'🌎 ← #{msg_id}: {method}({params_str}){size_str}' + record.msg = f"🌎 ← #{msg_id}: {method}({params_str}){size_str}" record.args = () return True - + elif not is_outgoing and msg_id: # Incoming response - if 'result:' in msg_clean: + if "result:" in msg_clean: # Extract whatever comes after result: - result_match = re.search(r'result:\s*({.*})', msg_clean) + result_match = re.search(r"result:\s*({.*})", msg_clean) if result_match: result_str = result_match.group(1) - + # Clean up common artifacts - result_str = re.sub(r'},?sessionId:[^}]*}?$', '}', result_str) - + result_str = re.sub(r"},?sessionId:[^}]*}?$", "}", result_str) + # Suppress empty results - if result_str == '{}': + if result_str == "{}": return False - + # Truncate if too long if len(result_str) > 200: - result_str = result_str[:200] + '...' - - record.msg = f'🌎 → #{msg_id}: ↳ {result_str}{size_str}' + result_str = result_str[:200] + "..." + + record.msg = f"🌎 → #{msg_id}: ↳ {result_str}{size_str}" record.args = () return True - - elif 'error:' in msg_clean: - error_match = re.search(r'error:\s*({[^}]*})', msg_clean) - error_str = error_match.group(1) if error_match else 'error' - record.msg = f'🌎 → #{msg_id}: ❌ {error_str}{size_str}' + + elif "error:" in msg_clean: + error_match = re.search(r"error:\s*({[^}]*})", msg_clean) + error_str = error_match.group(1) if error_match else "error" + record.msg = f"🌎 → #{msg_id}: ❌ {error_str}{size_str}" record.args = () return True - + elif not is_outgoing and method: # Event - params_match = re.search(r'params:\s*({.*})', msg_clean) - params_str = params_match.group(1) if params_match else '' - + params_match = re.search(r"params:\s*({.*})", msg_clean) + params_str = params_match.group(1) if params_match else "" + # Clean up common artifacts - params_str = re.sub(r'},?sessionId:[^}]*}?$', '}', params_str) - + params_str = re.sub(r"},?sessionId:[^}]*}?$", "}", params_str) + # Truncate if too long if len(params_str) > 200: - params_str = params_str[:200] + '...' - - if params_str == '{}' or not params_str: - record.msg = f'🌎 → Event: {method}(){size_str}' + params_str = params_str[:200] + "..." + + if params_str == "{}" or not params_str: + record.msg = f"🌎 → Event: {method}(){size_str}" else: - record.msg = f'🌎 → Event: {method}({params_str}){size_str}' + record.msg = f"🌎 → Event: {method}({params_str}){size_str}" record.args = () return True - + # === GENERIC ARROW REPLACEMENT === # Replace all arrows: > becomes ←, < becomes → # Add emoji for TEXT messages - if ' TEXT ' in msg: - msg = re.sub(r'^>', '🌎 ←', msg) - msg = re.sub(r'^<', '🌎 →', msg) - msg = msg.replace(' TEXT ', ' ') + if " TEXT " in msg: + msg = re.sub(r"^>", "🌎 ←", msg) + msg = re.sub(r"^<", "🌎 →", msg) + msg = msg.replace(" TEXT ", " ") else: - msg = re.sub(r'^>', '←', msg) - msg = re.sub(r'^<', '→', msg) - + msg = re.sub(r"^>", "←", msg) + msg = re.sub(r"^<", "→", msg) + # Remove all quotes - msg = re.sub(r"['\"]", '', msg) - + msg = re.sub(r"['\"]", "", msg) + record.msg = msg record.args = () return True + # Configure websockets logger -ws_logger = logging.getLogger('websockets.client') +ws_logger = logging.getLogger("websockets.client") ws_logger.addFilter(WebSocketLogFilter()) @@ -373,3 +385,16 @@ async def send_raw( # Wait for the response return await future + + async def emit_event( + self, + method: str, + params: Optional[Any] = None, + session_id: Optional[str] = None, + ) -> bool: + """Emit a synthetic/custom event through the registry. + + Useful for custom domains (e.g., BrowserUse) where events are + produced by your application rather than the browser. + """ + return await self._event_registry.handle_event(method, params or {}, session_id) diff --git a/cdp_use/custom_protocols/browseruse.json b/cdp_use/custom_protocols/browseruse.json new file mode 100644 index 0000000..4c17209 --- /dev/null +++ b/cdp_use/custom_protocols/browseruse.json @@ -0,0 +1,56 @@ +{ + "domains": [ + { + "domain": "BrowserUse", + "description": "Custom domain for BrowserUse specific automation events.", + "types": [], + "commands": [], + "events": [ + { + "name": "captchaSolverStarted", + "description": "Captcha solving started.", + "parameters": [ + { "name": "targetId", "$ref": "Target.TargetID" }, + { + "name": "vendor", + "type": "string" + }, + { + "name": "url", + "type": "string" + }, + { + "name": "startedAt", + "type": "integer", + "description": "Unix millis" + }, + { "name": "eventTimeout", "type": "number", "optional": true } + ] + }, + { + "name": "captchaSolverFinished", + "description": "Captcha solving finished.", + "parameters": [ + { "name": "targetId", "$ref": "Target.TargetID" }, + { + "name": "vendor", + "type": "string" + }, + { + "name": "url", + "type": "string" + }, + { "name": "durationMs", "type": "integer" }, + { + "name": "finishedAt", + "type": "integer", + "description": "Unix millis" + }, + { "name": "eventTimeout", "type": "number", "optional": true } + ] + } + ] + } + ] +} + diff --git a/cdp_use/generator/constants.py b/cdp_use/generator/constants.py index 3b2a46b..bde64fa 100644 --- a/cdp_use/generator/constants.py +++ b/cdp_use/generator/constants.py @@ -1,3 +1,18 @@ -JS_PROTOCOL_FILE = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/refs/heads/master/json/js_protocol.json" +""" +CDP Protocol Version Configuration -BROWSER_PROTOCOL_FILE = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/refs/heads/master/json/browser_protocol.json" +Change CDP_VERSION to pin a specific version of the Chrome DevTools Protocol. + +Examples: + - Latest master: "refs/heads/master" + - Specific commit: "4b0c3f2e8c5d6a7b9e1f2a3c4d5e6f7a8b9c0d1e" + - Tagged version: "refs/tags/v1.3" (when available) + +To find commits: https://github.com/ChromeDevTools/devtools-protocol/commits/master +""" + +CDP_VERSION = "refs/heads/master" # Change this to pin a specific version + +JS_PROTOCOL_FILE = f"https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/{CDP_VERSION}/json/js_protocol.json" + +BROWSER_PROTOCOL_FILE = f"https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/{CDP_VERSION}/json/browser_protocol.json" diff --git a/cdp_use/generator/generate.py b/cdp_use/generator/generate.py index 02fb945..eaa9a14 100644 --- a/cdp_use/generator/generate.py +++ b/cdp_use/generator/generate.py @@ -10,15 +10,17 @@ from pathlib import Path from urllib.request import urlretrieve -from .constants import BROWSER_PROTOCOL_FILE, JS_PROTOCOL_FILE +from .constants import BROWSER_PROTOCOL_FILE, CDP_VERSION, JS_PROTOCOL_FILE from .generator import CDPGenerator def download_protocol_files() -> tuple[str, str]: - """Download the latest protocol files from the official repository.""" + """Download the protocol files from the official repository.""" temp_dir = Path(tempfile.mkdtemp()) - print("Downloading Chrome DevTools Protocol specifications...") + print( + f"Downloading Chrome DevTools Protocol specifications (version: {CDP_VERSION})..." + ) # Download JavaScript protocol js_protocol_path = temp_dir / "js_protocol.json" @@ -41,9 +43,20 @@ def main(): # Download protocol files js_file, browser_file = download_protocol_files() - # Create generator and run it + # Discover any custom protocol JSON files placed under cdp_use/custom_protocols/ + project_root = Path(__file__).resolve().parents[2] + custom_dir = project_root / "cdp_use" / "custom_protocols" + custom_protocol_files: list[str] = [] + if custom_dir.exists() and custom_dir.is_dir(): + for path in sorted(custom_dir.glob("*.json")): + print(f" Including custom protocol: {path}") + custom_protocol_files.append(str(path)) + + # Create generator and run it, appending any custom protocol files generator = CDPGenerator() - generator.generate_all(protocol_files=[js_file, browser_file]) + generator.generate_all( + protocol_files=[js_file, browser_file, *custom_protocol_files] + ) print("🎉 CDP type-safe client generation completed!") print("") diff --git a/cdp_use/generator/registry_generator.py b/cdp_use/generator/registry_generator.py index bec84e0..4cf980c 100644 --- a/cdp_use/generator/registry_generator.py +++ b/cdp_use/generator/registry_generator.py @@ -12,83 +12,94 @@ class RegistryGenerator: def generate_registry(self, domains: List[Dict[str, Any]]) -> str: """Generate the central registry.py file.""" - + content = """# This file is auto-generated by the CDP protocol generator. # Do not edit this file manually as your changes will be overwritten. # Generated from Chrome DevTools Protocol specifications. """ content += '"""CDP Event Registry"""\n\n' - + content += "import logging\n" + content += "import inspect\n" content += "from typing import Any, Callable, Dict, Optional\n\n" - + content += "logger = logging.getLogger(__name__)\n\n" - + content += "class EventRegistry:\n" content += ' """Central registry for managing CDP event callbacks."""\n\n' content += " def __init__(self):\n" content += " self._handlers: Dict[str, Callable[[Any, Optional[str]], None]] = {}\n\n" - + content += " def register(\n" content += " self,\n" content += " method: str,\n" content += " callback: Callable[[Any, Optional[str]], None],\n" content += " ) -> None:\n" content += ' """\n' - content += ' Register a callback for a specific CDP event method.\n' - content += ' \n' - content += ' Args:\n' - content += ' method: The CDP method name (e.g., "Page.frameAttached")\n' - content += ' callback: Function to call when event occurs.\n' - content += ' Receives (event_data, session_id) as parameters.\n' + content += " Register a callback for a specific CDP event method.\n" + content += " \n" + content += " Args:\n" + content += ( + ' method: The CDP method name (e.g., "Page.frameAttached")\n' + ) + content += " callback: Function to call when event occurs.\n" + content += ( + " Receives (event_data, session_id) as parameters.\n" + ) content += ' """\n' content += ' logger.debug(f"Registering handler for {method}")\n' content += " self._handlers[method] = callback\n\n" - + content += " def unregister(self, method: str) -> None:\n" content += ' """\n' - content += ' Unregister a callback for a specific CDP event method.\n' - content += ' \n' - content += ' Args:\n' - content += ' method: The CDP method name to unregister\n' + content += " Unregister a callback for a specific CDP event method.\n" + content += " \n" + content += " Args:\n" + content += " method: The CDP method name to unregister\n" content += ' """\n' content += ' logger.debug(f"Unregistering handler for {method}")\n' content += " self._handlers.pop(method, None)\n\n" - - content += " def handle_event(\n" + + content += " async def handle_event(\n" content += " self,\n" content += " method: str,\n" content += " params: Any,\n" content += " session_id: Optional[str] = None,\n" content += " ) -> bool:\n" content += ' """\n' - content += ' Handle an incoming CDP event.\n' - content += ' \n' - content += ' Args:\n' - content += ' method: The CDP method name\n' - content += ' params: The event parameters\n' - content += ' session_id: Optional session ID\n' - content += ' \n' - content += ' Returns:\n' - content += ' True if a handler was found and called, False otherwise\n' + content += " Handle an incoming CDP event.\n" + content += " \n" + content += " Args:\n" + content += " method: The CDP method name\n" + content += " params: The event parameters\n" + content += " session_id: Optional session ID\n" + content += " \n" + content += " Returns:\n" + content += ( + " True if a handler was found and called, False otherwise\n" + ) content += ' """\n' content += " if method in self._handlers:\n" content += " try:\n" - content += " self._handlers[method](params, session_id)\n" + content += " handler = self._handlers[method]\n" + content += " if inspect.iscoroutinefunction(handler):\n" + content += " await handler(params, session_id)\n" + content += " else:\n" + content += " handler(params, session_id)\n" content += " return True\n" content += " except Exception as e:\n" content += ' logger.error(f"Error in event handler for {method}: {e}")\n' content += " return False\n" content += " return False\n\n" - + content += " def clear(self) -> None:\n" content += ' """Clear all registered handlers."""\n' content += ' logger.debug("Clearing all event handlers")\n' content += " self._handlers.clear()\n\n" - + content += " def get_registered_methods(self) -> list[str]:\n" content += ' """Get a list of all registered method names."""\n' content += " return list(self._handlers.keys())\n" - - return content \ No newline at end of file + + return content diff --git a/uv.lock b/uv.lock index 6d24399..6bd00c9 100644 --- a/uv.lock +++ b/uv.lock @@ -32,7 +32,7 @@ wheels = [ [[package]] name = "cdp-use" -version = "1.3.1" +version = "1.4.0" source = { editable = "." } dependencies = [ { name = "httpx" },