parsek-cdp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parsek_cdp/__init__.py +55 -0
- parsek_cdp/_logging.py +49 -0
- parsek_cdp/cdp/__init__.py +247 -0
- parsek_cdp/cdp/accessibility/__init__.py +15 -0
- parsek_cdp/cdp/accessibility/events.py +35 -0
- parsek_cdp/cdp/accessibility/functions.py +202 -0
- parsek_cdp/cdp/accessibility/types.py +235 -0
- parsek_cdp/cdp/animation/__init__.py +15 -0
- parsek_cdp/cdp/animation/events.py +52 -0
- parsek_cdp/cdp/animation/functions.py +140 -0
- parsek_cdp/cdp/animation/types.py +113 -0
- parsek_cdp/cdp/audits/__init__.py +15 -0
- parsek_cdp/cdp/audits/events.py +21 -0
- parsek_cdp/cdp/audits/functions.py +95 -0
- parsek_cdp/cdp/audits/types.py +1051 -0
- parsek_cdp/cdp/autofill/__init__.py +15 -0
- parsek_cdp/cdp/autofill/events.py +27 -0
- parsek_cdp/cdp/autofill/functions.py +64 -0
- parsek_cdp/cdp/autofill/types.py +108 -0
- parsek_cdp/cdp/backgroundservice/__init__.py +15 -0
- parsek_cdp/cdp/backgroundservice/events.py +40 -0
- parsek_cdp/cdp/backgroundservice/functions.py +59 -0
- parsek_cdp/cdp/backgroundservice/types.py +63 -0
- parsek_cdp/cdp/bluetoothemulation/__init__.py +15 -0
- parsek_cdp/cdp/bluetoothemulation/events.py +70 -0
- parsek_cdp/cdp/bluetoothemulation/functions.py +247 -0
- parsek_cdp/cdp/bluetoothemulation/types.py +120 -0
- parsek_cdp/cdp/browser/__init__.py +15 -0
- parsek_cdp/cdp/browser/events.py +46 -0
- parsek_cdp/cdp/browser/functions.py +350 -0
- parsek_cdp/cdp/browser/types.py +158 -0
- parsek_cdp/cdp/cachestorage/__init__.py +15 -0
- parsek_cdp/cdp/cachestorage/events.py +13 -0
- parsek_cdp/cdp/cachestorage/functions.py +126 -0
- parsek_cdp/cdp/cachestorage/types.py +89 -0
- parsek_cdp/cdp/cast/__init__.py +15 -0
- parsek_cdp/cdp/cast/events.py +38 -0
- parsek_cdp/cdp/cast/functions.py +77 -0
- parsek_cdp/cdp/cast/types.py +23 -0
- parsek_cdp/cdp/console/__init__.py +15 -0
- parsek_cdp/cdp/console/events.py +22 -0
- parsek_cdp/cdp/console/functions.py +36 -0
- parsek_cdp/cdp/console/types.py +30 -0
- parsek_cdp/cdp/css/__init__.py +15 -0
- parsek_cdp/cdp/css/events.py +79 -0
- parsek_cdp/cdp/css/functions.py +788 -0
- parsek_cdp/cdp/css/types.py +742 -0
- parsek_cdp/cdp/debugger/__init__.py +15 -0
- parsek_cdp/cdp/debugger/events.py +164 -0
- parsek_cdp/cdp/debugger/functions.py +663 -0
- parsek_cdp/cdp/debugger/types.py +171 -0
- parsek_cdp/cdp/deviceaccess/__init__.py +15 -0
- parsek_cdp/cdp/deviceaccess/events.py +30 -0
- parsek_cdp/cdp/deviceaccess/functions.py +54 -0
- parsek_cdp/cdp/deviceaccess/types.py +28 -0
- parsek_cdp/cdp/deviceorientation/__init__.py +15 -0
- parsek_cdp/cdp/deviceorientation/events.py +13 -0
- parsek_cdp/cdp/deviceorientation/functions.py +35 -0
- parsek_cdp/cdp/deviceorientation/types.py +10 -0
- parsek_cdp/cdp/dom/__init__.py +15 -0
- parsek_cdp/cdp/dom/events.py +228 -0
- parsek_cdp/cdp/dom/functions.py +1033 -0
- parsek_cdp/cdp/dom/types.py +293 -0
- parsek_cdp/cdp/domdebugger/__init__.py +15 -0
- parsek_cdp/cdp/domdebugger/events.py +13 -0
- parsek_cdp/cdp/domdebugger/functions.py +155 -0
- parsek_cdp/cdp/domdebugger/types.py +57 -0
- parsek_cdp/cdp/domsnapshot/__init__.py +15 -0
- parsek_cdp/cdp/domsnapshot/events.py +13 -0
- parsek_cdp/cdp/domsnapshot/functions.py +111 -0
- parsek_cdp/cdp/domsnapshot/types.py +312 -0
- parsek_cdp/cdp/domstorage/__init__.py +15 -0
- parsek_cdp/cdp/domstorage/events.py +60 -0
- parsek_cdp/cdp/domstorage/functions.py +79 -0
- parsek_cdp/cdp/domstorage/types.py +30 -0
- parsek_cdp/cdp/emulation/__init__.py +15 -0
- parsek_cdp/cdp/emulation/events.py +17 -0
- parsek_cdp/cdp/emulation/functions.py +688 -0
- parsek_cdp/cdp/emulation/types.py +305 -0
- parsek_cdp/cdp/eventbreakpoints/__init__.py +15 -0
- parsek_cdp/cdp/eventbreakpoints/events.py +13 -0
- parsek_cdp/cdp/eventbreakpoints/functions.py +41 -0
- parsek_cdp/cdp/eventbreakpoints/types.py +10 -0
- parsek_cdp/cdp/extensions/__init__.py +15 -0
- parsek_cdp/cdp/extensions/events.py +13 -0
- parsek_cdp/cdp/extensions/functions.py +118 -0
- parsek_cdp/cdp/extensions/types.py +19 -0
- parsek_cdp/cdp/fedcm/__init__.py +15 -0
- parsek_cdp/cdp/fedcm/events.py +45 -0
- parsek_cdp/cdp/fedcm/functions.py +93 -0
- parsek_cdp/cdp/fedcm/types.py +72 -0
- parsek_cdp/cdp/fetch/__init__.py +15 -0
- parsek_cdp/cdp/fetch/events.py +82 -0
- parsek_cdp/cdp/fetch/functions.py +215 -0
- parsek_cdp/cdp/fetch/types.py +81 -0
- parsek_cdp/cdp/filesystem/__init__.py +15 -0
- parsek_cdp/cdp/filesystem/events.py +13 -0
- parsek_cdp/cdp/filesystem/functions.py +36 -0
- parsek_cdp/cdp/filesystem/types.py +54 -0
- parsek_cdp/cdp/headlessexperimental/__init__.py +15 -0
- parsek_cdp/cdp/headlessexperimental/events.py +13 -0
- parsek_cdp/cdp/headlessexperimental/functions.py +77 -0
- parsek_cdp/cdp/headlessexperimental/types.py +24 -0
- parsek_cdp/cdp/heapprofiler/__init__.py +15 -0
- parsek_cdp/cdp/heapprofiler/events.py +65 -0
- parsek_cdp/cdp/heapprofiler/functions.py +193 -0
- parsek_cdp/cdp/heapprofiler/types.py +57 -0
- parsek_cdp/cdp/indexeddb/__init__.py +15 -0
- parsek_cdp/cdp/indexeddb/events.py +13 -0
- parsek_cdp/cdp/indexeddb/functions.py +232 -0
- parsek_cdp/cdp/indexeddb/types.py +120 -0
- parsek_cdp/cdp/input/__init__.py +15 -0
- parsek_cdp/cdp/input/events.py +25 -0
- parsek_cdp/cdp/input/functions.py +361 -0
- parsek_cdp/cdp/input/types.py +87 -0
- parsek_cdp/cdp/inspector/__init__.py +15 -0
- parsek_cdp/cdp/inspector/events.py +41 -0
- parsek_cdp/cdp/inspector/functions.py +27 -0
- parsek_cdp/cdp/inspector/types.py +10 -0
- parsek_cdp/cdp/io/__init__.py +15 -0
- parsek_cdp/cdp/io/events.py +13 -0
- parsek_cdp/cdp/io/functions.py +77 -0
- parsek_cdp/cdp/io/types.py +13 -0
- parsek_cdp/cdp/layertree/__init__.py +15 -0
- parsek_cdp/cdp/layertree/events.py +36 -0
- parsek_cdp/cdp/layertree/functions.py +178 -0
- parsek_cdp/cdp/layertree/types.py +104 -0
- parsek_cdp/cdp/log/__init__.py +15 -0
- parsek_cdp/cdp/log/events.py +22 -0
- parsek_cdp/cdp/log/functions.py +54 -0
- parsek_cdp/cdp/log/types.py +57 -0
- parsek_cdp/cdp/media/__init__.py +15 -0
- parsek_cdp/cdp/media/events.py +87 -0
- parsek_cdp/cdp/media/functions.py +27 -0
- parsek_cdp/cdp/media/types.py +101 -0
- parsek_cdp/cdp/memory/__init__.py +15 -0
- parsek_cdp/cdp/memory/events.py +13 -0
- parsek_cdp/cdp/memory/functions.py +164 -0
- parsek_cdp/cdp/memory/types.py +71 -0
- parsek_cdp/cdp/mixins/__init__.py +21 -0
- parsek_cdp/cdp/mixins/datatype.py +123 -0
- parsek_cdp/cdp/mixins/event.py +78 -0
- parsek_cdp/cdp/mixins/namespace.py +31 -0
- parsek_cdp/cdp/network/__init__.py +15 -0
- parsek_cdp/cdp/network/events.py +726 -0
- parsek_cdp/cdp/network/functions.py +734 -0
- parsek_cdp/cdp/network/types.py +1247 -0
- parsek_cdp/cdp/overlay/__init__.py +15 -0
- parsek_cdp/cdp/overlay/events.py +54 -0
- parsek_cdp/cdp/overlay/functions.py +407 -0
- parsek_cdp/cdp/overlay/types.py +328 -0
- parsek_cdp/cdp/page/__init__.py +15 -0
- parsek_cdp/cdp/page/events.py +416 -0
- parsek_cdp/cdp/page/functions.py +1081 -0
- parsek_cdp/cdp/page/types.py +1088 -0
- parsek_cdp/cdp/performance/__init__.py +15 -0
- parsek_cdp/cdp/performance/events.py +24 -0
- parsek_cdp/cdp/performance/functions.py +63 -0
- parsek_cdp/cdp/performance/types.py +22 -0
- parsek_cdp/cdp/performancetimeline/__init__.py +15 -0
- parsek_cdp/cdp/performancetimeline/events.py +22 -0
- parsek_cdp/cdp/performancetimeline/functions.py +30 -0
- parsek_cdp/cdp/performancetimeline/types.py +85 -0
- parsek_cdp/cdp/preload/__init__.py +15 -0
- parsek_cdp/cdp/preload/events.py +116 -0
- parsek_cdp/cdp/preload/functions.py +25 -0
- parsek_cdp/cdp/preload/types.py +267 -0
- parsek_cdp/cdp/profiler/__init__.py +15 -0
- parsek_cdp/cdp/profiler/events.py +64 -0
- parsek_cdp/cdp/profiler/functions.py +134 -0
- parsek_cdp/cdp/profiler/types.py +105 -0
- parsek_cdp/cdp/pwa/__init__.py +15 -0
- parsek_cdp/cdp/pwa/events.py +13 -0
- parsek_cdp/cdp/pwa/functions.py +196 -0
- parsek_cdp/cdp/pwa/types.py +45 -0
- parsek_cdp/cdp/runtime/__init__.py +15 -0
- parsek_cdp/cdp/runtime/events.py +123 -0
- parsek_cdp/cdp/runtime/functions.py +552 -0
- parsek_cdp/cdp/runtime/types.py +319 -0
- parsek_cdp/cdp/schema/__init__.py +15 -0
- parsek_cdp/cdp/schema/events.py +13 -0
- parsek_cdp/cdp/schema/functions.py +32 -0
- parsek_cdp/cdp/schema/types.py +22 -0
- parsek_cdp/cdp/security/__init__.py +15 -0
- parsek_cdp/cdp/security/events.py +64 -0
- parsek_cdp/cdp/security/functions.py +66 -0
- parsek_cdp/cdp/security/types.py +168 -0
- parsek_cdp/cdp/serviceworker/__init__.py +15 -0
- parsek_cdp/cdp/serviceworker/events.py +43 -0
- parsek_cdp/cdp/serviceworker/functions.py +115 -0
- parsek_cdp/cdp/serviceworker/types.py +95 -0
- parsek_cdp/cdp/storage/__init__.py +15 -0
- parsek_cdp/cdp/storage/events.py +288 -0
- parsek_cdp/cdp/storage/functions.py +571 -0
- parsek_cdp/cdp/storage/types.py +656 -0
- parsek_cdp/cdp/systeminfo/__init__.py +15 -0
- parsek_cdp/cdp/systeminfo/events.py +13 -0
- parsek_cdp/cdp/systeminfo/functions.py +75 -0
- parsek_cdp/cdp/systeminfo/types.py +132 -0
- parsek_cdp/cdp/target/__init__.py +15 -0
- parsek_cdp/cdp/target/events.py +109 -0
- parsek_cdp/cdp/target/functions.py +424 -0
- parsek_cdp/cdp/target/types.py +83 -0
- parsek_cdp/cdp/tethering/__init__.py +15 -0
- parsek_cdp/cdp/tethering/events.py +22 -0
- parsek_cdp/cdp/tethering/functions.py +35 -0
- parsek_cdp/cdp/tethering/types.py +10 -0
- parsek_cdp/cdp/tracing/__init__.py +15 -0
- parsek_cdp/cdp/tracing/events.py +61 -0
- parsek_cdp/cdp/tracing/functions.py +119 -0
- parsek_cdp/cdp/tracing/types.py +81 -0
- parsek_cdp/cdp/webaudio/__init__.py +15 -0
- parsek_cdp/cdp/webaudio/events.py +184 -0
- parsek_cdp/cdp/webaudio/functions.py +51 -0
- parsek_cdp/cdp/webaudio/types.py +155 -0
- parsek_cdp/cdp/webauthn/__init__.py +15 -0
- parsek_cdp/cdp/webauthn/events.py +69 -0
- parsek_cdp/cdp/webauthn/functions.py +214 -0
- parsek_cdp/cdp/webauthn/types.py +96 -0
- parsek_cdp/core/__init__.py +18 -0
- parsek_cdp/core/browser.py +240 -0
- parsek_cdp/core/browser_context.py +58 -0
- parsek_cdp/core/element.py +405 -0
- parsek_cdp/core/feature.py +376 -0
- parsek_cdp/core/frame.py +351 -0
- parsek_cdp/core/pagable.py +175 -0
- parsek_cdp/core/page.py +163 -0
- parsek_cdp/core/target.py +490 -0
- parsek_cdp/features/__init__.py +14 -0
- parsek_cdp/features/request_listener/__init__.py +256 -0
- parsek_cdp/features/request_listener/events.py +48 -0
- parsek_cdp/features/request_listener/types.py +235 -0
- parsek_cdp/generate.py +642 -0
- parsek_cdp/parsek/__init__.py +18 -0
- parsek_cdp/parsek/events.py +38 -0
- parsek_cdp/parsek/types.py +36 -0
- parsek_cdp/py.typed +0 -0
- parsek_cdp-0.1.0.dist-info/METADATA +277 -0
- parsek_cdp-0.1.0.dist-info/RECORD +242 -0
- parsek_cdp-0.1.0.dist-info/WHEEL +5 -0
- parsek_cdp-0.1.0.dist-info/licenses/LICENSE +201 -0
- parsek_cdp-0.1.0.dist-info/top_level.txt +1 -0
parsek_cdp/generate.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Code generator for the ``cdp/`` package.
|
|
3
|
+
|
|
4
|
+
Reads the Chrome DevTools Protocol JSON definitions and emits, per domain:
|
|
5
|
+
|
|
6
|
+
cdp/{domain}/types custom types + enums
|
|
7
|
+
cdp/{domain}/functions commands as typed methods on a domain class
|
|
8
|
+
cdp/{domain}/events event classes with handler registration
|
|
9
|
+
|
|
10
|
+
Every ``$ref`` is resolved at generation time (alias chains are followed to
|
|
11
|
+
their concrete shape) so the runtime only ever needs to look up enums and
|
|
12
|
+
object types -- which it does lazily by fully-qualified name, sidestepping the
|
|
13
|
+
circular cross-domain imports present in the protocol.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
python -m parsek_cdp.generate <commit> # download a devtools-protocol ref
|
|
18
|
+
python -m parsek_cdp.generate path/to/json # local JSON directory
|
|
19
|
+
|
|
20
|
+
The single argument is a devtools-protocol commit/tag/branch (downloaded from
|
|
21
|
+
GitHub) when it is not an existing directory; otherwise it is treated as a path
|
|
22
|
+
to a directory holding ``js_protocol.json`` + ``browser_protocol.json``. Output
|
|
23
|
+
goes to ``packages/client/parsek_cdp/cdp``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import keyword
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import urllib.request
|
|
34
|
+
from typing import Optional
|
|
35
|
+
|
|
36
|
+
# this file lives at <repo>/packages/client/parsek_cdp/generate.py
|
|
37
|
+
CLIENT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
38
|
+
PY_OUT = os.path.join(CLIENT, "parsek_cdp", "cdp")
|
|
39
|
+
|
|
40
|
+
#: Raw protocol JSON for a given commit/tag/branch of the upstream repo.
|
|
41
|
+
UPSTREAM = (
|
|
42
|
+
"https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/"
|
|
43
|
+
"{ref}/json/{name}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
PROTOCOL_FILES = ("js_protocol.json", "browser_protocol.json")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_domains(json_dir: str) -> list[dict]:
|
|
50
|
+
"""Read the two protocol JSON files from a local directory."""
|
|
51
|
+
domains: list[dict] = []
|
|
52
|
+
for name in PROTOCOL_FILES:
|
|
53
|
+
with open(os.path.join(json_dir, name)) as fh:
|
|
54
|
+
domains += json.load(fh)["domains"]
|
|
55
|
+
return domains
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def fetch_domains(ref: str) -> list[dict]:
|
|
59
|
+
"""Download the protocol JSON for a devtools-protocol commit/tag/branch."""
|
|
60
|
+
domains: list[dict] = []
|
|
61
|
+
for name in PROTOCOL_FILES:
|
|
62
|
+
url = UPSTREAM.format(ref=ref, name=name)
|
|
63
|
+
print(f"fetching {url}")
|
|
64
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310 -- fixed https host
|
|
65
|
+
domains += json.load(resp)["domains"]
|
|
66
|
+
return domains
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def classify_type(t: dict) -> str:
|
|
70
|
+
"""Return ``"enum"``, ``"object"`` or ``"alias"`` for a top-level type."""
|
|
71
|
+
if "enum" in t:
|
|
72
|
+
return "enum"
|
|
73
|
+
if t.get("type") == "object" and t.get("properties"):
|
|
74
|
+
return "object"
|
|
75
|
+
return "alias"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# qualified name "Domain.Type" -> {"kind", "domain", "spec"}
|
|
79
|
+
TYPES: dict[str, dict] = {}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def index_types(domains: list[dict]) -> None:
|
|
83
|
+
for d in domains:
|
|
84
|
+
dom = d["domain"]
|
|
85
|
+
for t in d.get("types", []):
|
|
86
|
+
TYPES[f"{dom}.{t['id']}"] = {
|
|
87
|
+
"kind": classify_type(t),
|
|
88
|
+
"domain": dom,
|
|
89
|
+
"spec": t,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def qualify(ref: str, domain: str) -> str:
|
|
94
|
+
return ref if "." in ref else f"{domain}.{ref}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def snake(name: str) -> str:
|
|
98
|
+
s = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
99
|
+
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
|
|
100
|
+
return s.lower()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def py_ident(name: str) -> str:
|
|
104
|
+
s = snake(name)
|
|
105
|
+
if keyword.iskeyword(s) or keyword.issoftkeyword(s) or s in ("self", "cls"):
|
|
106
|
+
s += "_"
|
|
107
|
+
return s
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def camel(name: str) -> str:
|
|
111
|
+
if name and name[0].isupper():
|
|
112
|
+
return name[0].lower() + name[1:]
|
|
113
|
+
return name
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def pascal(name: str) -> str:
|
|
117
|
+
return name[0].upper() + name[1:] if name else name
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def enum_member(value: str) -> str:
|
|
121
|
+
s = re.sub(r"[^0-9a-zA-Z]+", "_", value).strip("_")
|
|
122
|
+
if not s or s[0].isdigit():
|
|
123
|
+
s = "V_" + s
|
|
124
|
+
return s.upper()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def docstring(text: Optional[str], indent: str = " ") -> str:
|
|
128
|
+
if not text:
|
|
129
|
+
return ""
|
|
130
|
+
text = text.replace("\\", "\\\\").replace('"""', "'''")
|
|
131
|
+
lines = text.split("\n")
|
|
132
|
+
if len(lines) == 1:
|
|
133
|
+
return f'{indent}"""{lines[0]}"""\n'
|
|
134
|
+
body = "\n".join(indent + ln for ln in lines)
|
|
135
|
+
return f'{indent}"""\n{body}\n{indent}"""\n'
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def resolve_meta(spec: dict, domain: str) -> tuple[str, Optional[str], Optional[tuple]]:
|
|
139
|
+
"""Resolve a value spec to ``(kind, ref, inner)`` following alias chains."""
|
|
140
|
+
if "$ref" in spec:
|
|
141
|
+
q = qualify(spec["$ref"], domain)
|
|
142
|
+
desc = TYPES[q]
|
|
143
|
+
if desc["kind"] == "enum":
|
|
144
|
+
return ("enum", q, None)
|
|
145
|
+
if desc["kind"] == "object":
|
|
146
|
+
return ("object", q, None)
|
|
147
|
+
return resolve_meta(desc["spec"], desc["domain"]) # alias -> follow
|
|
148
|
+
t = spec.get("type")
|
|
149
|
+
if t == "array":
|
|
150
|
+
return ("array", None, resolve_meta(spec["items"], domain))
|
|
151
|
+
return ("primitive", None, None)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
PY_PRIMITIVES = {
|
|
155
|
+
"string": "str",
|
|
156
|
+
"integer": "int",
|
|
157
|
+
"number": "float",
|
|
158
|
+
"boolean": "bool",
|
|
159
|
+
"object": "Dict[str, Any]",
|
|
160
|
+
"any": "Any",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class PyTypeCtx:
|
|
165
|
+
"""Collects cross-domain references needed for TYPE_CHECKING imports."""
|
|
166
|
+
|
|
167
|
+
def __init__(self, domain: str):
|
|
168
|
+
self.domain = domain
|
|
169
|
+
self.crossrefs: set[tuple[str, str]] = set() # (domain, TypeName)
|
|
170
|
+
self.localrefs: set[str] = set() # same-domain TypeName
|
|
171
|
+
|
|
172
|
+
def py_type(self, spec: dict) -> str:
|
|
173
|
+
if "$ref" in spec:
|
|
174
|
+
q = qualify(spec["$ref"], self.domain)
|
|
175
|
+
dom, tname = q.split(".", 1)
|
|
176
|
+
if dom == self.domain:
|
|
177
|
+
self.localrefs.add(tname)
|
|
178
|
+
return tname
|
|
179
|
+
self.crossrefs.add((dom, tname))
|
|
180
|
+
return f"{dom}_{tname}"
|
|
181
|
+
if "enum" in spec:
|
|
182
|
+
return "Literal[" + ", ".join(repr(e) for e in spec["enum"]) + "]"
|
|
183
|
+
t = spec["type"]
|
|
184
|
+
if t == "array":
|
|
185
|
+
return f"List[{self.py_type(spec['items'])}]"
|
|
186
|
+
return PY_PRIMITIVES[t]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def fm_inner_literal(meta: tuple) -> str:
|
|
190
|
+
kind, ref, inner = meta
|
|
191
|
+
parts = [repr(""), repr(""), "False", repr(kind)]
|
|
192
|
+
if ref:
|
|
193
|
+
parts.append(f"ref={ref!r}")
|
|
194
|
+
if inner:
|
|
195
|
+
parts.append(f"inner={fm_inner_literal(inner)}")
|
|
196
|
+
return "FieldMeta(" + ", ".join(parts) + ")"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def fm_literal(py_name: str, json_name: str, optional: bool, meta: tuple) -> str:
|
|
200
|
+
kind, ref, inner = meta
|
|
201
|
+
parts = [repr(py_name), repr(json_name), repr(optional), repr(kind)]
|
|
202
|
+
if ref:
|
|
203
|
+
parts.append(f"ref={ref!r}")
|
|
204
|
+
if inner:
|
|
205
|
+
parts.append(f"inner={fm_inner_literal(inner)}")
|
|
206
|
+
return "FieldMeta(" + ", ".join(parts) + ")"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def py_crossref_imports(ctx: PyTypeCtx) -> str:
|
|
210
|
+
"""Explicit, aliased cross-domain type imports under ``TYPE_CHECKING``.
|
|
211
|
+
|
|
212
|
+
Kept type-check-only so the (string, lazy) annotations resolve without
|
|
213
|
+
risking the circular runtime imports present across domains.
|
|
214
|
+
"""
|
|
215
|
+
if not ctx.crossrefs:
|
|
216
|
+
return ""
|
|
217
|
+
lines = ["if TYPE_CHECKING:"]
|
|
218
|
+
for dom, tname in sorted(ctx.crossrefs):
|
|
219
|
+
lines.append(f" from ..{dom.lower()}.types import {tname} as {dom}_{tname}")
|
|
220
|
+
return "\n".join(lines) + "\n"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def py_typing_import(ctx: PyTypeCtx, *, include_local: bool = False) -> str:
|
|
224
|
+
"""The ``from typing import ...`` line, including TYPE_CHECKING only if used."""
|
|
225
|
+
names = ["Any", "ClassVar", "Dict", "List", "Literal", "Optional"]
|
|
226
|
+
needs_tc = bool(ctx.crossrefs) or (include_local and bool(ctx.localrefs))
|
|
227
|
+
if needs_tc:
|
|
228
|
+
names = ["TYPE_CHECKING", *names]
|
|
229
|
+
return "from typing import " + ", ".join(names)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def py_type_imports(ctx: PyTypeCtx) -> str:
|
|
233
|
+
"""Explicit same-domain + cross-domain type imports, all under TYPE_CHECKING.
|
|
234
|
+
|
|
235
|
+
Annotations are lazy strings (``from __future__ import annotations``) and the
|
|
236
|
+
runtime resolves nested types via the registry by name, so no type import is
|
|
237
|
+
needed at runtime -- keeping them type-check-only also rules out any import
|
|
238
|
+
cycle. No star imports.
|
|
239
|
+
"""
|
|
240
|
+
if not ctx.localrefs and not ctx.crossrefs:
|
|
241
|
+
return ""
|
|
242
|
+
lines = ["if TYPE_CHECKING:"]
|
|
243
|
+
if ctx.localrefs:
|
|
244
|
+
names = sorted(ctx.localrefs)
|
|
245
|
+
if len(names) == 1:
|
|
246
|
+
lines.append(f" from .types import {names[0]}")
|
|
247
|
+
else:
|
|
248
|
+
lines.append(" from .types import (")
|
|
249
|
+
lines += [f" {n}," for n in names]
|
|
250
|
+
lines.append(" )")
|
|
251
|
+
for dom, tname in sorted(ctx.crossrefs):
|
|
252
|
+
lines.append(f" from ..{dom.lower()}.types import {tname} as {dom}_{tname}")
|
|
253
|
+
return "\n".join(lines) + "\n"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def gen_py_dataclass(
|
|
257
|
+
name: str,
|
|
258
|
+
qualified: str,
|
|
259
|
+
properties: list,
|
|
260
|
+
domain: str,
|
|
261
|
+
ctx: PyTypeCtx,
|
|
262
|
+
base: str,
|
|
263
|
+
decorators: list[str],
|
|
264
|
+
description: Optional[str],
|
|
265
|
+
) -> str:
|
|
266
|
+
"""Emit a ``@dataclass`` subclass with a ``__FIELDS__`` table."""
|
|
267
|
+
fields = [] # (py_name, json_name, type_str, optional, meta)
|
|
268
|
+
for p in properties:
|
|
269
|
+
json_name = p["name"]
|
|
270
|
+
py_name = py_ident(json_name)
|
|
271
|
+
type_str = ctx.py_type(p)
|
|
272
|
+
optional = bool(p.get("optional"))
|
|
273
|
+
meta = resolve_meta(p, domain)
|
|
274
|
+
fields.append(
|
|
275
|
+
(py_name, json_name, type_str, optional, meta, p.get("description"))
|
|
276
|
+
)
|
|
277
|
+
# required first, optionals last (dataclass ordering)
|
|
278
|
+
fields.sort(key=lambda f: f[3])
|
|
279
|
+
|
|
280
|
+
out = []
|
|
281
|
+
out += decorators
|
|
282
|
+
out.append(f"class {name}({base}):")
|
|
283
|
+
body = docstring(description)
|
|
284
|
+
for py_name, json_name, type_str, optional, meta, _desc in fields:
|
|
285
|
+
if optional:
|
|
286
|
+
body += f" {py_name}: Optional[{type_str}] = None\n"
|
|
287
|
+
else:
|
|
288
|
+
body += f" {py_name}: {type_str}\n"
|
|
289
|
+
# __FIELDS__ table
|
|
290
|
+
body += " __FIELDS__: ClassVar[tuple] = (\n"
|
|
291
|
+
for py_name, json_name, type_str, optional, meta, _desc in fields:
|
|
292
|
+
body += " " + fm_literal(py_name, json_name, optional, meta) + ",\n"
|
|
293
|
+
body += " )\n"
|
|
294
|
+
if not fields:
|
|
295
|
+
body = docstring(description) or " pass\n"
|
|
296
|
+
body += " __FIELDS__: ClassVar[tuple] = ()\n"
|
|
297
|
+
out.append(body)
|
|
298
|
+
return "\n".join(out)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def gen_py_types(domain: dict) -> str:
|
|
302
|
+
dom = domain["domain"]
|
|
303
|
+
ctx = PyTypeCtx(dom)
|
|
304
|
+
blocks: list[str] = []
|
|
305
|
+
|
|
306
|
+
for t in domain.get("types", []):
|
|
307
|
+
kind = classify_type(t)
|
|
308
|
+
qualified = f"{dom}.{t['id']}"
|
|
309
|
+
if kind == "enum":
|
|
310
|
+
block = [f'@register("{qualified}")', f"class {t['id']}(str, Enum):"]
|
|
311
|
+
ds = docstring(t.get("description"))
|
|
312
|
+
body = ds
|
|
313
|
+
for v in t["enum"]:
|
|
314
|
+
body += f" {enum_member(v)} = {v!r}\n"
|
|
315
|
+
block.append(body)
|
|
316
|
+
blocks.append("\n".join(block))
|
|
317
|
+
elif kind == "object":
|
|
318
|
+
blocks.append(
|
|
319
|
+
gen_py_dataclass(
|
|
320
|
+
t["id"],
|
|
321
|
+
qualified,
|
|
322
|
+
t["properties"],
|
|
323
|
+
dom,
|
|
324
|
+
ctx,
|
|
325
|
+
base="DataType",
|
|
326
|
+
decorators=[f'@register("{qualified}")', "@dataclass"],
|
|
327
|
+
description=t.get("description"),
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
else: # alias -- PEP 695 lazy alias avoids evaluating cross-domain refs
|
|
331
|
+
type_str = ctx.py_type(t)
|
|
332
|
+
ds = ""
|
|
333
|
+
if t.get("description"):
|
|
334
|
+
ds = f" # {t['description'].splitlines()[0]}"
|
|
335
|
+
blocks.append(f"type {t['id']} = {type_str}{ds}\n")
|
|
336
|
+
|
|
337
|
+
header = [
|
|
338
|
+
'"""Custom types and enums for the {} domain (generated)."""'.format(dom),
|
|
339
|
+
"from __future__ import annotations",
|
|
340
|
+
"",
|
|
341
|
+
"from dataclasses import dataclass",
|
|
342
|
+
"from enum import Enum",
|
|
343
|
+
py_typing_import(ctx),
|
|
344
|
+
"",
|
|
345
|
+
"from ..mixins.datatype import DataType, FieldMeta, register",
|
|
346
|
+
"",
|
|
347
|
+
py_crossref_imports(ctx),
|
|
348
|
+
"",
|
|
349
|
+
]
|
|
350
|
+
names = sorted(t["id"] for t in domain.get("types", []))
|
|
351
|
+
all_block = "__all__ = [" + ", ".join(f'"{n}"' for n in names) + "]\n"
|
|
352
|
+
body = "\n".join(header) + "\n\n".join(blocks)
|
|
353
|
+
return body.rstrip("\n") + "\n\n" + all_block
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def gen_py_events(domain: dict) -> str:
|
|
357
|
+
dom = domain["domain"]
|
|
358
|
+
ctx = PyTypeCtx(dom)
|
|
359
|
+
blocks: list[str] = []
|
|
360
|
+
for e in domain.get("events", []):
|
|
361
|
+
qualified = f"{dom}.{e['name']}"
|
|
362
|
+
name = pascal(e["name"])
|
|
363
|
+
blocks.append(
|
|
364
|
+
gen_py_dataclass(
|
|
365
|
+
name,
|
|
366
|
+
qualified,
|
|
367
|
+
e.get("parameters", []),
|
|
368
|
+
dom,
|
|
369
|
+
ctx,
|
|
370
|
+
base="Event",
|
|
371
|
+
decorators=[f'@register_event("{qualified}")', "@dataclass"],
|
|
372
|
+
description=e.get("description"),
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
header = [
|
|
376
|
+
'"""Events for the {} domain (generated)."""'.format(dom),
|
|
377
|
+
"from __future__ import annotations",
|
|
378
|
+
"",
|
|
379
|
+
"from dataclasses import dataclass",
|
|
380
|
+
py_typing_import(ctx, include_local=True),
|
|
381
|
+
"",
|
|
382
|
+
"from ..mixins.datatype import FieldMeta",
|
|
383
|
+
"from ..mixins.event import Event, register_event",
|
|
384
|
+
"",
|
|
385
|
+
py_type_imports(ctx),
|
|
386
|
+
"",
|
|
387
|
+
]
|
|
388
|
+
names = sorted(pascal(e["name"]) for e in domain.get("events", []))
|
|
389
|
+
all_block = "__all__ = [" + ", ".join(f'"{n}"' for n in names) + "]\n"
|
|
390
|
+
if not blocks:
|
|
391
|
+
blocks = ["# This domain defines no events.\n"]
|
|
392
|
+
body = "\n".join(header) + "\n\n".join(blocks)
|
|
393
|
+
return body.rstrip("\n") + "\n\n" + all_block
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def gen_py_functions(domain: dict) -> str:
|
|
397
|
+
dom = domain["domain"]
|
|
398
|
+
ctx = PyTypeCtx(dom)
|
|
399
|
+
return_classes: list[str] = []
|
|
400
|
+
methods: list[str] = []
|
|
401
|
+
|
|
402
|
+
for c in domain.get("commands", []):
|
|
403
|
+
method_name = f"{dom}.{c['name']}"
|
|
404
|
+
py_method = py_ident(c["name"])
|
|
405
|
+
params = c.get("parameters", [])
|
|
406
|
+
returns = c.get("returns", [])
|
|
407
|
+
|
|
408
|
+
# parameters sorted: required first so the signature is valid
|
|
409
|
+
sig_params = []
|
|
410
|
+
for p in params:
|
|
411
|
+
json_name = p["name"]
|
|
412
|
+
pid = py_ident(json_name)
|
|
413
|
+
tstr = ctx.py_type(p)
|
|
414
|
+
optional = bool(p.get("optional"))
|
|
415
|
+
meta = resolve_meta(p, dom)
|
|
416
|
+
sig_params.append((pid, json_name, tstr, optional, meta))
|
|
417
|
+
sig_params.sort(key=lambda x: x[3])
|
|
418
|
+
|
|
419
|
+
# return type
|
|
420
|
+
ret_type = "None"
|
|
421
|
+
ret_cls_name = None
|
|
422
|
+
if returns:
|
|
423
|
+
ret_cls_name = f"{pascal(c['name'])}Return"
|
|
424
|
+
ret_type = ret_cls_name
|
|
425
|
+
return_classes.append(
|
|
426
|
+
gen_py_dataclass(
|
|
427
|
+
ret_cls_name,
|
|
428
|
+
f"{method_name}.return",
|
|
429
|
+
returns,
|
|
430
|
+
dom,
|
|
431
|
+
ctx,
|
|
432
|
+
base="DataType",
|
|
433
|
+
decorators=["@dataclass"],
|
|
434
|
+
description=f"Return value of :meth:`{dom}.{py_method}`.",
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# signature
|
|
439
|
+
# annotations are lazy strings thanks to ``from __future__ import
|
|
440
|
+
# annotations``, so cross-domain names only imported under TYPE_CHECKING
|
|
441
|
+
# are never evaluated at runtime.
|
|
442
|
+
sig = "self"
|
|
443
|
+
if sig_params:
|
|
444
|
+
sig += ", *"
|
|
445
|
+
for pid, _jn, tstr, optional, _m in sig_params:
|
|
446
|
+
if optional:
|
|
447
|
+
sig += f", {pid}: Optional[{tstr}] = None"
|
|
448
|
+
else:
|
|
449
|
+
sig += f", {pid}: {tstr}"
|
|
450
|
+
|
|
451
|
+
body = f" async def {py_method}({sig}) -> {ret_type}:\n"
|
|
452
|
+
# docstring with param descriptions
|
|
453
|
+
doc_lines = []
|
|
454
|
+
if c.get("description"):
|
|
455
|
+
doc_lines.append(c["description"])
|
|
456
|
+
if c.get("deprecated"):
|
|
457
|
+
doc_lines.append("\n.. deprecated::")
|
|
458
|
+
for p in params:
|
|
459
|
+
d = p.get("description", "")
|
|
460
|
+
doc_lines.append(f":param {py_ident(p['name'])}: {d}".rstrip())
|
|
461
|
+
if doc_lines:
|
|
462
|
+
body += docstring("\n".join(doc_lines), indent=" ")
|
|
463
|
+
body += " _params: Dict[str, Any] = {}\n"
|
|
464
|
+
for pid, json_name, _tstr, optional, meta in sig_params:
|
|
465
|
+
enc = f"encode({fm_inner_literal(meta)}, {pid})"
|
|
466
|
+
if optional:
|
|
467
|
+
body += f" if {pid} is not None:\n"
|
|
468
|
+
body += f" _params[{json_name!r}] = {enc}\n"
|
|
469
|
+
else:
|
|
470
|
+
body += f" _params[{json_name!r}] = {enc}\n"
|
|
471
|
+
body += f" _result = await self._target.send({method_name!r}, _params)\n"
|
|
472
|
+
if ret_cls_name:
|
|
473
|
+
body += f" return {ret_cls_name}.from_json(_result)\n"
|
|
474
|
+
else:
|
|
475
|
+
body += " return None\n"
|
|
476
|
+
methods.append(body)
|
|
477
|
+
|
|
478
|
+
header = [
|
|
479
|
+
'"""Commands for the {} domain (generated)."""'.format(dom),
|
|
480
|
+
"from __future__ import annotations",
|
|
481
|
+
"",
|
|
482
|
+
"from dataclasses import dataclass",
|
|
483
|
+
py_typing_import(ctx, include_local=True),
|
|
484
|
+
"",
|
|
485
|
+
"from ..mixins.datatype import DataType, FieldMeta, encode",
|
|
486
|
+
"",
|
|
487
|
+
py_type_imports(ctx),
|
|
488
|
+
"",
|
|
489
|
+
]
|
|
490
|
+
|
|
491
|
+
cls = [
|
|
492
|
+
f"class {dom}:",
|
|
493
|
+
f' """Commands of the {dom} domain, bound to a target."""',
|
|
494
|
+
"",
|
|
495
|
+
" def __init__(self, target: Any) -> None:",
|
|
496
|
+
" self._target = target",
|
|
497
|
+
"",
|
|
498
|
+
]
|
|
499
|
+
if not methods:
|
|
500
|
+
cls.append(" # This domain defines no commands.\n")
|
|
501
|
+
out = "\n".join(header) + "\n\n".join(return_classes)
|
|
502
|
+
if return_classes:
|
|
503
|
+
out += "\n\n"
|
|
504
|
+
out += "\n".join(cls) + "\n\n" + "\n".join(methods)
|
|
505
|
+
return out
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def gen_py_domain_init(domain: dict) -> str:
|
|
509
|
+
dom = domain["domain"]
|
|
510
|
+
return (
|
|
511
|
+
f'"""The {dom} CDP domain (generated).\n\n'
|
|
512
|
+
f"Importing ``{dom}`` from ``parsek_cdp.cdp`` gives a namespace with this\n"
|
|
513
|
+
f"domain's events and types as attributes (``{dom}.SomeEvent`` /\n"
|
|
514
|
+
f"``{dom}.SomeType``); commands run on a target via ``page.cdp.{dom}``.\n"
|
|
515
|
+
'"""\n'
|
|
516
|
+
"from . import events, functions, types\n"
|
|
517
|
+
f"from .functions import {dom}\n"
|
|
518
|
+
"from .events import * # noqa: F401,F403 -- expose events on the namespace\n"
|
|
519
|
+
"from .types import * # noqa: F401,F403 -- expose types on the namespace\n\n"
|
|
520
|
+
f"#: Protocol domain name (used by features that declare ``domains=({dom},)``).\n"
|
|
521
|
+
f'DOMAIN = "{dom}"\n\n'
|
|
522
|
+
'__all__ = ["events", "functions", "types", "' + dom + '", "DOMAIN"]\n'
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def gen_py_cdp_init(domains: list[dict]) -> str:
|
|
527
|
+
lines = [
|
|
528
|
+
'"""Generated CDP package: every protocol domain.',
|
|
529
|
+
"",
|
|
530
|
+
"Each domain is importable as a namespace::",
|
|
531
|
+
"",
|
|
532
|
+
" from parsek_cdp.cdp import Network",
|
|
533
|
+
" Network.RequestWillBeSent # an event class",
|
|
534
|
+
" Network.ResourceType # a type / enum",
|
|
535
|
+
" await page.cdp.Network.enable() # a command (target-bound)",
|
|
536
|
+
'"""',
|
|
537
|
+
"",
|
|
538
|
+
"from __future__ import annotations",
|
|
539
|
+
"",
|
|
540
|
+
"from typing import TYPE_CHECKING",
|
|
541
|
+
"",
|
|
542
|
+
]
|
|
543
|
+
|
|
544
|
+
# Imports are emitted in isort order (alphabetical by module name). The
|
|
545
|
+
# explanatory comments stay attached above the first protocol domain's
|
|
546
|
+
# import, which is where they land once the block is sorted.
|
|
547
|
+
first_mod = domains[0]["domain"].lower()
|
|
548
|
+
sorted_doms = sorted(domains, key=lambda d: d["domain"].lower())
|
|
549
|
+
|
|
550
|
+
for d in sorted_doms:
|
|
551
|
+
mod = d["domain"].lower()
|
|
552
|
+
if mod == first_mod:
|
|
553
|
+
lines.append("")
|
|
554
|
+
lines.append("# Domain namespaces (events + types + command class).")
|
|
555
|
+
lines.append(f"from . import {mod} as {d['domain']}")
|
|
556
|
+
for d in sorted_doms:
|
|
557
|
+
mod = d["domain"].lower()
|
|
558
|
+
if mod == first_mod:
|
|
559
|
+
lines.append("")
|
|
560
|
+
lines.append("# Command classes for the per-target aggregator below.")
|
|
561
|
+
line = f"from .{mod}.functions import {d['domain']} as _{d['domain']}"
|
|
562
|
+
if len(line) > 88: # black wraps over-long single imports
|
|
563
|
+
lines.append(f"from .{mod}.functions import (")
|
|
564
|
+
lines.append(f" {d['domain']} as _{d['domain']},")
|
|
565
|
+
lines.append(")")
|
|
566
|
+
else:
|
|
567
|
+
lines.append(line)
|
|
568
|
+
|
|
569
|
+
lines.append("")
|
|
570
|
+
lines.append("if TYPE_CHECKING:")
|
|
571
|
+
lines.append(" from ..core.target import CDPConnection")
|
|
572
|
+
lines.append("")
|
|
573
|
+
lines.append("")
|
|
574
|
+
lines.append("class CDP:")
|
|
575
|
+
lines.append(
|
|
576
|
+
' """Access every CDP domain through a single target (commands)."""'
|
|
577
|
+
)
|
|
578
|
+
lines.append("")
|
|
579
|
+
lines.append(' def __init__(self, target: "CDPConnection") -> None:')
|
|
580
|
+
lines.append(" self.connection = target")
|
|
581
|
+
for d in domains:
|
|
582
|
+
lines.append(f" self.{d['domain']} = _{d['domain']}(target)")
|
|
583
|
+
lines.append("")
|
|
584
|
+
lines.append("")
|
|
585
|
+
names = ['"CDP"'] + [f'"{d["domain"]}"' for d in domains]
|
|
586
|
+
lines.append("__all__ = [")
|
|
587
|
+
for n in names:
|
|
588
|
+
lines.append(f" {n},")
|
|
589
|
+
lines.append("]")
|
|
590
|
+
return "\n".join(lines) + "\n"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# ============================================================================ #
|
|
594
|
+
# Driver
|
|
595
|
+
# ============================================================================ #
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def write(path: str, content: str) -> None:
|
|
599
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
600
|
+
with open(path, "w") as fh:
|
|
601
|
+
fh.write(content)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def generate(domains: list[dict]) -> None:
|
|
605
|
+
"""Emit the whole ``cdp/`` package from the parsed protocol domains."""
|
|
606
|
+
index_types(domains)
|
|
607
|
+
|
|
608
|
+
for d in domains:
|
|
609
|
+
dom = d["domain"].lower()
|
|
610
|
+
write(os.path.join(PY_OUT, dom, "types.py"), gen_py_types(d))
|
|
611
|
+
write(os.path.join(PY_OUT, dom, "events.py"), gen_py_events(d))
|
|
612
|
+
write(os.path.join(PY_OUT, dom, "functions.py"), gen_py_functions(d))
|
|
613
|
+
write(os.path.join(PY_OUT, dom, "__init__.py"), gen_py_domain_init(d))
|
|
614
|
+
|
|
615
|
+
write(os.path.join(PY_OUT, "__init__.py"), gen_py_cdp_init(domains))
|
|
616
|
+
|
|
617
|
+
print(f"Generated {len(domains)} domains -> {PY_OUT}")
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def main(argv: Optional[list[str]] = None) -> None:
|
|
621
|
+
parser = argparse.ArgumentParser(
|
|
622
|
+
prog="python -m parsek_cdp.generate",
|
|
623
|
+
description="Generate the cdp/ package from a Chrome DevTools Protocol "
|
|
624
|
+
"revision (downloaded by commit/tag/branch) or a local JSON directory.",
|
|
625
|
+
)
|
|
626
|
+
parser.add_argument(
|
|
627
|
+
"source",
|
|
628
|
+
help="devtools-protocol commit/tag/branch to download, or a path to a "
|
|
629
|
+
"local directory holding js_protocol.json + browser_protocol.json.",
|
|
630
|
+
)
|
|
631
|
+
args = parser.parse_args(argv)
|
|
632
|
+
|
|
633
|
+
if os.path.isdir(args.source):
|
|
634
|
+
domains = load_domains(args.source)
|
|
635
|
+
else:
|
|
636
|
+
domains = fetch_domains(args.source)
|
|
637
|
+
|
|
638
|
+
generate(domains)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
if __name__ == "__main__":
|
|
642
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The Parsek contract -- the small slice of protocol that is *not* plain CDP.
|
|
2
|
+
|
|
3
|
+
Browser control flows over CDP through the transparent proxy, so the only shared
|
|
4
|
+
contract is the lifecycle of a server-supervised browser: the :class:`BrowserState`
|
|
5
|
+
enum and the :class:`BrowserStateChanged` event the server broadcasts and the
|
|
6
|
+
client consumes. Shipped here in the client distribution so both sides share one
|
|
7
|
+
wire definition.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .events import BrowserStateChanged
|
|
13
|
+
from .types import BrowserState
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BrowserState",
|
|
17
|
+
"BrowserStateChanged",
|
|
18
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Events of the Parsek contract.
|
|
2
|
+
|
|
3
|
+
Only the *protocol-level* events live here (browser lifecycle); per-feature
|
|
4
|
+
``Parsek.*`` events (e.g. ``Parsek.RequestListener.request``) are declared inside
|
|
5
|
+
their own feature package. Every event is a :class:`~parsek_cdp.cdp.mixins.event.Event`
|
|
6
|
+
registered under its ``Parsek.*`` method name, so the client connection dispatch
|
|
7
|
+
deserializes and routes it exactly like a CDP event.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import ClassVar, Optional
|
|
14
|
+
|
|
15
|
+
from ..cdp.mixins.datatype import FieldMeta
|
|
16
|
+
from ..cdp.mixins.event import Event, register_event
|
|
17
|
+
from .types import BrowserState
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register_event("Parsek.browserStateChanged")
|
|
21
|
+
@dataclass
|
|
22
|
+
class BrowserStateChanged(Event):
|
|
23
|
+
"""A supervised browser changed lifecycle state.
|
|
24
|
+
|
|
25
|
+
Emitted by the supervisor on every transition (started, crashed, restarting,
|
|
26
|
+
closed) and broadcast to the browser's control-channel clients. ``reason``
|
|
27
|
+
carries a human-readable cause for the abnormal transitions.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
state: BrowserState
|
|
31
|
+
reason: Optional[str] = None
|
|
32
|
+
browser_uuid: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
35
|
+
FieldMeta("state", "state", False, "enum", ref="Parsek.BrowserState"),
|
|
36
|
+
FieldMeta("reason", "reason", True, "primitive"),
|
|
37
|
+
FieldMeta("browser_uuid", "browserUuid", True, "primitive"),
|
|
38
|
+
)
|